- 后端
- Web框架
- 微服务
- RPC框架
- 异步编程
【免费下载链接】hyperf
🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.
本文是 Hyperf 框架数据库组件中模型关联(Model Relationships)的完整实战指南。Hyperf 的模型基于Hyperf\Database\Model\Model构建,其关联系统沿用了成熟 ORM 的关系建模思想,覆盖一对一(hasOne)、一对多(hasMany)、反向关联(belongsTo)、多对多(belongsToMany)以及多态关联(Polymorphic)等全部常见关系类型,并内置了 N + 1 查询问题的预加载(Eager Loading)解决方案。读完本文,你将掌握在 Hyperf 中定义各类模型关联、通过动态属性与方法链查询关联数据、操作多对多中间表、自定义多态类型映射,以及使用whereHasMorph进行多态存在性查询的完整能力。
在 Hyperf 中定义模型关联
在 Hyperf 中,模型关联(Relationships)以方法的形式定义在模型类中。与模型本身一样,每个关联方法本质上都是一个强大的查询构建器(Query Builder),支持方法链(method chaining)式的连续调用。例如,我们可以在role关联的查询链上追加约束条件:
$user->role()->where('level', 1)->get();之所以能做到这一点,是因为关联方法返回的不是普通的数据集合,而是一个Hyperf\Database\Model\Relations\Relation及其子类的实例(如HasOne、HasMany、BelongsTo、BelongsToMany、MorphOne、MorphTo等,这些类都位于 src/database/src/Model/Relations 目录下)。这些关联对象持有相关的查询构建器,允许你继续叠加where、orderBy、limit等约束,最终再通过get()、first()等方法获取结果。
所有关联的定义入口方法(hasOne、hasMany、belongsTo、belongsToMany、morphOne、morphMany、morphTo、morphToMany等)都由模型基类通过 HasRelationships.php 提供,业务模型只需继承Hyperf\DbConnection\Model\Model即可直接使用。
模型基类的选择
Hyperf 项目中使用DbConnection组件时,模型应继承自Hyperf\DbConnection\Model\Model(即 src/db-connection/src/Model/Model.php 中定义的类),它扩展了Hyperf\Database\Model\Model,并额外注入了容器访问(HasContainer)与仓储访问(HasRepository)能力,能够无缝接入 Hyperf 的依赖注入容器和连接池体系。本文后续示例均基于该类。
One To One:一对一关联
一对一(one-to-one)是最基础的关联形态。例如User模型与Role模型一一对应。定义方式是在User模型中编写role方法,在方法内调用hasOne并返回其结果:
<?php declare(strict_types=1); namespace App\Models; use Hyperf\DbConnection\Model\Model; class User extends Model { public function role() { return $this->hasOne(Role::class, 'user_id', 'id'); } }hasOne方法的签名(见 HasRelationships.php)为:
public function hasOne($related, $foreignKey = null, $localKey = null)- 第一个参数
$related:关联模型(related model)的类名; - 第二个参数
$foreignKey:关联模型表上的外键字段名; - 第三个参数
$localKey:当前模型表上的本地键字段名。
当省略$foreignKey时,框架会调用$this->getForeignKey()自动推断,其规则是将当前模型类名转换为 snake_case 并追加_id后缀(例如User模型对应user_id);省略$localKey时默认使用当前模型的主键(getKeyName())。
定义好关联后,就可以利用 Hyperf 的动态属性(Dynamic Attributes)来获取关联数据——把关联方法当作模型属性一样访问:
$role = User::query()->find(1)->role;动态属性的实现链路为:模型类重载了魔术方法__get(见 src/database/src/Model/Model.php#L144-L147),它委托给getAttribute;getAttribute(见 HasAttributes.php#L194-L217)在确认该键不是普通字段属性、也没有对应的 get 访问器之后,将其交给getRelationValue,最终由getRelationshipFromMethod(见 HasAttributes.php#L871-L894)调用同名方法并校验返回值必须是Relation实例——如果关联方法忘记写return(返回了null)或返回了非关联对象,会抛出LogicException并给出明确的错误提示("Was the 'return' keyword used?"),这在实际开发中非常有助于快速定位问题。
One To Many:一对多关联
一对多(one-to-many)用于表达「一个模型拥有多个相关模型」的关系。例如一位作者(User)可以撰写多本书(Book):
<?php declare(strict_types=1); namespace App\Models; use Hyperf\DbConnection\Model\Model; class User extends Model { public function books() { return $this->hasMany(Book::class, 'user_id', 'id'); } }外键命名约定
Hyperf 会自动推断Book表上的外键字段名。按照约定,外键采用「属主模型名的 snake_case 形式 +_id后缀」,因此上例中User关联到Book的外键就是user_id。hasMany的签名与默认值推断逻辑与hasOne完全一致(见 HasRelationships.php 中hasMany的实现)。
访问一对多关联数据
定义完成后,通过动态属性books即可获得书的集合(Collection):
$books = User::query()->find(1)->books; foreach ($books as $book) { // }同样地,由于关联本质是查询构建器,可以对其追加约束并配合终结方法使用:
$book = User::query()->find(1)->books()->where('title', 'Mastering Hyperf Framework in One Month')->first();One To Many (Inverse):反向关联 belongsTo
既然已经能从作者获取其全部作品,自然也需要能从一本书反向找到它的作者。这个关联是hasMany的逆操作,需要在子模型(Book)上使用belongsTo定义:
<?php declare(strict_types=1); namespace App\Models; use Hyperf\DbConnection\Model\Model; class Book extends Model { public function author() { return $this->belongsTo(User::class, 'user_id', 'id'); } }belongsTo的签名(见 HasRelationships.php)为:
public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null)当省略$foreignKey时,框架会借助 PHP 的 debug backtrace 推断出当前关联方法名(这里即author),再按「snake(方法名) + '_' + 关联模型主键名」的规则推导外键,例如方法名author对应外键author_id。省略$ownerKey时默认使用关联模型的主键。
定义完成后,通过动态属性author即可拿到对应的User模型:
$book = Book::find(1); echo $book->author->name;Many To Many:多对多关联
多对多(many-to-many)比hasOne/hasMany稍复杂。例如一个用户可以拥有多个角色,而同一角色(如 "Administrator")也可以被多个用户共享。此时需要三张数据表:users、roles以及中间表(join table)role_user。中间表的命名基于两个关联模型名的字母顺序拼接(role+user=role_user),表中包含user_id和role_id两个外键字段。
多对多关联通过调用belongsToMany定义。例如在User模型中定义roles方法:
<?php namespace App; use Hyperf\DbConnection\Model\Model; class User extends Model { public function roles() { return $this->belongsToMany(Role::class); } }定义完成后,通过动态属性roles获取用户的角色集合:
$user = User::query()->find(1); foreach ($user->roles as $role) { // }同样地,roles方法也可以作为查询构建器使用,并通过方法链追加约束:
$roles = User::find(1)->roles()->orderBy('name')->get();自定义中间表与键名
如前述,默认情况下 Hyperf 会按两个关联模型名的字母顺序拼接出中间表名。你也可以通过belongsToMany的第二个参数显式指定:
return $this->belongsToMany(Role::class, 'role_user');belongsToMany的完整签名(见 HasRelationships.php)支持更多自定义项:
public function belongsToMany( $related, // 关联模型类名 $table = null, // 中间表名 $foreignPivotKey = null, // 定义方在中间表中的外键名 $relatedPivotKey = null, // 关联方在中间表中的外键名 $parentKey = null, // 定义方本地键 $relatedKey = null, // 关联方本地键 $relation = null // 关联名(默认从方法名推断) )其中第三个参数是定义关联的模型在中间表中的外键名(如user_id),第四个参数是另一模型在中间表中的外键名(如role_id):
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');访问中间表字段(pivot)
多对多关联依赖中间表存储关联元信息。Hyperf 提供了一系列方法用于操作这张表。例如User关联多个Role,取回关联对象后,可通过每个Role模型上的pivot属性访问中间表数据:
$user = User::find(1); foreach ($user->roles as $role) { echo $role->pivot->created_at; }需要注意:每个取回的Role模型都会被自动附加一个pivot属性,它代表中间表对应的模型对象(对应 Pivot.php 中的Pivot类),用法与普通 Hyperf 模型一致。
默认情况下,pivot对象只包含两个关联模型的主键。如果中间表还有其他业务字段,必须在定义关联时显式声明(withPivot):
return $this->belongsToMany(Role::class)->withPivot('column1', 'column2');如果希望中间表自动维护created_at与updated_at时间戳,追加withTimestamps即可:
return $this->belongsToMany(Role::class)->withTimestamps();自定义pivot属性名
中间表数据默认通过pivot属性访问,但你完全可以按业务语义重命名它。例如应用中有「用户订阅播客」的多对多关系,可以把访问器从pivot改为subscription,使用as方法:
return $this->belongsToMany(Podcast::class)->as('subscription')->withTimestamps();定义后即可通过自定义名称访问中间表数据:
$users = User::with('podcasts')->get(); foreach ($users->flatMap->podcasts as $podcast) { echo $podcast->subscription->created_at; }通过中间表过滤关联
定义多对多关联时,还可以使用wherePivot与wherePivotIn直接对belongsToMany返回的结果进行过滤:
return $this->belongsToMany('App\Role')->wherePivot('approved', 1); return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);wherePivot用于等值过滤中间表字段,wherePivotIn用于IN集合过滤;它们与withPivot、withTimestamps、as一样,都是在 BelongsToMany.php 上提供的中间表交互能力。
Eager Loading:预加载与 N + 1 查询
将 Hyperf 关联作为属性访问时,关联数据是懒加载(lazy load)的——也就是说,关联数据只有在属性第一次被访问时才真正发起查询。而 Hyperf 同时支持在查询父模型时预加载(eager load)子关联,用于消除 N + 1 查询问题。
先看 N + 1 问题的典型场景。假设User与Role是一对一关联:
<?php declare(strict_types=1); namespace App\Models; use Hyperf\DbConnection\Model\Model; class User extends Model { public function role() { return $this->hasOne(Role::class, 'user_id', 'id'); } }现在取出所有用户及其角色:
$users = User::query()->get(); foreach ($users as $user){ echo $user->role->name; }这个循环会执行1 次查询取出所有用户,然后为每一个用户再各执行 1 次查询取出其角色。如果有 10 个用户,就会产生 11 条查询:1 条查user表,另外 10 条分别查role表。这就是典型的 N + 1 问题。
幸运的是,预加载可以把整个流程压缩到仅 2 条查询。查询时通过with方法声明需要预加载的关联:
$users = User::query()->with('role')->get(); foreach ($users as $user){ echo $user->role->name; }此时框架只执行两条 SQL:
SELECT * FROM `user`; SELECT * FROM `role` WHERE id in (1, 2, 3, ...);with方法内部会收集所有父模型的主键值,构造WHERE id IN (...)一次性取出全部关联数据,再按主键关系挂载到各个父模型上(relationLoaded、setRelation等挂载逻辑见 HasRelationships.php),最终在访问$user->role时直接命中已加载的内存数据,不再发起额外查询。
Polymorphic Relationships:多态关联
多态关联(Polymorphic Relationships)允许一个目标模型通过同一条关联被多个不同类型的模型所拥有。在 Hyperf 中实现方式与标准 ORM 一致。
One To One (Polymorphic):多态一对一
多态一对一与普通一对一类似,区别在于目标模型可以被多种模型类型共享。例如Book和User共用Image模型:利用多态一对一,一张图片表可以同时服务于书籍和用户。
表结构
book id - integer title - string user id - integer name - string image id - integer url - string imageable_id - integer imageable_type - stringimage表中的imageable_id字段根据不同的imageable_type具有不同的含义。默认情况下,imageable_type直接存储关联模型的类名字符串(如App\Model\User)。
模型示例
<?php namespace App\Model; class Image extends Model { public function imageable() { return $this->morphTo(); } } class Book extends Model { public function image() { return $this->morphOne(Image::class, 'imageable'); } } class User extends Model { public function image() { return $this->morphOne(Image::class, 'imageable'); } }morphOne($related, $name, $type = null, $id = null, $localKey = null)的第二个参数$name是多态关联名(即多态字段前缀)。实现中会通过getMorphs($name, $type, $id)生成{name}_type与{name}_id两个字段名(对应上表的imageable_type与imageable_id),并据此构造查询条件(见 HasRelationships.php 中morphOne的实现)。
获取关联数据
按上述模型定义,即可通过关联获取对应数据。例如获取某个用户的图片:
use App\Model\User; $user = User::find(1); $image = $user->image;也可以反向获取某张图片对应的用户或书籍——imageable会根据imageable_type自动实例化对应的User或Book:
use App\Model\Image; $image = Image::find(1); $imageable = $image->imageable;One To Many (Polymorphic):多态一对多
模型示例
<?php namespace App\Model; class Image extends Model { public function imageable() { return $this->morphTo(); } } class Book extends Model { public function images() { return $this->morphMany(Image::class, 'imageable'); } } class User extends Model { public function images() { return $this->morphMany(Image::class, 'imageable'); } }获取关联数据
获取某个用户的全部图片:
use App\Model\User; $user = User::query()->find(1); foreach ($user->images as $image) { // ... }Custom Polymorphic Mapping:自定义多态类型映射
默认情况下,框架要求type字段(如imageable_type)存储对应的模型类名(即User::class、Book::class),这在实际应用中会带来不便——数据库里直接写入完整类名,既冗长又让数据库与应用的内部结构强耦合。为此,Hyperf 提供了自定义映射机制,使用Relation::morphMap将别名与模型类对应起来,从而把数据库结构与应用内部结构解耦:
use App\Model; use Hyperf\Database\Model\Relations\Relation; Relation::morphMap([ 'user' => Model\User::class, 'book' => Model\Book::class, ]);morphMap的实现(见 Relation.php#L329-L339)支持传入$map数组与$merge合并标志(默认true,即与已有映射合并而非覆盖);定义后,框架通过getMorphedModel($alias)(见 Relation.php#L347-L350)将存储的别名(如user)解析回模型类,从而正确加载关联。
由于Relation::morphMap修改后驻留在内存中(静态属性),因此需要在项目启动时完成映射注册。推荐做法是注册一个监听BootApplication事件的 Listener:
<?php declare(strict_types=1); /** * This file is part of Hyperf. * * @link https://www.hyperf.io * @document https://doc.hyperf.io * @contact group@hyperf.io * @license https://github.com/hyperf/hyperf/blob/master/LICENSE */ namespace App\Listener; use App\Model; use Hyperf\Database\Model\Relations\Relation; use Hyperf\Event\Annotation\Listener; use Hyperf\Event\Contract\ListenerInterface; use Hyperf\Framework\Event\BootApplication; #[Listener] class MorphMapRelationListener implements ListenerInterface { public function listen(): array { return [ BootApplication::class, ]; } public function process(object $event) { Relation::morphMap([ 'user' => Model\User::class, 'book' => Model\Book::class, ]); } }该 Listener 通过#[Listener]注解注册(由Hyperf\Event组件驱动),在应用启动(BootApplication事件)时执行映射注册,从而保证整个生命周期内所有多态关联都能正确解析别名。
Nested Eager LoadingmorphToRelationships:嵌套预加载多态关联
如果希望预加载morphTo关联,并同时预加载该关联可能返回的各种实体上的嵌套关联,可以将with与morphTo关联的morphWith方法结合使用。
例如,我们希望从一张图片预加载book.user(书籍及其作者)两层关联:
use App\Model\Book; use App\Model\Image; use Hyperf\Database\Model\Relations\MorphTo; $images = Image::query()->with([ 'imageable' => function (MorphTo $morphTo) { $morphTo->morphWith([ Book::class => ['user'], ]); }, ])->get();对应的 SQL 查询序列如下:
-- Query semua images select * from `images`; -- Query daftar user yang sesuai dengan images select * from `user` where `user`.`id` in (1, 2); -- Query daftar buku yang sesuai dengan images select * from `book` where `book`.`id` in (1, 2, 3); -- Query daftar user yang sesuai dengan daftar buku select * from `user` where `user`.`id` in (1, 2);即:先查询全部图片;再根据图片中记录的imageable_type分别批量查询各类型对应的实体(这里是user与book);最后对book实体上的user嵌套关联再执行一次批量预加载。整个过程共 4 条 SQL,避免了逐条访问时的 N + 1 膨胀。
Polymorphic Relationship Query:多态关联存在性查询
要按MorphTo关联的「存在性」过滤查询,可以使用whereHasMorph方法及其配套方法。
以下示例查询「所属实体(book 或 user)的 ID 为 1」的图片列表:
use App\Model\Book; use App\Model\Image; use App\Model\User; use Hyperf\Database\Model\Builder; $images = Image::query()->whereHasMorph( 'imageable', [ User::class, Book::class, ], function (Builder $query) { $query->where('imageable_id', 1); } )->get();whereHasMorph接收三个参数:多态关联名、允许的模型类列表,以及一个接收Builder的闭包用于追加存在性子查询约束。这使你可以跨多个多态类型,用一条查询完成带关联条件的过滤。
总结
Hyperf 的模型关联系统为日常业务建模提供了完整的工具箱:
| 关联类型 | 定义方法 | 典型场景 |
|---|---|---|
| 一对一 | hasOne | 用户 ↔ 角色 |
| 一对多 | hasMany | 作者 → 多本书 |
| 一对多反向 | belongsTo | 书 → 作者 |
| 多对多 | belongsToMany | 用户 ↔ 角色(借助中间表) |
| 多态一对一 | morphOne+morphTo | 图片可属于用户或书籍 |
| 多态一对多 | morphMany+morphTo | 图片列表可属于用户或书籍 |
| 多态多对多 | morphToMany | 带类型区分的多对多(对应 MorphToMany.php 与 MorphPivot.php) |
关键要点回顾:
- 关联以方法形式定义,本质是查询构建器,可无限叠加约束后取值;
- 通过动态属性访问关联,
__get→getAttribute→getRelationValue的调用链(Model.php#L144-L147)会自动完成懒加载,且关联方法必须return一个Relation实例,否则抛出LogicException; - 默认外键/中间表命名均遵循约定,但每个参数都支持显式覆盖;
- 多对多中间表可通过
pivot(或as自定义名)访问,用withPivot、withTimestamps、wherePivot、wherePivotIn增强; - 使用
with预加载可把 N + 1 查询压缩到常量级别;多态场景可用morphWith做嵌套预加载、用whereHasMorph做存在性查询; - 通过
Relation::morphMap(Relation.php#L329-L339)+ 启动期 Listener,可将数据库中的多态类型别名与模型类解耦。
关联的底层实现分布在 src/database/src/Model/Concerns/HasRelationships.php(定义入口)与 src/database/src/Model/Relations(各类关联对象)中,动手调试或深挖细节时可以直接阅读这些文件。
- 后端
- Web框架
- 微服务
- RPC框架
- 异步编程
【免费下载链接】hyperf
🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.
相关推荐
Hyperf 模型关联(Model Relationships)完整实战指南:从一对一、多对多到多态关联与预加载
Hyperf 模型关联(Model Relationships)完整实战指南:从一对一、多对多到多态关联与预加载 导读 本文是 Hyperf 框架模型层关联功能
后端Web框架微服务RPC框架异步编程Bookshelf.js 一对一关联(One-to-One)完整指南:hasOne / belongsTo / morphOne 从模型定义到迁移实战
Bookshelf.js 一对一关联(One to One)完整指南:hasOne / belongsTo / morphOne 从模型定义到迁移实战 导读 本
后端Reflex 数据库关系(Relationships)实战指南:外键关联、双向关系与查询加载策略
Reflex 数据库关系(Relationships)实战指南:外键关联、双向关系与查询加载策略 在 Reflex 中,模型之间通过外键(Foreign Key
后端前端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考