はじめに
業務で「多対多(Many to Many)」のリレーションを持つテーブルおよび、リレーションの管理のための中間テーブルを用いる機会があった。
そこで、Laravelで中間テーブルを扱う方法や、その際に便利な attach
メソッドやsync
メソッド、またsync
メソッドの注意点など学んだことをまとめておく。
事前準備
テーブル構成
例として以下のようなテーブル構成の場合について説明する。
blogsとtagsは多対多の関係であり、blog_tagテーブルが中間テーブルとなる。
多対多の関係となるモデルとマイグレーションファイルを作成
まずは下記コマンドを実行し、blogsテーブルとtagsテーブルについてのモデルとマイグレーションファイルを作成する。
$ php artisan make:model Blog --migration
$ php artisan make:model Tag --migration
<?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();
}
}
<?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つのマイグレーションファイルが生成されているので、編集する。
<?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');
}
}
<?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」とする。
<?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が発行されているのかを理解しつつ、便利なものを上手く使っていきたいと思う。
参考文献