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

简介

你可能会这样设计你的博客系统:一张文章表(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,
   }
本帖由系统于 11个月前 自动加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 8

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

1年前
zhangbao

@Hexor ;)

1年前
tonyski

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

1年前
zhangbao

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

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

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

1年前

请问一下

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

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

8个月前

@ruooooooli

$comment = Comment::with(['commentable' => function ($query){
    $query->select('a', 'b');
}])->find(1);
8个月前

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

6个月前

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

2个月前

  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
  请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!