介紹 Eloquent 關聯中的多型關聯(Polymorphic Relations)

zhangbao發表於2017-07-21

簡介

你可能會這樣設計你的部落格系統:一張文章表(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 協議》,轉載必須註明作者和本文連結

相關文章