介绍 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_id
和 commentable_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年前 自动加精
之前也是这么想的, 这个更科学.
@Hexor ;)
请问一下用
CONST
定义表名有什么特殊的使用技巧吗?@tonyski 你能看到用这个 TABLE 常量的地方:
这就能够说到它的好处了,就是解耦――可能改变的地方不要写死,统一约定名称,不会发生牵一发而动全身的情况,比较灵活。
请问一下
像这种,我
with
的时候,怎么指定字段列表,只取需要的字段?@ruooooooli
@shankesgk2 那怎么查两个表里的指定字段呢?
我现在遇到的问题是,morphMap里边,假如定义评论视频为1,评论文章为2。过了一阵,有另外个需求,对动态和音频点赞,我还是想定义对动态点赞为1,对音频点赞为2。这时候可怎么办???morphMap里边没法写了。
这个 Relation::morphMap 是必须的么? 什么时候用 因为我没加 也是可以ok 的