14
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Laravel】多対多のリレーションでsyncメソッドを使って中間テーブルを更新する

Posted at

はじめに

業務で「多対多(Many to Many)」のリレーションを持つテーブルおよび、リレーションの管理のための中間テーブルを用いる機会があった。

そこで、Laravelで中間テーブルを扱う方法や、その際に便利な attach メソッドやsync メソッド、またsyncメソッドの注意点など学んだことをまとめておく。

事前準備

テーブル構成

例として以下のようなテーブル構成の場合について説明する。

blogsとtagsは多対多の関係であり、blog_tagテーブルが中間テーブルとなる。

ER図.png

多対多の関係となるモデルとマイグレーションファイルを作成

まずは下記コマンドを実行し、blogsテーブルとtagsテーブルについてのモデルとマイグレーションファイルを作成する。

$ php artisan make:model Blog --migration
$ php artisan make:model Tag --migration
app/Models/Blog.php
<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
 
class Blog extends Model
{
    use HasFactory;
 
    protected $fillable = [
        'title',
    ];
 
    public function tags()
    {
        return $this->belongsToMany('App\Models\Tag')->withTimestamps();
    }
}
app/Models/Tag.php
<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
 
class Tag extends Model
{
    use HasFactory;
 
    protected $fillable = [
        'name',
    ];
 
    public function blogs()
    {
        return $this->belongsToMany('App\Models\Blog')->withTimestamps();
    }
}

ポイント

リレーションの記述の際に ->withTimestamps() を書くこと。
これにより中間テーブルのタイムスタンプを更新することが出来る。
 

また database/migrationsディレクトリに2つのマイグレーションファイルが生成されているので、編集する。

2022_04_14_055438_create_blogs_table.php
<?php
 
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
class CreateBlogsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('blogs', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('blogs');
    }
}
2022_04_13_055461_create_tags_table.php
<?php
 
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
class CreateTagsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('tags');
    }
}

中間テーブルを作成する

次に中間テーブルのマイグレーションファイルを作成する。

$ php artisan make:migration create_blog_tag_table

※基本的には中間テーブルの名前は、関係するモデル名(blogとtag)をアルファベット順に並べた「blog_tag」とする。

2022_04_13_060221_create_blog_tag_table.php
<?php
 
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
class CreateBookTagTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('blog_tag', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('blog_id');
            $table->unsignedBigInteger('tag_id');
            $table->foreign('blog_id')->references('id')->on('blogs')->onDelete('cascade');
            $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('blog_tag');
    }
}

ポイント

以下の部分で外部キー制約を設定している。
これにより、blogsテーブルからレコード削除した場合には中間テーブルからもレコード削除する(tagも同様)というようになる。

$table->foreign('blog_id')->references('id')->on('blogs')->onDelete('cascade');
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');

これでテーブルの用意ができた。

シーダーファイルやビューについてはここでは書かないが、blogsテーブル、tagsテーブルに適当なサンプルデータがあるものとして、次に attach メソッド・detachメソッド・syncメソッドの使い方を紹介していく。

リレーションの紐付け・解除

attachメソッド(新たに紐付けする場合)

attach メソッドを使うと、新たに紐付けの登録を行うことができる。

使い方

例えばブログに対してタグを紐づける場合は、ブログのモデルからリレーションを呼び出し、 attach メソッドで紐付けたいタグのIDを指定する。

以下のように、 attach メソッドには配列で複数のidを渡すこともできる。

// tagsテーブルのid=1のデータをブログに紐づける場合
Blog::first()->tags()->attach(1);

// tagsテーブルのid=1,2,3のデータをブログに紐づける場合
Blog::first()->tags()->attach([1, 2, 3]);

attachメソッドにより発行されるSQL

attach メソッドがどのようなSQLを実行しているのか出力してみると、以下の通り一度のINSERTが行われていた。

紐付け登録したいタグのidを複数渡したとしても、一度の INSERT で登録してくれている。

// attachメソッドに、ブログに紐付け対タグのタグのidを2つ渡した場合
insert into `blog_tag` (`created_at`, `blog_id`, `tag_id`, `updated_at`) values (?, ?, ?, ?), (?, ?, ?, ?)

sync メソッド実行時のSQLについても後述するが、仮に sync メソッドを使って同じことを行うと、紐付けしたいタグの回数分 INSERT が実行されていたので、単純に新たに紐付けを登録するだけの場合は attach メソッドの方が良いだろう。

detachメソッド(紐付けを解除する場合)

detach メソッドは attach メソッドの逆で、紐付けを解除するもの。

実行すると中間テーブルからレコードが削除される。

使い方

使い方は attach と同様。

配列で複数のidを渡すこともできる。

Blog::first()->tags()->detach(1);
Blog::first()->tags()->detach([1, 2, 3]);

syncメソッド(紐付けと紐付け解除をまとめて行う場合)

sync メソッドは、紐づけるidのリストを引数として渡すと、中間テーブルに登録されていない紐付けについては新たに紐付け登録を行い、引数に渡したidリストに無い紐付けは中間テーブルから削除を行う。

引数に渡したidリストでまるっと紐付けを上書きするイメージなので、引数に渡したid以外の紐付け登録は自動的に解除されることに留意。

※紐付け解除はせずに sync を行う syncWithoutDetaching というメソッドもある。

使い方

使い方は attach メソッドや detach メソッドと同様。

以下のように書くと、タグのid=1,2,3 の紐付けを登録し、これ以外の紐付けは中間テーブルから削除する。

Blog::first()->tags()->sync([1, 2, 3]);

syncメソッドにより発行されるSQL

sync メソッドがどんなSQLを実行しているのか出力してみると、流れは以下の通りだった。

  • 中間テーブル(folder_templateテーブル)から該当のテンプレートのtemplate_idを持つレコード取得(既存の紐付け登録状況を確認する)

  • 紐付けを解除するdelete実行(一度のdeleteで削除したいフォルダをまとめて削除する)

  • 紐付けを新規登録するinsert実行(登録したいフォルダの数だけinsertを行う)

※紐付けが維持されるフォルダに対してはdelete,insertは行われない。

例えば、元々あるブログに対してid=1,2のタグが紐づけられているときに、 sync([3, 4]) を実行した場合は以下の通りのSQLが実行される。

// 既存の紐付け登録状況を確認する
select * from `blog_tag` where `blog_tag`.`blog_id` = ?

// 一度のdeleteで削除したいタグをまとめて削除する
delete from `blog_tag` where `blog_tag`.`blog_id` = ? and `blog_tag`.`folder_id` in (?, ?)

// 登録したいタグの数だけinsertを行う
insert into `blog_tag` (`created_at`, `blog_id`, `folder_id`, `updated_at`) values (?, ?, ?, ?)
insert into `blog_tag` (`created_at`, `blog_id`, `folder_id`, `updated_at`) values (?, ?, ?, ?)

限られた規模感のアプリケーションだったり、紐付け登録する数に限度が見えているならば問題無いだろうが、紐付け登録したい回数分の INSERT が行われているということには留意。

注意点

紐付けを便利に行ってくれる syncメソッドだが、多対多の双方から紐付けを行う場合は注意が必要になる。
例えば以下のような動作を順に行うケースを考えてみる。

①ブログ側からブログAとタグBをsync (ブログAに紐づくタグは、タグBのみとなる)
②タグ側からタグCとブログAをsync (タグCに紐づくブログは、ブログAのみとなる)
③ブログ側からブログAとタグDをsync (ブログAに紐づくタグは、タグDのみとなる)

この場合、②までは問題なく行うことができるが、③を行うと②で行ったタグCとブログAの紐付けは解除されてしまうこととなる。

なので上記のように双方から紐付け登録を行いたい場合は、
・ブログ側からの紐付け: sync メソッドを使用する
・タグ側からの紐付け:条件分岐で attach メソッドと detach メソッドを使用して手動で紐付けの登録と解除を行う
などの方法を検討した方が良さそうだ。

さいごに

Laravel の恩恵を上手く利用すれば簡単に中間テーブルを扱うことが出来る反面、その挙動や発行されるSQLをきちんと把握して注意する必要があると学んだ。

Laravelのメソッドがどのような挙動をしているのか、どのようなSQLが発行されているのかを理解しつつ、便利なものを上手く使っていきたいと思う。

参考文献

14
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?