0%

Laravel Eloquent 模型关联关系(二)

前言

前面我们已经演示了一对一,一对多,多对多的关系这三种是日常工作中很常见的关联关系,我们会介绍更加复杂的关联关系,分别是远层一对多和多态关联。

远层一对多关联

远层一对多在一对多关联的基础上加上了一个修饰词【远层】,意味着这个一对多关系不是直接关联,而是【远层】关联,远层关联需要借助中间表,前面我们讨论多对多关联也是借助中间表,但是远层一对多与其区别在于还是一对多的关联。

理论和实例结合才容易理解。如果博客系统是针对全球市场的话,可能针对不同的国家推出不同的用户系统和功能,然后中国用户过来就只展示中国用户发布的文章,日本用户过来就只展示日本用户发表的文章,这里就涉及了三张表,存储国家的 countries 表,存储用户的 users 表以及存储文章的 posts 表。

用户与文章是一对多的关联关系,国家与用户之间是一对多的关联(一个用户只能有一个国际),那么通过用户这张中间表,国家和文章之间也建立起来一对多的关联,只是这个关联不是直接的关联,而是【远层】的关联。

针对这个情况,我们说国家和文章之间是远层的一对多关联。

建立远层一对多关联关系

我们要先创建 Country 模型类及其对应数据库迁移:

1
php artisan make:model Country -m

编写新生成的数据库迁移文件对应迁移类的 up 方法如下:

1
public function up()
2
{
3
    Schema::create('countries', function (Blueprint $table) {
4
        $table->increments('id');
5
        $table->string('name', 100)->unique();
6
        $table->string('slug', 100)->unique();
7
        $table->timestamps();
8
    });
9
}

然后,编写迁移文件为 users 表新增一个 country_id 字段:

1
php artisan make:migration alter_users_add_country_id --table=users

编写新生成的迁移类文件如下:

1
<?php
2
3
use Illuminate\Support\Facades\Schema;
4
use Illuminate\Database\Schema\Blueprint;
5
use Illuminate\Database\Migrations\Migration;
6
7
class AlterUsersAddCountryId extends Migration
8
{
9
    /**
10
     * Run the migrations.
11
     *
12
     * @return void
13
     */
14
    public function up()
15
    {
16
        Schema::table('users', function (Blueprint $table) {
17
            $table->integer('country_id')->unsigned()->default(0);
18
            $table->index('country_id');
19
        });
20
    }
21
22
    /**
23
     * Reverse the migrations.
24
     *
25
     * @return void
26
     */
27
    public function down()
28
    {
29
        Schema::table('users', function (Blueprint $table) {
30
            $table->dropColumn('country_id');
31
        });
32
    }
33
}

接下来,运行 php artisan migrate 让迁移生效。在 countries 表和 users 表填充一些测试数据便于后续测试。

准备好数据库、模型类并填充测试数据后,接下来,我们在 Country 模型类中通过 Eloquent 提供的 hasManyThrough 方法定义其与 Post 模型类之间的远层一对多关联:

1
public function posts()
2
{
3
    return $this->hasManyThrough(Post::class, User::class);
4
}

其中,第一个参数是关联的模型类,第二个参数是中间借助的模型类。

这样,我们就可以在代码中通过 Country 模型实例获取归属于该国家的所有文章了,查询方式和前面其它关联查询一样,可以懒惰式加载,也可以渴求式加载:

1
$country = Country::findOrFail(1);
2
$posts = $country->posts;

hasManyThrough 方法的签名

同样,我们在通过 hasManyThrough 方法定义一个远层一对多关联关系的时候,并没有指定关联字段,因为我们在定义字段数据库字段、模型类的时候遵循了 Eloquent 底层的约定:

1
public function hasManyThrough(
2
    $related, $through, $firstKey = null, $secondKey = null,
3
    $localKey = null, $secondLocalKey = null)
  • $related 关联模型类
  • $through 中间模型类
  • $firstKey 表示中间模型类与当前模型类的关联外键,按照默认约定,本例中凭借出来的字段是 country_id,正好和我们中间表 users 中新增的 country_id 吻合,所以不需要额外指定。
  • $secondKey 指的是中间模型类与关联模型类的关联外键,按照默认约定,在本例中凭借出来的字段是user_id,正好和我们在关联表 posts 中定义的 user_id 吻合,所以不需要额外指定。
  • $localKey 默认是当前模型类的主键 ID,
  • $secondLocalKey 是中间模型类的主键 ID

一对一的多态关联

接下来讲的三个关联关系都属于多态关联,多态关联允许目标模型通过单个关联归属多种类型的模型,根据模型之间关联关系类型,又可以将多态关联分为一对一,一对多,多对多三种关联。

这里的一对一关联是【多态】的,举个例子:假设在博客系统中用户可以设置头像,文章也可以设置缩略图,我们知道每个用户只能有一个头像,一篇文章只能有一个缩略图,所以此时:

  • 用户和图片是一对一关联
  • 文章和图片是一对一关联

通过多态关联,我们可以让用户和文章共享与图片的一对一关联,我们只需要在图片模型通过一次定义,就可以动态建立与用户和文章的关联。

如何建立一对一的多态关联

开始之前我们要创建图片模型类 Image 及其对应数据库迁移文件:

1
php artisan make:model Image -m

然后编写新创建的 create_images_table 迁移文件对应迁移类的 up 方法如下:

1
public function up()
2
{
3
    Schema::create('images', function (Blueprint $table) {
4
        $table->increments('id');
5
        $table->string('url')->comment('图片URL');
6
        $table->morphs('imageable');
7
        $table->timestamps();
8
    });
9
}

其中 $table->morphs(‘imageable’) 用于创建 imageable_id 和 imageable_type 两个字段,其中 imageable_type 用于存放 User 模型类或 Post 模型类,而 imageable_id 用于存放对应的模型实例 ID,从而方便遵循默认约定建立多态关联。

运行 php artisan migrate 让迁移生效,准备好数据表和模型类后,接下来我们在模型类中建立一对一的多态关联。首先在 Image 模型类中通过 morphTo 建立其与 User/Post 模型类之间的关联:

Image.php 创建方法 imageable:

1
public function imageable()
2
{
3
    return $this->morphTo();
4
}

我们不需要指定任何字段,因为我们在创建数据表和定义关联方法的时候都遵循了 Eloquent 底层的约定,还是来看下 morphTo 方法的完整签名:

1
public function morphTo($name = null, $type = null, $id = null, $ownerKey = null)
  • $name 是关联关系的 i 你改成,默认是关联的方法名,本例为imageable
  • $type $id 是结合 $name 用于构建的关联字段,就是 imageable_typeimageable_id。由于我们的数据库字段和关联方法名都遵循了默认约定,所以不需要额外指定。如果你的数据库字段名是自定义的,比如 item_iditem_type,那么第一个参数值为 `item.
  • $ownerKey 是当前模型的主键 id

这样,我们就可以在 images 表中填充一些测试数据进行测试了,你可以借助填充器来填充,或者手动插入,需要注意的是在 imageable_type 字段中需要插入完整的类名作为类型,比如 App\User 或者 App\Post,以便 Eloquent 在插询的时候结合 imageable_id 字段利用反射构造对应的模型实例.

定义相对的关联关系

当然,我们在日常开发中,更常见的是获取某个用户的头像或者某篇文章的缩略图,这样,我们就需要在 User 模型中定义其与 Image 模型的关联:

1
public function image()
2
{
3
    return $this->morphOne(Image::class, 'imageable');
4
}

然后在 Post 模型中定义其与 Image 模型的关联:

1
public function image()
2
{
3
    return $this->morphOne(Image::class, 'imageable');
4
}

同样,因为我们遵循了 Eloquent 底册的约定,只需要传入最少的参数即可建立关联。morphOne 方法的完整签名如下:

1
public function morphOne($related, $name, $type = null, $id = null, $localKey = null)
  • $related 表示关联的模型类
  • $name $type $id 和前面 morphTo 方法的前三个参数一样,用于在关联表中拼接关联外键,在本例中就是 imageable_typeimageable_id,所以第三个和第四个参数不需要额外指定,当然如果你是用的是 item_id 和 item_type 字段需要将第二个参数设置为 item,如果结尾不是以 type 和 id 作为后缀,也需要通过 $type 和 $id 参数传入。
  • $localKey 表示当前模型类的主键 ID。

一对多的多态关联

理解了一对一的多态关联之后,一对多的多态关联理解起来就简单多了,其实就是模型类与关联类之间的关联变成一对多了,只不过这个一对多是多态的,如何理解这个多态,其实就是在关联表引入了类型的概念,关联表中的数据不再是与某一张表有关联,而是与多张表有关联,具体是哪张表通过关联类型来确定,具体与哪条记录关联,通过关联 ID 来确定。能理解到这个层面基本上就可以通吃多态关联了。这种逻辑和面向对象中的多态很像(面向对象三大特性:继承、封装、多态),所以将其称作「多态关联」。

博客系统避免不了评论,一篇文章和单页面我们会区分开来,我们用户可以评论文章也可以评论单页面,但是这些评论可以统一存储在一个表中。

在模型类中构建一对多多态关联

首先还是要创建对应数据表和模型,我们先创建评论模型类 Comment 及其数据库迁移文件:

1
php artisan make:model Comment -m

编写新生成的 create_comments_table 迁移文件对应迁移类的 up 方法如下:

1
public function up()
2
{
3
    Schema::create('comments', function (Blueprint $table) {
4
        $table->increments('id');
5
        $table->text('content')->comment('评论内容');
6
        $table->integer('user_id')->unsigned()->default(0);
7
        $table->morphs('commentable');
8
        $table->index('user_id');
9
        $table->softDeletes();
10
        $table->timestamps();
11
    });
12
}

然后创建一个 Page 模型类及其对应数据库迁移文件用于存放页面内容:

1
php artisan make:model Page -m

编写新生成的 create_pages_table 迁移文件对应迁移类的 up 方法如下:

1
public function up()
2
{
3
    Schema::create('pages', function (Blueprint $table) {
4
        $table->increments('id');
5
        $table->string('title');
6
        $table->string('slug')->unique();
7
        $table->text('content');
8
        $table->integer('user_id')->unsigned()->default(0);
9
        $table->index('user_id');
10
        $table->softDeletes();
11
        $table->timestamps();
12
    });
13
}

运行 php artisan migrate 让迁移生效。

准备好数据库之后,我们通过填充器填充一些数据到刚创建的两张数据表。然后在 Comment 模型类中通过 Eloquent 提供的 morphTo 方法定义其与 Post 模型和 Page 之间的一对多多态关联:

1
public function commentable()
2
{
3
    return $this->morphTo();
4
}

因为一个评论只会对应一篇文章/页面,所以,通过和一对一的多态关联同样的 morphTo 方法定义其与文章和页面的关联关系即可。和前面的一对一多态关联一样,因为我们的数据表字段和关联方法名都遵循了 Eloquent 底层的默认约定,所以不需要指定任何额外参数,即可完成关联关系的构建。这些默认约定我们在上面一对一多态关联中已经详细列出,这里就不再赘述了。

这样,我们就可以通过 Comment 实例查询其归属的文章或页面了:

1
$comment = Comment::findOrFail(1);
2
$item = $comment->commentable;

定义相对的关联关系

同样,我们在日常开发中,更多的是通过文章或页面实例获取对应的评论信息,比如在文章页或页面页获取该文章或页面的所有评论。为此,我们需要在 Post 模型类和 Page 模型类中定义其与 Comment 模型的关联关系,这需要通过 morphMany 方法来实现:

1
public function comments()
2
{
3
    return $this->morphMany(Comment::class, 'commentable');
4
}

和 morphOne 方法一样,因为我们遵循了 Eloquent 底层的默认约定,所以只需要传递很少的必要参数就可以定义关联关系了,morphMany 方法的完整签名如下:

1
public function morphMany($related, $name, $type = null, $id = null, $localKey = null)

这些参数的含义和 morphOne 方法完全一样,这里就不再赘述了。如果想要在 Post 模型下获取对应的所有评论,可以这么做:

1
$post = Post::with('comments')->findOrFail(23);
2
$comments = $post->comments;

对应的关联查询底层 SQL 语句是:

1
select
2
  *
3
from
4
  `comments`
5
where
6
  `comments`.`commentable_id` in (23)
7
  and `comments`.`commentable_type` = "App\Post"
8
  and `comments`.`deleted_at` is null

多对多的多态关联

多对多的多态关联比前面的一对一和一对多更加复杂,但是有了前面讲解的基础,理解起来也很简单。你可以类比下常规的多对多关联,现在加入了「多态」的概念,常规的多对多需要借助中间表,多态的也是,只不过此时不仅仅是两张表之间的关联,而是也要引入类型字段。

还是以文章和标签的关联为例,在常规的多对多关联中,中间表只需要一个标签 ID 和文章 ID 即可建立它们之间的关联,但当我们添加新的内容类型,比如页面、视频、音频,它们也有标签,而且完全可以共享一张标签表,此时仅仅一个文章 ID 已经满足不了定义内容与标签之间的关联了,所以此时引入多对多的多态关联,和前面两种多态关联思路一样,只是在多对多关联中,我们需要在中间表中引入类型字段来标识内容类型,将原来的文章 ID 调整为内容 ID,这样就可以从数据库层面满足不同内容类型与标签之间的关联了。

所以你可以看到从一对一、一对多(远层一对多)、多对多、一对一多态关联、一对多多态关联、多对多多态关联,它们之间是层层递进的,理解了前面的,后面的也就更好理解。

下面我们以标签与文章、页面关联关系为例,演示如何定义和使用多对多的多态关联。

在模型类中定义多对多的多态关联

首先我们要废弃原来的 post_tags 数据表,创建一个新的 taggables 数据表来构建不同内容类型与标签之间的关联:

1
php artisan make:migration create_taggables_table --create=taggables

编写新生成的 create_taggables_table 迁移文件对应迁移类的 up 方法如下:

1
Schema::create('taggables', function (Blueprint $table) {
2
    $table->increments('id');
3
    $table->integer('tag_id');
4
    $table->morphs('taggable');
5
    $table->index('tag_id');
6
    $table->unique(['tag_id', 'taggable_id', 'taggable_type']);
7
    $table->timestamps();
8
});

运行 php artisan migrate 让迁移生效。然后通过填充器填充一些测试数据到新生成的 taggables 数据表。

接下来我们在 Tag 模型类中通过 Eloquent 提供的 morphedByMany 方法定义其与其他模型类的多对多多态关联:

1
public function posts()
2
{
3
    return $this->morphedByMany(Post::class, 'taggable');
4
}
5
6
public function pages()
7
{
8
    return $this->morphedByMany(Page::class, 'taggable');
9
}

和之前一样,因为我们遵循了 Eloquent 底层的默认约定,所以我们只需传递必需参数,无需额外配置即可定义关联关系,我们来看下 morphedByMany 方法的完整签名:

1
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 指定的模型而定。

如果你不是按照默认约定的规则定义的数据库字段,需要明确每一个参数的含义,然后传入对应的参数值,和之前一样,对新手来说,还是按照默认约定来比较好,免得出错。

定义好上述关联关系后,就可以查询指定标签模型上关联的文章/页面了:

1
$tag = Tag::with('posts', 'pages')->findOrFail(53);
2
$posts = $tag->posts;
3
$pages = $tag->pages;

定义相对的关联关系

最后,我们还可以在 Post 模型类或 Page 模型类中通过 Eloquent 提供的 morphToMany 方法定义该模型与 Tag 模型的关联关系(两个模型类中定义的方法完全一样):

1
public function tags()
2
{
3
    return $this->morphToMany(Tag::class, 'taggable');
4
}

因为我们遵循和 Eloquent 底层默认的约定,所以指定很少的参数就可以定义多对多的多态关联,morphToMany 方法的完整签名如下:

1
public function morphToMany($related, $name, $table = null, $foreignPivotKey = null, $relatedPivotKey = null, $parentKey = null, $relatedKey = null, $inverse = false)

其中前七个参数和 morphedByMany 方法含义一致,只不过针对的关联模型对调过来,最后一个参数 $inverse 表示定义的是否是相对的关联关系,默认是 false。如果你是不按套路出牌自定义的字段,需要搞清楚以上参数的含义并传入自定义的参数值。

定义好上述关联关系后,就可以通过 Post 模型或 Page 模型获取对应的标签信息了:

1
$post = Post::with('tags')->findOrFail(6);
2
$tags = $post->tags;

对应的底层查询 SQL 语句是:

1
select
2
  `tags`.*,
3
  `taggables`.`taggable_id` as `pivot_taggable_id`,
4
  `taggables`.`tag_id` as `pivot_tag_id`,
5
  `taggables`.`taggable_type` as `pivot_taggable_type`
6
from
7
  `tags`
8
  inner join `taggables` on `tags`.`id` = `taggables`.`tag_id`
9
where
10
  `taggables`.`taggable_id` in (6)
11
  and `taggables`.`taggable_type` = "App\Post"

总结

至此,关于 Eloquent 模型内置支持的七种关联关系定义我们就已经全部介绍完了。还是要多动手练习才能慢慢熟悉。