たけるのプログラミング

作ったものとか、気ままにアップします。

【Laravel】中間テーブルを触ってみる【追加・削除・同期】

超参考にしたドキュメントサイト
Eloquent:リレーション 8.x Laravel


例えばplayerとteamの関係について

1人のplayerは過去現在含めて複数のteamに所属して(いました)います。
1つのteamは過去現在含めて複数のplayerが所属して(いました)います。

つまりplayerとteamは多対多の関係になります。
多対多のリレーションを表すときは、多対多となるテーブルの中間地点に中間テーブルをかまし

1 対  対 1となるようなリレーションになるように設計します。

では上記に書いたplayerとteamのリレーションをlaravelに落とし込みたいと思います。

マイグレーションファイル

create_players_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('players', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('players');
    }
};

create_team_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('teams', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('teams');
    }
};
中間テーブル create_player_team_table

中間テーブルの命名規則
Eloquent:リレーション 8.x Laravel

によると

2つの関連するモデル名をアルファベット順に結合

となっています。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('player_team', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('player_id');
            $table->unsignedBigInteger('team_id');
            $table->string('note');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('player_team');
    }
};

モデルの定義

Player.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Team extends Model
{
    use HasFactory;

    public function players()
    {
        return $this->belongsToMany(Player::class);
    }
}

Team.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Team extends Model
{
    use HasFactory;

    public function players()
    {
        return $this->belongsToMany(Player::class);
    }
}

実際に使ってみる

 $player = Player::find(1)->teams;
 dd($player);

実行結果

中間テーブルで、user_idが1と対応付くteamsテーブルのレコード(Teamモデルのインスタンス)を取得することが出来ました。
hasOneやbelongsToだと対応ずくテーブルは外部キーを持っていることが一般的ですが、今回は中間テーブルとのリレーションを持っている話なので
playersテーブルはteamsテーブルの外部キーを持ちませんし、teamsテーブルもplayersテーブルの外部キーを持っていません。

中間テーブルの値にアクセスする

先程の実行結果のTeamモデルのインスタンスがどのような値を持っているのか確認すると

teamsテーブルのカラムに加えて中間テーブルの外部キーplayer_idの値がpivot_player_id、外部キーteam_idの値がpivot_team_idという名前で取得できていることがわかる。
この値にアクセスするには以下のようにする。firstはコレクションの最初のインスタンスを取得して、そのインスタンスに対して->pivot->team_id;として中間テーブルの値を取得している。

    $player = Player::find(1)->teams->first()
    ->pivot->team_id;

しかし上記の実行結果を見てみると、中間テーブルで設定したはずのnoteカラムがない。noteカラムの値を設定するには
モデル定義にて
Player.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Player extends Model
{
    use HasFactory;

    public function teams()
    {
        return $this->belongsToMany(Team::class)->withPivot('note');
    }
}

withPivot(カラム名)とする。そうすることで->pivot->noteとすると、中間テーブルのnoteカラムにアクセスできるようになる。
Pivotは中心という意味で「with中心(Pivot)のカラム」と捉えると感覚的に理解がしやすい。

中間テーブルに値を追加、削除する方法

attach()

$player = Player::find(2);
$player->teams()->attach(2,['note'=>'シャックと共に優勝!']);

player_teamテーブルにレコードを追加します。user_idカラムに2、team_idカラムに2、noteカラムに'シャックと共に優勝!'が設定されます。

detach()

$player = Player::find(2);
$player->teams()->detach(2);

player_teamテーブルから、user_idカラムが2、team_idカラムが2のレコードを削除する。
もしdetach()のように引数にteam_idを指定しないと、user_idカラムが2のレコードを全て削除する。

sync()

$player = Player::find(2);
$player->teams()->sync([1 => ['note' => 'hello'],2 => ['note'=>'hello2']]);

syncは中間テーブルの追加や削除といった認識より、文字通り同期をとるイメージ。
上記コードは中間テーブルのuser_idカラムが2のレコードにおいて、team_idが1と2のものを同期するという意味。
つまり既存の中間テーブルにuser_idとteam_idの組み合わせが2,3のものがあった場合に削除される。
また上記コードはuser_idが2以外のレコードには影響はない。

syncWithPivotValues()

$player = Player::find(2);
$player->teams()->syncWithPivotValues([1,2,3],['note'=>'hello']);

$player->teams()->sync([1 => ['note' => 'hello'],2 => ['note'=>'hello'],3=>['note'=>'hello']]);

と同じ意味

syncWithoutDetaching()

$player = Player::find(2);
$player->teams()->syncWithoutDetaching([4 =>['note'=>'hello']]);

既にテーブルのplayer_idとteam_idカラムの組み合わせとして
(2,1) (2,2) (2,3)があったときに、(2,4)として追加できる。
attach()と異なる点ですが、attachの場合はattach(1)を2回実行すると(,1)(,1)のように重複できてしまいます。
sync系は同期なので重複の心配がありません。