Skip to content

多态多对多

多对多多态最常见的应用场景就是标签,比如一篇文章对应多个标签,一个视频也对应多个标签,同时一个标签可能对应多篇文章或多个视频,这就是所谓的“多对多多态关联”。

此时仅仅在标签表 tags 上定义一个 item_iditem_type 已经不够了,因为这个标签可能对应多个文章或视频,那么如何建立关联关系呢,我们可以通过一张中间表 taggables 来实现:该表中定义了文章/视频与标签的对应关系。

软件版本

  • Laravel Version 5.4.19

  • PHP Version 7.0.8

关键字和表

  • morphToMany()

  • morphedByMany()

  • attach()

  • detach()

  • sync()

  • toggle()

  • postsvideostagstaggablesusers

生成模型和迁移文件

php artisan make:model Post -m
php artisan make:model Video -m
php artisan make:model Tag -m
php artisan make:model Taggable -m

编辑迁移文件

文件 <project>/database/migrate/*_create_users_table.php 内容如下

Schema::create('users' , function(Blueprint $table){
    $table->increments('id');
    $table->string('name');
    $table->string('email' , 30)->unique();
    $table->string('password');
    $table->rememberToken();
    $table->timestamps();
});

文件 <project>/database/migrate/*_create_posts_table.php 内容如下

Schema::create('posts' , function(Blueprint $table){
    $table->increments('id');
    $table->unsignedInteger('user_id');
    $table->string('title' , 60);
    $table->unsignedInteger('views')->comment('浏览数');
    $table->text('body');
    $table->timestamp('published_at')->nullable();
    $table->timestamps();

    $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onUpdate('cascade')
        ->onDelete('cascade');
});

文件 <project>/database/migrate/*_create_videos_table.php 内容如下

Schema::create('videos' , function(Blueprint $table){
    $table->increments('id');
    $table->unsignedInteger('user_id')->comment('用户id');
    $table->unsignedTinyInteger('status')->comment('数据状态');
    $table->string('title' , 30)->comment('标题');
    $table->string('description' , 120)->comment('描述');
    $table->text('body')->comment('内容');
    $table->timestamps();

    $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onUpdate('cascade')
        ->onDelete('cascade');
});

文件 <project>/database/migrate/*_create_tags_table.php 内容如下

Schema::create('tags', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name',20)->default('')->comment('标签名');
    $table->timestamps();
});

文件 <project>/database/migrate/*_create_taggables_table.php 内容如下

Schema::create('taggables' , function(Blueprint $table){
    $table->increments('id');
    $table->unsignedInteger('taggable_id')->comment('数据id');
    $table->string('taggable_type' , 40)->comment('关联模型');
    $table->unsignedInteger('tag_id')->comment('标签id');
    $table->timestamps();
});

运行 php artisan 命令保存修改到数据库

php artisan migrate

执行上面的命令后数据库将生成七张表, migrations password_resets posts taggables tags users videos

定义关联关系和修改模型的 fillable 属性

Post 模型中定义关联关系:

public function tags()
{
  return $this->morphToMany('App\Tag','taggable');
}

Video 模型中定义关联关系:

public function tags()
{
  return $this->morphToMany('App\Tag','taggable');
}

Tag 模型中定义关联关系:

public $timestamps = false;

// 多对多多态关联
public function posts()
{
return $this->morphedByMany('App\Post','taggable')->withTimestamps();
}
// 多对多多态关联
public function videos()
{
return $this->morphedByMany('App\Video','taggable')->withTimestamps();
}

使用 tinker 填充数据

修改 /databases/factories/ModelFactory.php,新增关联数据。

/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\User::class, function (Faker\Generator $faker) {
    static $password;

    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => $password ?: $password = bcrypt('secret'),
        'remember_token' => str_random(10),
    ];
});

$factory->define(App\Post::class, function (Faker\Generator $faker) {
    $user_ids = \App\User::pluck('id')->toArray();
    return [
        'user_id' => $faker->randomElement($user_ids),
        'title' => $faker->title,
        'body' => $faker->text(),
        'views' => $faker->numberBetween(0, 1000),
    ];
});

$factory->define(App\Video::class, function (Faker\Generator $faker) {
    $user_ids = \App\User::pluck('id')->toArray();
    return [
        'user_id' => $faker->randomElement($user_ids),
        'title' => $faker->title,
        'body' => $faker->text(),
        'description' => $faker->title,
        'status' => 1
    ];
});

$factory->define(App\Tag::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->lastName,
    ];
});

使用 tinker 命令

php artisan tinker

## 进入到 tinker 界面执行如下命令
namespace App
factory(User::class,4)->create(); // 生成4个用户
factory(Post::class,20)->create() // 生成20条 posts 表的测试数据
factory(Video::class,20)->create() // 生成20条 videos 表的测试数据

关联操作

新增数据

添加一个文章标签

$tag = new \App\Tag(['name' => 'A Post Tag For 1.']);
$post = \App\Post::find(1);
$post->tags()->save($tag); // 新增的 `tag` 模型中 `taggable_id` 和 `taggable_type` 字段会被自动设定

添加多个文章标签

$tags = [
    new \App\Tag(['name' => 'A Post Tag For 2.']),
    new \App\Tag(['name' => 'A Post Tag For 2.'])
];
$post = \App\Post::find(2);
$post->tags()->saveMany($tags); // 新增的 `tag` 模型中 `taggable_id` 和 `taggable_type` 字段会被自动设定

添加一个视频标签

$tag = new \App\Tag(['name' => 'A Post Tag For 2.']);
$video = \App\Video::find(2);
$video->tags()->save($tag); // 新增的 `tag` 模型中 `taggable_id` 和 `taggable_type` 字段会被自动设定

添加多个视频标签

$tags = [
    new \App\Tag(['name' => 'A Video Tag For 1.']),
    new \App\Tag(['name' => 'A Video Tag For 1.']),
];
$video = \App\Video::find(1);
$video->tags()->saveMany($tags);

删除数据

删除一篇文章下的所有标签

$post = \App\Post::find(1);

$post->tags()->delete();  // 删除tags Table 中的关联数据
$post->tags()->detach(); // 同步删除 toggables Table中的关联数据

查询数据

查询一篇文章的标签

$post = \App\Post::find(2);
$tags = $post->tags;

查询一个视频的标签

$video = \App\Video::find(1);
$tags = $video->tags;

查询标签对应节点

$tag = \App\Tag::find(1);
$posts = $tag->posts;

编辑数据

其他

建立关联

tags 跟 videosposts 做关联

$tag->videos()->attach($video->id);

$tag->posts()->attach($post->id);

videosposts 跟 tag 做关联

$videos->tags()->attach($tag->id);

$post->tags()->attach($tag->id);

将视频或者文字添加某个标签

删除关联

$tag->videos()->detach($vedio->id);

$tag->posts()->detach($post->id);