laravel 入门(11)Eloquent 模型关联关系(上)
Eloquent 模型支持的关联关系包括以下七种:
- 一对一
- 一对多
- 多对多
- 远层一对多
- 多态关联(一对一)
- 多态关联(一对多)
- 多态关联(多对多)
下面我们将以设计一个简单的博客系统数据库为例一一介绍上述关联关系。
一对一#
建立关联关系#
一对一是最简单的关联关系,一般可用于某张数据表的扩展表与主表之间的关联关系。比如在大型系统中,我们的用户表通常用于最基本信息的存储,如邮箱、用户名、密码等,然后像用户爱好、标签、个性签名、所在地等信息都存到另一张扩展表中,需要的时候才会去扩展表取数据,从而提高查询性能。针对这样的场景,我们就可以在两张表对应模型之间建立一对一关联。
在开始之前,我们先通过数据库迁移创建一张 user_profiles 数据表,并创建对应模型 UserProfile,这可以通过以下 Artisan 命令一次完成:
php artisan make:model UserProfile -m
在生成的 create_user_profiles 迁移文件中编写迁移类的 up 方法如下:
public function up()
{
Schema::create('user_profiles', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned()->default(0)->unique();
$table->string('bio')->nullable()->comment('个性签名');
$table->string('city')->nullable()->comment('所在城市');
$table->json('hobby')->nullable()->comment('个人爱好');
$table->timestamps();
});
}
注意,我们在 user_profiles 表中添加了一个 user_id 字段用于指向所属用户,从而建立于 users 表的关联。运行 php artisan migrate 在数据库创建这张数据表。
准备好数据表之后,接下来,我们来通过模型类建立 users 表和 user_profiles 表之间的关联,Eloquent 模型类底层提供了相应的 API 方法帮助我们建立模型之间的关联。首先,我们在 User 模型类中通过 hasOne 方法定义其与 UserProfile 的一对一关联:
public function profile()
{
return $this->hasOne(UserProfile::class);
}
我们通过数据库填充技术在 user_profiles 插入一些数据,这样就可以在 User 模型实例上通过关联方法名作为动态属性访问与其对应的 UserProfile 模型实例了:
$user = User::findOrFail(1);
$profile = $user->profile;
Eloquent 底层约定#
需要指出的是,在关联关系的建立过程中,Eloquent 也遵循了「约定大于配置」的原则。你可能注意到了我们在定义关联关系时,仅仅指定了模型类名,并没有指定通过哪些数据表字段建立关联,这并不是说 Laravel 神通广大,能知过去未来之事,而是因为 Eloquent 对此做了默认的约定。hasOne 方法的完整签名是:
public function hasOne($related, $foreignKey = null, $localKey = null)
其中,第一个参数是关联模型的类名,第二个参数是关联模型类所属表的外键,这里对应的是 user_profiles 表的 user_id 字段,第三个参数是关联表的外键关联到当前模型所属表的哪个字段,这里对应的是 users 表的 id 字段。为什么我们不需要指定 Laravel 就能完成这种关联呢,这是因为如果没有指定 $foreignKey,Eloquent 底层会通过如下方法去拼接:
public function getForeignKey()
{
return Str::snake(class_basename($this)).'_'.$this->getKeyName();
}
你可以看到,在本例中,拼接的结果正好是 user_id。
同样,没有指定 $localKey 的话,Eloquent 底层会返回主键 ID:
public function getKeyName()
{
return $this->primaryKey;
}
在本例中,就是 id 了。
遵循这种默认的约定,可以帮我们少写很多代码,减少很多额外的配置,所以如果不是迫不得已(比如从其他系统迁移过来),建议你在使用 Eloquent 的话,尽量遵循这些默认约定。如果数据表没有遵循这种约定的话,只能手动传参了。
建立相对的关联关系#
通常我们都是通过 User 模型获取 UserProfile 模型,但是有时候我们可能需要反过来通过 UserProfile 反查所属的 User 模型,Eloquent 底层也为我们提供了相应的 belongsTo 方法来建立相对的一对一关联关系,我们在 UserProfile 模型类定义其与 User 模型的关联如下:
public function user()
{
return $this->belongsTo(User::class);
}
同样,采用关联关系方法名作为动态属性即可访问该模型所属 User 模型实例:
$profile = UserProfile::findOrFail(2);
$user = $profile->user;
同样,和 hasOne 方法一样,belongsTo 方法也是遵循了默认的约定规则,其完整方法签名如下:
public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null)
其中第一个参数是关联模型的类名。
第二个参数是当前模型类所属表的外键,在本例中是 user_profiles 表的 user_id 字段,拼接规则和 hasOne 那里类似,只不过这里是基于第四个参数关联关系名称 $relation:
if (is_null($relation)) {
$relation = $this->guessBelongsToRelation();
}
...
if (is_null($foreignKey)) {
$foreignKey = Str::snake($relation).'_'.$instance->getKeyName();
}
$relation 默认约定是对应关联关系方法名,这里的是 user。如果你这里定义的方法名不是 user,则需要手动指定外键参数。
第三个参数是关联模型类所属表的主键:
$instance = $this->newRelatedInstance($related);
...
$ownerKey = $ownerKey ?: $instance->getKeyName();
第四个参数前面已经说过,默认约定就是关联关系方法名,也是关联关系动态属性名。
还是很之前一样,建议遵循这些默认约定,可以少写很多代码,避免配置出错导致程序bug。如果实在满足不了这些默认的约定,只能指定按照上述约定指定传入自己的参数了。
一对多#
建立关联关系#
一对多关联是我们日常开发中经常碰到的一种关联关系。以博客系统为例,一个用户可以发布多篇文章,反过来,一篇只能归属于一个用户,那么用户和文章之间就是一对多的关系,同样,用户可以发布多条评论,一条评论只能归属于一个用户,用户与评论之间也是一对多关系。
要定义用户文章之间的一对多关联,可以在 User 模型类中通过 Eloquent 底层提供的 hasMany 方法来实现:
public function posts()
{
return $this->hasMany(Post::class);
}
由于我们之间已经创建过 users 表和 posts 表,并且初始化过数据,所以我们可以直接通过动态属性的方式来调用用户模型上的文章:
$user = User::findOrFail(1);
$posts = $user->posts;
与 hasOne 返回的是单个模型实例不一样,hasMany 返回的是模型类集合:
Eloquent 底层约定#
和 hasOne 方法一样,hasMany 方法底层也对如何建立关联关系做了约定,而且 hasMany 方法和 hasOne 方法的签名一样:
public function hasMany($related, $foreignKey = null, $localKey = null)
$foreignKey 和 $localKey 默认获取逻辑也和 hasOne 完全一样,这里不再赘述。其实你完全可以把一对一关联看作一对多关联的简化版本,只不过一对一退化为只返回一条记录,所以实现逻辑一样也不难理解了。
如果你的数据表结构不符合这种默认约定,可以自定义传入对应字段参数值。
建立相对的关联关系#
与一对一一样,我们可以在文章模型中建立于用户模型之间的相对关联关系,而且这种使用场景很普遍,比如在文章详细页或列表页显示文章作者信息。还是通过 Eloquent 提供的 belongsTo 方法来实现:
public function user()
{
return $this->belongsTo(User::class);
}
这样,我们就可以在文章模型实例上通过动态属性 user 来访问对应的用户信息:
$post = Post::findOrFail(29);
$author = $post->user;
belongsTo 方法的底层约定我们在前面一对一关联中已经讨论过。这里,如果你想要让代码可读性更好,可以将 Post 模型中的关联关系调方法名修改为 author,这样,我们就需要手动指定更多的 belongsTo 方法传入参数了:
public function author()
{
return $this->belongsTo(User::class, 'user_id', 'id', 'author');
}
相应的访问方式也需要做调整:
$author = $post->author;
返回结果完全一样。
渴求式加载#
前面我们演示的关联关系查询都是通过动态属性的方式,这种加载方式叫做「懒惰式加载」,因为都是用到的时候才回去查询,这就意味着要多次对数据库的进行查询才能返回需要的结果。如果是单条记录获取关联关系,就需要两次查询;如果是多条记录获取关联关系,比如文章列表页获取作者信息,因为每篇文章的作者通过动态属性获取都有一次查询,所以对N条记录来说,需要「N+1」次查询才能返回需要的结果,从数据库查询优化的角度来说,显然是不合理的。能不能一次就返回所有的关联查询结果呢?
可以,Eloquent 为我们提供了 with 方法,我们将需要查询的关联关系动态属性(关联方法名)传入该方法,并将其链接到 Eloquent 模型原有的查询中,就可以一次完成关联查询,加上模型自身查询,总共查询两次。我们将这种加载方式叫做「渴求式加载」,即根据所需预先查询所有数据。
以文章列表为例,我们可以通过这种方式获取文章及对应作者信息:
$posts = Post::with('author')
->where('views', '>', 0)
->offset(1)->limit(10)
->get();
对应的底层 SQL 执行语句是:
select * from `posts` where `views` > 0 and `posts`.`deleted_at` is null limit 10 offset 0;
select * from `users` where `users`.`id` in (?, ?, ?, ?, ?, ?) and `email_verified_at` is not null
这样,就可以在返回的列表中看到关联的作者信息了,在遍历的时候可以通过 $post->author 获取,而无需每次加载,从而提高数据库查询性能。
多对多#
建立关联关系#
多对多关联也很常见,还是以博客系统为例,我们会为每篇文章设置标签,一篇文章往往有多个标签,反过来,一个标签可能会归属于多篇文章,这时,我们说文章和标签之间是多对多的关联关系。
多对多关联比一对一和一对多关联复杂一些,需要借助一张中间表才能建立关联关系。以文章标签为例,文章表已经存在了,还需要创建一张 tags 表和中间表 post_tags。首先创建 Tags 模型类及其对应数据表 tags 迁移文件:
php artisan make:model Tag -m
编写 create_tags_table 迁移文件对应类的 up 方法如下:
public function up()
{
Schema::create('tags', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 100)->unique()->comment('标签名');
$table->timestamps();
});
}
然后创建 post_tags 数据表迁移文件:
php artisan make:migration create_post_tags_table --create=post_tags
编写其对应迁移类的 up 方法如下:
public function up()
{
Schema::create('post_tags', function (Blueprint $table) {
$table->increments('id');
$table->integer('post_id')->unsigned()->default(0);
$table->integer('tag_id')->unsigned()->default(0);
$table->unique(['post_id', 'tag_id']);
$table->timestamps();
});
}
运行 php artisan migrate 让迁移生效。
接下来,我们在 Post 模型类中定义其与 Tags 模型类的关联关系,通过 Eloquent 提供的 belongsToMany 方法来实现:
public function tags()
{
return $this->belongsToMany(Tag::class, 'post_tags');
}
通过数据库填充器填充一些数据到 tags 表和 post_tags 表,这样我们就可以通过关联查询查询指定 Post 模型上的标签信息了:
$post = Post::with('tags')->find(1);
$tags = $post->tags;
返回的是个模型集合
Eloquent 底层约定#
可以看到我们在定义多对多关联的时候,也没有指定通过哪些字段进行关联,这同样是遵循 Eloquent 底层默认约定的功劳,belongsToMany 方法签名如下:
public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null, $parentKey = null, $relatedKey = null, $relation = null)
除了第一个参数之外,其它参数都可以为空。第一个参数是关联模型的类名,这里是 Tag。
第二个参数 $table 是建立多对多关联的中间表名,该表名默认拼接规则如下:
$segments = [
$instance ? $instance->joiningTableSegment()
: Str::snake(class_basename($related)),
$this->joiningTableSegment(),
];
sort($segments);
return strtolower(implode('_', $segments));
其中$this->joiningTableSegment()
将当前模型类名转化为小写字母+下划线格式(注意不是复数格式,所以并不是对应默认表名),$instance
对应关联模型类实例,如果为空的话返回Str::snake(class_basename($related)),也会将关联类名转化为小写字母+下划线格式(也不是表名),然后对转化后的字符片段按字母表排序。所以本例中如果不指定中间表名,按照默认约定该值是 post_tag。但是为了遵循 Laravel 数据表名都是复数,所以我这里自定义了一回。
第三个参数是 $foreignPivotKey
指的是中间表中当前模型类的外键,默认拼接规则和前面一对一、一对多一样,所以在本例中是 posts 表的 post_id 字段。我在建表的时候遵循了这个约定,所以不需要额外指定。
第四个参数 $relatedPivotKey
是中间表中当前关联模型类的外键,拼接规则和 $foreignPivotKey 一样,只不过作用于关联模型类,所以在本例中是 tags 表的 tag_id 字段。同样,我在建表的时候也遵循了这一约定,不需要额外指定。
第五个参数 $parentKey 表示对应当前模型的哪个字段(即 $foreignPivotKey 映射到当前模型所属表的哪个字段),默认是主键 ID,即 posts 表的 id 字段,所以这里不需要额外指定。
第六个参数 $relatedKey 表示对应关联模型的哪个字段(即 $relatedPivotKey 映射到关联模型所属表的哪个字段),默认是关联模型的主键 ID,即 tags 表的 id 字段,所以这里也不需要额外指定。
最后一个参数 $relation 表示关联关系名称,用于设置查询结果中的关联属性,默认是关联方法名。
如果你没有遵循上述约定,需要手动指定自己的参数字段,不过还是建议遵循这些默认的约定,不然写着写着容易把自己绕晕。
建立相对的关联关系#
与之前的关联关系一样,多对多关联也支持建立相对的关联关系,而且由于多对多的双方是平等的,不存在谁归属谁的问题,所以建立相对关联的方法都是一样的,我们可以在 Tag 模型中通过 belongsToMany 方法建立其与 Post 模型的关联关系:
public function posts()
{
return $this->belongsToMany(Post::class, 'post_tags');
}
比如博客的标签页,通过指定标签查看归属该标签下的所有文章,就可以用到类似的关联查询,相应的实现代码如下:
$tag = Tag::with('posts')->where('name', 'ab')->first();
$posts = $tag->posts;
获取中间表字段#
Eloquent 还提供了方法允许你获取中间表的字段,你仔细看查询结果字段,会发现 relations 字段中有一个 pivot 属性,中间表字段就存放在这个属性对象上
我们在遍历返回结果的时候可以在循环中通过 $post->pivot->tag_id 获取中间表字段值。不过中间表默认只返回关联模型的主键字段,如果要返回额外字段,需要在定义关联关系的时候手动指定,比如如果想要返回时间戳信息,可以这么定义:
public function tags()
{
return $this->belongsToMany(Tag::class, 'post_tags')->withTimestamps();
}
这样就可以返回文章标签创建时间和更新时间了
如果除此之外,你还在中间表中定义了额外的字段信息,比如 user_id,可以通过 with 方法传入字段然后将其返回:
public function tags()
{
return $this->belongsToMany(Tag::class, 'post_tags')->withPivot('user_id')->withTimestamps();
}
自定义中间表模型类#
你还可以通过自定义中间表对应模型类实现更多自定义操作,中间表模型类继承自 Illuminate\Database\Eloquent\Relations\Pivot,Pivot 也是 Eloquent Model 类的子类,只不过为中间表操作定义了很多方法和属性,比如我们创建一个自定义的中间表模型类 PostTag:
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class PostTag extends Pivot
{
protected $table = 'post_tags';
}
这样,我们在定义多对多关联关系的时候指定自定义的模型类了:
public function tags()
{
return $this->belongsToMany(Tag::class, 'post_tags')->using(PostTag::class);
}
更多中间表操作#
此外,如果你觉得 pivot 可读性不好,你还可以自定义中间表实例属性名称:
$this->belongsToMany(Tag::class, 'post_tags')->as('taggable')->withTimestamps();
这样,就可以通过 $tag->taggable->created_at 访问中间表字段值了。
还可以通过中间表字段值过滤关联数据(支持 where 和 in 查询):
return $this->belongsToMany(Tag::class, 'post_tags')->wherePivot('user_id', 1);
return $this->belongsToMany(Tag::class, 'post_tags')->wherePivotIn('user_id', [1, 2]);
远层一对多关联#
什么是远层一对多关联#
远层一对多在一对多关联的基础上加上了一个修饰词「远层」,意味着这个一对多关系不是直接关联,而是「远层」关联,远层怎么关联呢?借助中间表。前面我们讨论的多对多关联也是借助中间表,但是远层一对多与其区别在于还是一对多的关联。所以理解一对多和多对多关联是理解今天介绍的几种关联关系的基础。
光说概念你可能还是懵,下面我们举个例子来说明。如果我们的博客系统是针对全球市场的话,可能针对不同的国家推出不同的用户系统和功能,然后中国用户过来就只展示中国用户发布的文章,日本用户过来就只展示日本用户发布的文章,这里面涉及到三张表,存储国家的 countries 表,存储用户的 users 表,以及存储文章的 posts 表。用户与文章是一对多的关联关系,这一点我们上篇教程已经说过,国家与用户之间是一对多的关联(一个用户只能有一个国籍),那么通过用户这张中间表,国家和文章之间也建立起来一对多的关联,只是这个关联不是直接的关联,而是「远层」的关联。针对这样的情况,我们说国家和文章之间是远层的一对多关联。
建立远层一对多关联关系#
了解这个关联的概念之后,我们要查询某个国家下的文章,要怎么做呢?或者说我们要怎么在模型类之间建立远层的一对多关联呢?
开始之前,我们要先创建 Country 模型类及其对应数据库迁移:
php artisan make:model Country -m
编写新生成的数据库迁移文件对应迁移类的 up 方法如下:
public function up()
{
Schema::create('countries', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 100)->unique();
$table->string('slug', 100)->unique();
$table->timestamps();
});
}
然后,编写迁移文件为 users 表新增一个 country_id 字段:
php artisan make:migration alter_users_add_country_id --table=users
编写新生成的迁移类文件如下:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AlterUsersAddCountryId extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->integer('country_id')->unsigned()->default(0);
$table->index('country_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('country_id');
});
}
}
接下来,运行 php artisan migrate 让迁移生效。在 countries 表和 users 表填充一些测试数据便于后续测试。
准备好数据库、模型类并填充测试数据后,接下来,我们在 Country 模型类中通过 Eloquent 提供的 hasManyThrough 方法定义其与 Post 模型类之间的远层一对多关联:
public function posts()
{
return $this->hasManyThrough(Post::class, User::class);
}
其中,第一个参数是关联的模型类,第二个参数是中间借助的模型类。
这样,我们就可以在代码中通过 Country 模型实例获取归属于该国家的所有文章了,查询方式和前面其它关联查询一样,可以懒惰式加载,也可以渴求式加载:
$country = Country::findOrFail(1);
$posts = $country->posts;
返回结果也是模型集合
Eloquent 底层约定#
同样,我们在通过 hasManyThrough 方法定义远层一对多关联关系的时候,并没有指定关联字段,因为我们在定义数据库字段、模型类的时候都遵循了 Eloquent 底层的约定。
我们来看一下 hasManyThrough 方法的完整签名:
public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null)
其中,第一个参数和第二个参数分别是关联模型类和中间模型类。
** 第三个参数 $firstKey表示中间模型类与当前模型类的关联外键**,按照默认约定,在本例中拼接出来的字段是 country_id,正好和我们在中间表 users 中新增的 country_id 吻合,所以不需要额外指定。
** 第四个参数 $secondKey 指的是中间模型类与关联模型类的关联外键** ,按照默认约定,在本例中拼接出来的字段是 user_id,正好和我们在关联表 posts 中定义的 user_id 吻合,所以也不需要额外指定。
第五个参数 $localKey 默认是当前模型类的主键 ID,第六个参数是中间模型类的主键 ID。
如果你的字段定义与 Eloquent 底层默认约定拼接出来的字段不一致,需要手动指定对应参数。
一对一的多态关联#
什么是一对一的多态关联#
接下来讲的三个关联关系都归属于多态关联,多态关联允许目标模型通过单个关联归属于多种类型的模型,根据模型之间的关联关系类型,又可以将多态关联细分为一对一、一对多和多对多三种关联。首先我们来看最简单的一对一多态关联。
一对一多态关联和上一篇的一对一关联有点类似,只不过这里的一对一关联是「多态」的,说理论太抽象,我们还是举例来说明。假设在我们的博客系统中用户可以设置头像,文章也可以设置缩略图,我们知道每个用户只能有一个头像,一篇文章也只能有一个缩略图,所以此时用户和图片之间是一对一关联,文章和图片之间也是一对一关联,通过多态关联,我们可以让用户和文章共享与图片的一对一关联,我们只需要在图片模型类通过一次定义,就可以动态建立与用户和文章的关联。
要建立这种多态管理,需要图片表结构支持与对应用户和文章的关联,只是,在这里,我们需要两个字段才能建立这种关联,一个是类型字段,表示归属于用户还是文章,另一个是ID字段,指向对应的用户/文章ID,这样,我们就可以结合这两个字段唯一确定该图片归属于哪个用户/哪篇文章了。
如何建立一对一的多态关联#
开始之前我们要创建图片模型类 Image 及其对应数据库迁移文件:
php artisan make:model Image -m
然后编写新创建的 create_images_table 迁移文件对应迁移类的 up 方法如下:
public function up()
{
Schema::create('images', function (Blueprint $table) {
$table->increments('id');
$table->string('url')->comment('图片URL');
$table->morphs('imageable');
$table->timestamps();
});
}
其中 $table->morphs('imageable') 用于创建 imageable_id 和 imageable_type 两个字段,其中 imageable_type 用于存放 User 模型类或 Post 模型类,而 imageable_id 用于存放对应的模型实例 ID,从而方便遵循默认约定建立多态关联。
运行 php artisan migrate 让迁移生效,准备好数据表和模型类后,接下来我们在模型类中建立一对一的多态关联。首先在 Image 模型类中通过 morphTo 建立其与 User/Post 模型类之间的关联:
public function imageable()
{
return $this->morphTo();
}
我们不需要指定任何字段,因为我们在创建数据表和定义关联方法的时候都遵循了 Eloquent 底层的约定,还是来看下 morphTo 方法的完整签名:
public function morphTo($name = null, $type = null, $id = null, $ownerKey = null)
第一个参数 $name 是关联关系名称,默认就是关联方法名,在本例中是 imageable。
第二个参数 $type、第三个参数 $id 结合第一个参数 $name 用于构建关联字段,在本例中就是 imageable_type 和 imageable_id。由于我们的数据库字段和关联方法名都遵循了默认约定,所以不需要额外指定。如果你的数据库字段名是自定义的,比如 item_id 和 item_type,那么就需要指定第一个参数值为 item。
最后一个参数是当前模型类的主键 ID。
这样,我们就可以在 images 表中填充一些测试数据进行测试了,你可以借助填充器来填充,或者手动插入,需要注意的是在 imageable_type 字段中需要插入完整的类名作为类型,比如 App\User 或者 App\Post,以便 Eloquent 在插询的时候结合 imageable_id 字段利用反射构造对应的模型实例。
这样,我们就可以在 Image 实例上获取其归属的模型实例了:
$image = Image::findOrFail(1);
$item = $image->imageable;
返回结果是对应的模型类实例
定义相对的关联关系#
当然,我们在日常开发中,更常见的是获取某个用户的头像或者某篇文章的缩略图,这样,我们就需要在 User 模型中定义其与 Image 模型的关联:
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
然后在 Post 模型中定义其与 Image 模型的关联:
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
同样,因为我们遵循了 Eloquent 底册的约定,只需要传入最少的参数即可建立关联。morphOne 方法的完整签名如下:
public function morphOne($related, $name, $type = null, $id = null, $localKey = null)
第一个参数表示关联的模型类。
第二个参数 $name、第三个参数 $type、第四个参数 $id 和前面的 morphTo 方法的前三个参数一样,用于在关联表中拼接关联外键,在本例中就是 imageable_type 和 imageable_id,所以第三个和第四个参数不需要额外指定,当然如果你是用的是 item_id 和 item_type 字段需要将第二个参数设置为 item,如果结尾不是以 type 和 id 作为后缀,也需要通过 $type 和 $id 参数传入。
最后一个参数 $localKey 表示当前模型类的主键 ID。
在模型类中定义完关联方法后,就可以在代码中通过相应方法获取关联模型了:
$post = Post::findOrFail(1);
$image = $post->image;
返回结果是 Image 模型实例
底层对应的查询 SQL 语句如下:
select
*
from
`images`
where
`images`.`imageable_id` = 1
and `images`.`imageable_id` is not null
and `images`.`imageable_type` = "App\Post"
limit
1
一对多的多态关联#
什么是一对多的多态关联#
理解了一对一的多态关联之后,一对多的多态关联理解起来就简单多了,其实就是模型类与关联类之间的关联变成一对多了,只不过这个一对多是多态的,如何理解这个多态,其实就是在关联表引入了类型的概念,关联表中的数据不再是与某一张表有关联,而是与多张表有关联,具体是哪张表通过关联类型来确定,具体与哪条记录关联,通过关联ID来确定。能理解到这个层面基本上就可以通吃多态关联了。这种逻辑和面向对象中的多态很像(面向对象三大特性:继承、封装、多态),所以将其称作「多态关联」。
下面我们还是以一个具体的例子来解释下一对多多态关联。
博客系统中免不了评论系统,以 Larave 学院为例,它支持两种类型的内容发布,一种是普通的文章,一种是独立的页面,分别存在两张表里。用户可以评论普通文章,也可以评论页面,我们不可能去为不同类型的内容分别创建评论表,因为它们的结构是完全一样的。我们知道如果单独看文章和评论,它们是一对多的关系,现在我们的评论表还要支持页面评论的存储,因此,需要引入一个类型字段做区分,这样,文章/页面与评论之间的关联关系就变成一对多的多态关联了。是不是这么看起来,也就那么回事了?
接下来,我们就来实际演示如何在模型类中建立一对多的多态关联。
在模型类中构建一对多多态关联#
首先还是要创建对应数据表和模型,我们先创建评论模型类 Comment 及其数据库迁移文件
php artisan make:model Comment -m
编写新生成的 create_comments_table 迁移文件对应迁移类的 up 方法如下:
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->increments('id');
$table->text('content')->comment('评论内容');
$table->integer('user_id')->unsigned()->default(0);
$table->morphs('commentable');
$table->index('user_id');
$table->softDeletes();
$table->timestamps();
});
}
然后创建一个 Page 模型类及其对应数据库迁移文件用于存放页面内容:
php artisan make:model Page -m
编写新生成的 create_pages_table 迁移文件对应迁移类的 up 方法如下:
public function up()
{
Schema::create('pages', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->integer('user_id')->unsigned()->default(0);
$table->index('user_id');
$table->softDeletes();
$table->timestamps();
});
}
运行 php artisan migrate 让迁移生效。
准备好数据库之后,我们通过填充器填充一些数据到刚创建的两张数据表。然后在 Comment 模型类中通过 Eloquent 提供的 morphTo 方法定义其与 Post 模型和 Page 之间的一对多多态关联:
public function commentable()
{
return $this->morphTo();
}
因为一个评论只会对应一篇文章/页面,所以,通过和一对一的多态关联同样的 morphTo 方法定义其与文章和页面的关联关系即可。和前面的一对一多态关联一样,因为我们的数据表字段和关联方法名都遵循了 Eloquent 底层的默认约定,所以不需要指定任何额外参数,即可完成关联关系的构建。这些默认约定我们在上面一对一多态关联中已经详细列出,这里就不再赘述了。
这样,我们就可以通过 Comment 实例查询其归属的文章或页面了:
$comment = Comment::findOrFail(1);
$item = $comment->commentable;
返回的结果是对应的模型实例
定义相对的关联关系#
同样,我们在日常开发中,更多的是通过文章或页面实例获取对应的评论信息,比如在文章页或页面页获取该文章或页面的所有评论。为此,我们需要在 Post 模型类和 Page 模型类中定义其与 Comment 模型的关联关系,这需要通过 morphMany 方法来实现:
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
和 morphOne 方法一样,因为我们遵循了 Eloquent 底层的默认约定,所以只需要传递很少的必要参数就可以定义关联关系了,morphMany 方法的完整签名如下:
public function morphMany($related, $name, $type = null, $id = null, $localKey = null)
这些参数的含义和 morphOne 方法完全一样,这里就不再赘述了。如果想要在 Post 模型下获取对应的所有评论,可以这么做:
$post = Post::with('comments')->findOrFail(23);
$comments = $post->comments;
返回的结果是 Comment 模型对象集合
对应的关联查询底层 SQL 语句是:
select
*
from
`comments`
where
`comments`.`commentable_id` in (23)
and `comments`.`commentable_type` = "App\Post"
and `comments`.`deleted_at` is null
多对多的多态关联#
什么是多对多的多态关联#
多对多的多态关联比前面的一对一和一对多更加复杂,但是有了前面讲解的基础,理解起来也很简单。你可以类比下常规的多对多关联,现在加入了「多态」的概念,常规的多对多需要借助中间表,多态的也是,只不过此时不仅仅是两张表之间的关联,而是也要引入类型字段。
还是以文章和标签的关联为例,在常规的多对多关联中,中间表只需要一个标签 ID 和文章 ID 即可建立它们之间的关联,但当我们添加新的内容类型,比如页面、视频、音频,它们也有标签,而且完全可以共享一张标签表,此时仅仅一个文章 ID 已经满足不了定义内容与标签之间的关联了,所以此时引入多对多的多态关联,和前面两种多态关联思路一样,只是在多对多关联中,我们需要在中间表中引入类型字段来标识内容类型,将原来的文章ID调整为内容ID,这样就可以从数据库层面满足不同内容类型与标签之间的关联了。
所以你可以看到从一对一、一对多(远层一对多)、多对多、一对一多态关联、一对多多态关联、多对多多态关联,它们之间是层层递进的,理解了前面的,后面的也就更好理解。
下面我们以标签与文章、页面关联关系为例,演示如何定义和使用多对多的多态关联。
在模型类中定义多对多的多态关联#
首先我们要废弃原来的 post_tags 数据表,创建一个新的 taggables 数据表来构建不同内容类型与标签之间的关联:
php artisan make:migration create_taggables_table --create=taggables
编写新生成的 create_taggables_table 迁移文件对应迁移类的 up 方法如下:
Schema::create('taggables', function (Blueprint $table) {
$table->increments('id');
$table->integer('tag_id');
$table->morphs('taggable');
$table->index('tag_id');
$table->unique(['tag_id', 'taggable_id', 'taggable_type']);
$table->timestamps();
});
运行 php artisan migrate 让迁移生效。然后通过填充器填充一些测试数据到新生成的 taggables 数据表。
接下来我们在 Tag 模型类中通过 Eloquent 提供的 morphedByMany 方法定义其与其他模型类的多对多多态关联:
public function posts()
{
return $this->morphedByMany(Post::class, 'taggable');
}
public function pages()
{
return $this->morphedByMany(Page::class, 'taggable');
}
和之前一样,因为我们遵循了 Eloquent 底层的默认约定,所以我们只需传递必需参数,无需额外配置即可定义关联关系,我们来看下 morphedByMany 方法的完整签名:
public function morphedByMany($related, $name, $table = null, $foreignPivotKey = null, $relatedPivotKey = null, $parentKey = null, $relatedKey = null)
其中第一个参数 $related 表示关联的模型类。
第二个参数 $name 表示关联的名称,和定义中间表数据库迁移的时候 morphs 方法中指定的值一致,也就是 taggable。
第三个参数 $table 表示中间表名称,默认是第二个参数 $name 的复数格式,这里就是 taggables 了,因为我们在创建数据表的时候遵循了这一约定,所以不需要额外指定。
第四个参数 $foreignPivotKey 表示当前模型类在中间表中的外键,默认拼接结果是 tag_id,和我们在数据表中定义的一样,所以这里不需要额外指定。
第五个参数 $relatedPivotKey 表示默认是通过 $name 和 _id 组合而来,表示中间表中的关联ID字段,这里组合结果是 taggable_id,和我们定义的一致,也不需要额外指定。
第六个参数 $parentKey 默认表示当前模型类的主键 ID,即与中间表中 tag_id 关联的字段。
第七个参数 $relatedKey 表示关联模型类的主键 ID,这个因 $related 指定的模型而定。
如果你不是按照默认约定的规则定义的数据库字段,需要明确每一个参数的含义,然后传入对应的参数值,和之前一样,对新手来说,还是按照默认约定来比较好,免得出错。
定义好上述关联关系后,就可以查询指定标签模型上关联的文章/页面了:
$tag = Tag::with('posts', 'pages')->findOrFail(53);
$posts = $tag->posts;
$pages = $tag->pages;
返回的结果都是对应的模型实例集合。
定义相对的关联关系#
最后,我们还可以在 Post 模型类或 Page 模型类中通过 Eloquent 提供的 morphToMany 方法定义该模型与 Tag 模型的关联关系(两个模型类中定义的方法完全一样):
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
因为我们遵循和 Eloquent 底层默认的约定,所以指定很少的参数就可以定义多对多的多态关联,morphToMany 方法的完整签名如下:
public function morphToMany($related, $name, $table = null, $foreignPivotKey = null, $relatedPivotKey = null, $parentKey = null, $relatedKey = null, $inverse = false)
其中前七个参数和 morphedByMany 方法含义一致,只不过针对的关联模型对调过来,最后一个参数 $inverse 表示定义的是否是相对的关联关系,默认是 false。如果你是不按套路出牌自定义的字段,需要搞清楚以上参数的含义并传入自定义的参数值。
定义好上述关联关系后,就可以通过 Post 模型或 Page 模型获取对应的标签信息了:
$post = Post::with('tags')->findOrFail(6);
$tags = $post->tags;
返回的结果也是模型集合
对应的底层查询 SQL 语句是:
select
`tags`.*,
`taggables`.`taggable_id` as `pivot_taggable_id`,
`taggables`.`tag_id` as `pivot_tag_id`,
`taggables`.`taggable_type` as `pivot_taggable_type`
from
`tags`
inner join `taggables` on `tags`.`id` = `taggables`.`tag_id`
where
`taggables`.`taggable_id` in (6)
and `taggables`.`taggable_type` = "App\Post"
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?