ThinkPHP8学习篇(十二):模型关联(二)
在掌握模型基础关联关系后,ThinkPHP 还提供了更灵活的关联方案来应对复杂业务场景。多态关联突破了传统关联的限制,预载入与统计查询则显著提升了关联数据的获取效率。本文作为模型关联系列的第二篇,同时也是模型主题的收官文章,将系统学习多态关联的实现逻辑、关联预载入的性能优化策略、关联统计的便捷应用,以及关联数据的输出。本篇文章将记录这些内容的学习过程。
一、多态关联
1、多态一对多关联
1.1、关联新增
1.2、获取多态关联
2、多态一对一关联
二、关联预载入
1、延迟预载入
2、关联预载入缓存
三、关联统计
四、关联输出
1、隐藏关联属性
2、显示关联属性
一、多态关联
1、多态一对多关联
多态关联允许一个模型在单个关联定义方法中从属一个以上其它模型,例如用户可以评论书和文章,但评论表通常都是同一个数据表,通过字段内容的不同来进行区分。多态一对多关联关系,就是为了满足类似的使用场景而设计。
下面是关联表的数据表结构:
CREATE TABLE `article`(id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(30),content TEXT
)COMMENT '文章';CREATE TABLE `book`(id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(30)
)COMMENT '书籍';CREATE TABLE `comment`(id INT PRIMARY KEY AUTO_INCREMENT,content TEXT,commentable_id INT,commentable_type VARCHAR(50)
)COMMENT '评论表';
comment 表中有两个字段是我们需要注意的,分别是 commentable_id 和 commentable_type,我们称这两个字段为多态字段。其中,commentable_id 用于存放书或者文章的id(主键),commentable_type 用于存放所属模型的类型,该字段用于区分评论的是书还是文章。通常的设计是多态字段有一个公共的前缀(例如这里用的 commentable),当然,也支持设置完全不同的字段名(例如使用 data_id 和 type )。
接着,我们就可以进行创建这种关联所需的模型定义。morphMany 方法用于定义多态一对多关联,该方法参数如下:
morphMany('关联模型', '多态字段', '多态类型');
- 关联模型(必须):关联的模型类名。
- 多态字段(可选):支持两种方式定义 如果是字符串表示多态字段的前缀,多态字段使用 多态前缀_type 和 多态前缀_id,如果是数组,表示使用 ['多态类型字段名', '多态ID字段名'],默认为当前的关联方法名作为字段前缀。
- 多态类型(可选):当前模型对应的多态类型,默认为当前模型名,可以使用模型名(如 Article)或者完整的命名空间模型名(如 app\model\Article)。
下面开始定义模型,首先是文章模型:
<?php
namespace app\model;use think\Model;class Article extends Model
{/*** 获取所有针对文章的评论*/public function comments(){return $this->morphMany(Comment::class, 'commentable');}
}
然后是书籍模型:
<?php
namespace app\model;use think\Model;class Book extends Model
{/*** 获取所有针对书籍的评论*/public function comments(){return $this->morphMany(Comment::class, 'commentable');}
}
书籍模型的设置方法同文章模型一样,区别在于多态类型不同,但由于多态类型默认会取当前模型名,因此不需要单独设置。
最后是评论模型的关联定义:
<?php
namespace app\model;use think\Model;class Comment extends Model
{/*** 获取评论对应的多态模型。*/public function commentable(){return $this->morphTo();}
}
morphTo 方法的参数如下:
morphTo('多态字段', ['多态类型别名']);
- 多态字段(可选):支持两种方式定义,如果是字符串表示多态字段的前缀,多态字段使用 多态前缀_type 和 多态前缀_id,如果是数组,表示使用 ['多态类型字段名', '多态ID字段名'],默认为当前的关联方法名作为字段前缀。
- 多态类型别名(可选):数组方式定义。
1.1、关联新增
$article = Article::find(1);
// 给文章添加评论,会自动向评论表中写入数据
$article->comments()->save(['content' => '文章评论1']);$book = Book::find(1);
// 给书籍添加评论,会自动向评论表中写入数据
$book->comments()->save(['content' => '书籍评论1']);
数据保存成功后,我们一起来看下 comment (评论表)中存储的内容。

从存储的数据中可以看到,commentable_type 字段存储的是所属的模型,内容为完整的命名空间模型名。
1.2、获取多态关联
一旦数据表及模型被定义,则可以通过模型来访问关联。例如,若要访问某篇文章的所有评论,则可以简单的使用 comments 动态属性:
$article = Article::find(1);foreach ($article->comments as $comment) {dump($comment);
}
也可以从多态模型的多态关联中,通过访问调用 morphTo 的方法名称来获取拥有者,也就是此例子中 Comment 模型的 commentable 方法。所以,我们可以使用动态属性来访问这个方法:
$comment = Comment::find(1);
echo $comment->commentable;
Comment 模型的 commentable 关联会返回 Article 或 Book 模型的对象实例,这取决于评论所属模型的类型。
2、多态一对一关联
多态一对一相比多态一对多关联的区别是动态的一对一关联,举个例子说有一个个人和团队表,而无论个人还是团队都有一个头像需要保存但都会对应同一个头像表。下面是数据表结构:
CREATE TABLE `member`(id INT PRIMARY KEY AUTO_INCREMENT,`name` VARCHAR(30)
)COMMENT '会员';CREATE TABLE `team`(id INT PRIMARY KEY AUTO_INCREMENT,`name` VARCHAR(30)
)COMMENT '团队';CREATE TABLE `avatar`(id INT PRIMARY KEY AUTO_INCREMENT,avatar VARCHAR(80),imageable_id INT,imageable_type VARCHAR(50)
)COMMENT '头像';
morphOne 方法用于定义多态一对一关联,该方法的参数如下:
morphOne('关联模型', '多态字段', '多态类型');
- 关联模型(必须):关联的模型类名。
- 多态字段(可选):支持两种方式定义,如果是字符串表示多态字段的前缀,多态字段使用 多态前缀_type 和 多态前缀_id,如果是数组,表示使用 ['多态类型字段名', '多态ID字段名'],默认为当前的关联方法名作为字段前缀。
- 多态类型(可选):当前模型对应的多态类型,默认为当前模型名,可以使用模型名(如 Member)或者完整的命名空间模型名(如 app\model\Member)。
下面开始定义模型,首先是会员模型:
<?php
namespace app\model;use think\Model;class Member extends Model
{/*** 获取用户的头像*/public function avatar(){return $this->morphOne(Avatar::class, 'imageable');}
}
然后是团队模型:
<?php
namespace app\model;use think\Model;class Team extends Model
{/*** 获取团队的头像*/public function avatar(){return $this->morphOne(Avatar::class, 'imageable');}
}
最后是头像模型:
<?php
namespace app\model;use think\Model;class Avatar extends Model
{/*** 获取头像对应的多态模型。*/public function imageable(){return $this->morphTo();}
}
理解了多态一对多关联后,多态一对一关联其实就很容易理解了,区别就是当前模型和动态关联的模型之间的关联属于一对一关系。具体的操作这里就不再进行演示了,可参考多态一对多的操作。
二、关联预载入
关联查询的预查询载入功能,主要解决了 N+1 次查询的问题,例如下面的查询如果有3个记录,会执行4次查询:
$list = User::select([1, 2, 3]);
foreach($list as $user){// 获取用户关联的profile模型数据dump($user->profile);
}
// 先根据id [1,2,3] 查询用户数据,再分别对 user_id为1,2,3的档案数据进行查询,供4次查询
/* 生成的SQL语句为:
SELECT * FROM `user` WHERE `id` IN (1,2,3)
SELECT * FROM `profile` WHERE `user_id` = 1 LIMIT 1
SELECT * FROM `profile` WHERE `user_id` = 2 LIMIT 1
SELECT * FROM `profile` WHERE `user_id` = 3 LIMIT 1
*/
如果使用关联预查询功能,就可以变成2次查询,有效提高性能。
$list = User::with(['profile'])->select([1, 2, 3]);
foreach($list as $user){// 获取用户关联的profile模型数据dump($user->profile);
}
/* 关联查询方式会根据给出的id只查询一次,生成的SQL语句为:
SELECT * FROM `user` WHERE `id` IN (1,2,3)
SELECT * FROM `profile` WHERE ( `user_id` IN (1,2,3) ) ORDER BY `id` DESC LIMIT 1000
*/
支持预载入多个关联,例如:
$list = User::with(['profile', 'book'])->select([1, 2, 3]);
也可以支持嵌套预载入,例如:
$list = User::with(['profile.phone'])->select([1, 2, 3]);
foreach($list as $user){// 获取用户关联的phone模型dump($user->profile->phone);
}
支持使用数组方式定义嵌套预载入,例如下面的预载入要同时获取用户的 Profile 关联模型的 Phone、Job 和 Img 子关联模型数据:
$list = User::with(['profile'=>['phone', 'job', 'img']])->select([1, 2, 3]);
foreach($list as $user){// 获取用户关联dump($user->profile->phone);dump($user->profile->job); dump($user->profile->img);
}
可以使用 withField 方法指定查询哪些字段:
$list = User::field('id,name')->with(['profile' => function($query){$query->withField(['user_id', 'email', 'phone']);
}])->select([1, 2, 3]);foreach($list as $user){// 获取用户关联的profile模型数据dump($user->profile);
}
也可以使用 withoutField 方法排除某些字段,例如:
$list = User::field('id,name')->with(['profile' => function($query){$query->withoutField('create_time,delete_time,update_time');
}])->select([1, 2, 3]);
对于一对多关联来说,如果需要设置返回的关联数据数量,可以使用 withLimit 方法。
Article::with(['comments' => function($query) {$query->order('create_time', 'desc')->withLimit(3);
}])->select();
关联预载入名称是关联方法名,支持传入方法名的小写和下划线定义方式,例如关联方法名是 userProfile 和 userBook 的话:
$list = User::with(['userProfile', 'userBook'])->select([1, 2, 3]);
和下面的方法是等效的:
$list = User::with(['user_profile', 'user_book'])->select([1, 2, 3]);
区别在于获取关联数据的时候必须和传入的关联名称保持一致。
$user = User::with(['userProfile'])->find(1);
dump($user->userProfile);$user = User::with(['user_profile'])->find(1);
dump($user->user_profile);
1、延迟预载入
有些情况下,需要根据查询出来的数据来决定是否需要使用关联预载入,当然关联查询本身就能解决这个问题,因为关联查询是惰性的,不过用预载入的理由也很明显,性能具有优势。
延迟预载入仅针对多个数据的查询,因为单个数据的查询用延迟预载入和关联惰性查询没有任何区别,所以不需要使用延迟预载入。
如果数据集查询返回的是数据集对象,可以使用调用数据集对象的 load 实现延迟预载入:
// 查询数据集
$list = User::select([1, 2, 3]);
// 延迟预载入
$list->load(['cards']);
foreach($list as $user){// 获取用户关联的card模型数据dump($user->cards);
}
2、关联预载入缓存
关联预载入可以支持查询缓存,例如:
$list = User::with(['profile'])->withCache(30)->select([1, 2, 3]);
表示对关联数据缓存30秒。
如果有多个关联数据,也可以仅缓存部分关联:
$list = User::with(['profile', 'book'])->withCache(['profile'], 30)->select([1, 2, 3]);
三、关联统计
有些时候,并不需要获取关联数据,而只是希望获取关联数据的统计,这个时候可以使用 withCount 方法进行指定关联的统计。
$list = User::withCount('profile')->select([1,2,3]);
foreach($list as $user){// 获取用户关联的profile关联统计echo $user->profile_count . '<br>';
}
关联统计功能会在模型的对象属性中自动添加一个以“关联方法名+_count”为名称的动态属性来保存相关的关联统计数据。
可以通过数组的方式同时查询多个统计字段。
$list = User::withCount(['cards', 'phone'])->select([1, 2, 3]);
foreach($list as $user){// 获取用户关联关联统计echo $user->cards_count;echo $user->phone_count;
}
支持给关联统计指定统计属性名,例如:
$list = User::withCount(['profile' => 'profilesCount'])->select([1,2,3]);
foreach($list as $user){// 此时统计的属性名称改为了profilesCountecho $user->profilesCount . '<br>';
}
关联统计暂不支持多态关联。
如果需要对关联统计进行条件过滤,可以使用闭包方式。
$list = User::withCount(['cards' => function($query) {$query->where('status', 1);
}])->select([1, 2, 3]);foreach($list as $user){// 获取用户关联的card关联统计echo $user->cards_count;
}
使用闭包的方式,如果需要自定义统计字段名称,可以使用:
$list = User::withCount(['cards' => function($query, &$alias) {$query->where('status', 1);$alias = 'card_count';
}])->select([1, 2, 3]);foreach($list as $user){// 获取用户关联的card关联统计echo $user->card_count;
}
和 withCount 类似的方法,还有:
| 关联统计方法 | 描述 |
| withSum | 关联SUM统计 |
| withMax | 关联Max统计 |
| withMin | 关联Min统计 |
| withAvg | 关联Avg统计 |
除了 withCount 之外的统计方法需要在第二个字段传入统计字段名,用法如下:
$list = User::withSum('cards', 'total')->select([1, 2, 3]);foreach($list as $user){// 获取用户关联的card关联余额统计echo $user->cards_sum;
}
同样,也可以指定统计字段名:
$list = User::withSum(['cards' => 'card_total'], 'total')->select([1, 2, 3]);foreach($list as $user){// 获取用户关联的card关联余额统计echo $user->card_total;
}
所有的关联统计方法可以多次调用,每次查询不同的关联统计数据。
四、关联输出
1、隐藏关联属性
使用 hidden 方法可以隐藏关联模型的属性,例如:
$list = User::with('profile')->select();
$list->hidden(['profile.email'])->toArray();
输出的结果中就不会包含 Profile 模型的 email 属性,如果需要隐藏多个属性可以使用下面的方式:
$list = User::with('profile')->select();
$list->hidden(['profile' => ['address', 'phone', 'email']])->toArray();
2、显示关联属性
同样地,可以使用 visible 方法来显示关联属性:
$list = User::with('profile')->select();
$list->visible(['profile' => ['address', 'phone', 'email']])->toArray();