☰
Hyperf 模型关联(Model Relationships)完全指南:从 hasOne 到多态关联与预加载
2026/10/8 13:56:46 网站建设 项目流程
  • 后端
  • Web框架
  • 微服务
  • RPC框架
  • 异步编程

【免费下载链接】hyperf

🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.

项目地址:https://gitcode.com/hyperf/hyperf
点击查看免费下载

本文是 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 - string

image表中的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.

项目地址:https://gitcode.com/hyperf/hyperf
点击查看免费下载

相关推荐

上一篇:Fresh语言开发常见问题解答:新手到专家的进阶之路
下一篇:WhiteNoise 使用教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询