介绍 Eloquent 关联中的多态关联(Polymorphic Relations)

简介

你可能会这样设计你的博客系统:一张文章表(posts)和一张评论表(comments)。

posts
    id - integer
    title - string
    body - text

comments
    id - integer
    body - text
    post_id - integer

突然有一天,你开始录播视频教程了,那么就会多一个张视频表(videos)。

videos
    id - integer
    title - string
    url - string

此时,为了能够重用之前的评论表,就要对评论表修改了。怎么改才好呢?用冗余字段?

comments
    id - integer
    body - text
    post_id - integer
    video_id - integer

这当然没问题!但是,如果以后又多了什么图片、音频、名人名言之类的内容,它们也都可以评论,那是否就意味着评论表又变了?

comments
    id - integer
    body - text
    post_id - integer
    video_id - integer
    image_id - integer
    audio_id - integer
    quote_id - integer

这让人抓狂,因为冗余字段实在太多了,对于后台逻辑判断也是负担。Laravel 提供的解决方案是这样的:

comments
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string

使用 commentable_idcommentable_type 两个字段替代冗余字段的方式。comments 表的内容类似于这样:

id body commentable_id commentable_type
1 这是文章 1 的评论 1 posts
2 这是文章 2 的评论 2 posts
3 这是视频 1 的评论 1 videos
4 这是视频 2 的评论 2 videos
5 这是音频 1 的评论 1 audios
6 这是音频 2 的评论 2 audios

这样即使日后增加新的内容类型,只要定义一个新的 commentable_type 值就可以了。

我们称 Comment Model 与 Post Model、Video Model 的关系是多态关系,而在它们的 Model 中定义的关联称为多态关联

实现

创建表

php artisan make:model Models/Post -m -c

php artisan make:model Models/Video -m -c

php artisan make:model Models/Comment -m -c
Schema::create('posts', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title')->unique();
    $table->text('body');
    $table->timestamps();
});

Schema::create('videos', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
    $table->string('url')->unique();
    $table->timestamps();
});

Schema::create('comments', function (Blueprint $table) {
    $table->increments('id');
    $table->text('body');
    $table->unsignedInteger('commentable_id');
    $table->string('commentable_type');
    $table->timestamps();
});
php artisan migrate

定义关联关系

class Comment extends Model
{
    protected $fillable = ['body'];

    /**
     * 取得评论的文章/视频。
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    const TABLE = 'posts';

    protected $table = self::TABLE;

    /**
     * 取得文章评论
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
     */
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Video extends Model
{
    const TABLE = 'videos';

    protected $table = self::TABLE;

    /**
     * 取得视频评论
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
     */
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

AppServiceProvider boot 方法中自定义多态关联的类型字段。

use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Relations\Relation;

public function boot()
{
    $this->bootEloquentMorphs();
}

/**
 * 自定义多态关联的类型字段
 */
private function bootEloquentMorphs()
{
    Relation::morphMap([
        Post::TABLE => Post::class,
        Video::TABLE => Video::class,
    ]);
}

插入数据

在 ModelFactory 中定义 Model 的工厂方法。

use App\Models\Post;
use App\Models\Video;
use App\Models\Comment;

$factory->define(Post::class, function (Faker\Generator $faker) {
    return [
        'title' => $faker->sentence,
        'body' => $faker->text,
    ];
});

$factory->define(Video::class, function (Faker\Generator $faker) {

    return [
        'title' => $faker->sentence,
        'url' => $faker->url,
    ];
});

$factory->define(Comment::class, function (Faker\Generator $faker) {
    return [
        'body' => $faker->text,
        'commentable_id' => factory(Post::class)->create()->id,
        'commentable_type' => Post::TABLE,
    ];

//    return [
//        'body' => $faker->text,
//        'commentable_id' => factory(Video::class)->create()->id,
//        'commentable_type' => Video::TABLE,
//    ];
});

插入伪数据。

php artisan tinker

>>> namespace App;
>>> factory(Models\Comment::class, 10)->create();

使用

php artisan tinker

>>> namespace App\Models;
>>> $post = Post::find(1);
>>> $post->comments
=> Illuminate\Database\Eloquent\Collection {#733
     all: [
       App\Models\Comment {#691
         id: 1,
         body: "Ut omnis voluptatem esse mollitia nisi saepe vero. Est sed et eius pariatur hic harum sed. Laboriosam autem quis vel optio fugiat tota
m laboriosam.",
         commentable_id: 1,
         commentable_type: "posts",
         created_at: "2017-07-21 02:42:17",
         updated_at: "2017-07-21 02:42:17",
       },
     ],
   }
>>> $comment = Models\Comment::find(1);
>>> $comment->commentable
=> App\Models\Post {#731
     id: 4,
     title: "Earum est nisi praesentium numquam nisi.",
     body: "Dicta quod dolor quibusdam aut. Ut at numquam dolorem non modi adipisci vero sit. Atque enim cum ut aut dolore voluptas.",
     created_at: "2017-07-21 02:42:17",
     updated_at: "2017-07-21 02:42:17",
   }
>>> Post::find(1)->comments()->save(new Comment(['body' => 'a new comment']));
=> App\Models\Comment {#711
     body: "a new comment",
     commentable_type: "posts",
     commentable_id: 1,
     updated_at: "2017-07-21 06:45:28",
     created_at: "2017-07-21 06:45:28",
     id: 11,
   }
本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 6年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 9

@ruooooooli

$comment = Comment::with(['commentable' => function ($query){
    $query->select('a', 'b');
}])->find(1);
5年前 评论
怎样的心 3年前

请问一下用 CONST 定义表名有什么特殊的使用技巧吗?

6年前 评论

@tonyski 你能看到用这个 TABLE 常量的地方:

  1. 自定义多态关联的类型字段
  2. 插入伪数据
  3. 表名

这就能够说到它的好处了,就是解耦――可能改变的地方不要写死,统一约定名称,不会发生牵一发而动全身的情况,比较灵活。

6年前 评论
Artisan丶 3年前

之前也是这么想的, 这个更科学.

6年前 评论
ruolis

请问一下

$comment = Comment::with(['commentable'])->find(1);

像这种,我 with 的时候,怎么指定字段列表,只取需要的字段?

5年前 评论

@shankesgk2 那怎么查两个表里的指定字段呢?

5年前 评论

我现在遇到的问题是,morphMap里边,假如定义评论视频为1,评论文章为2。过了一阵,有另外个需求,对动态和音频点赞,我还是想定义对动态点赞为1,对音频点赞为2。这时候可怎么办???morphMap里边没法写了。

5年前 评论

这个 Relation::morphMap 是必须的么? 什么时候用 因为我没加 也是可以ok 的

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!