【Laravel】中間テーブルを使ってみる (基本編)

えび

Laravel多対多のリレーションを扱うにあたり、中間テーブルを導入してみる

よくある1対多のテーブル構造

下記のようなカテゴリ商品を繋ぐデータベースがあったとする

  • categoriesテーブル
idname
1お菓子
2アルコール
  • itemsテーブル
idcategory_idname
11チョコレート
22ブランデー

例えばここに、「ウィスキーボンボン」という商品を追加したいとする
そうすると、カテゴリーお菓子 かつアルコールになるが、
上記のテーブル構造だとどちらか1つしか登録できない・・・

そこで使用するのが中間テーブル

中間テーブルを使う流れ

上記例のように、多対多の関係を実現するために、「中間テーブル」というものを使ってみる

流れ
  1. 中間テーブルを作成する (マイグレーション)
  2. 各テーブルのModelにリレーションを設定する
  3. 実際に登録・取得してみる

1. 中間テーブルを作成する (マイグレーション)

早速まずはテーブルから作ってみる

※Laravelの「中間テーブル」には命名規則があるので注意

規則自体は簡単で下の2ポイントのみ

  1. 2つのテーブルをアルファベット順に並べる
  2. 2つのテーブル名 (※単数形)_ (アンダーバー) で繋げる

今回はitemsテーブルとcategoriesテーブルなので、上記ルールに従うと、「category_item」テーブルとなる

ここにcategory_iditem_idを持たせてあげることで
中間テーブルとして機能するようになる

  • マイグレーション作成
php artisan make:migration create_category_item_table
  • 内容
<?php

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

class CreateCategoryItemTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('category_item', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('category_id')->comment('カテゴリID');
            $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
            $table->unsignedBigInteger('item_id')->comment('商品ID');
            $table->foreign('item_id')->references('id')->on('items')->onDelete('cascade');
            $table->timestamps();
        });
        
     /*** これより下は既にitemsテーブルにcategory_idカラムがくっついていた場合のみ ***/
        // 1. 既存データ移行
        \App\Models\Item::all()->each(function ($item) {
            $item->categories()->sync([ $item->category_id ]);
        });
        // 2. 既存カラム削除
        Schema::table('items', function (Blueprint $table) {
            $table->dropForeign('items_category_id_foreign');
            $table->dropColumn('category_id');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('category_item');
        /*** これより下は既にitemsテーブルにcategory_idカラムがくっついていた場合のみ ***/
        Schema::table('items', function (Blueprint $table) {
            $table->unsignedBigInteger('category_id')->comment('カテゴリID')->nullable()->after('name');
            $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
        });
    }
}

2. 各テーブルのModelにリレーションを設定する

各テーブルのModelにBelongsToManyリレーションを設定する

  • app/Models/Item.php
<?php

namespace App\Models;

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

class Item extends Model
{
    use HasFactory;

    protected $guarded = ['id'];

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class);
    }
}

  • app/Models/Category.php
<?php

namespace App\Models;

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

class Category extends Model
{
    use HasFactory;

    protected $guarded = ['id'];
    
    public function items(): BelongsToMany
    {
        return $this->belongsToMany(Item::class);
    }
}

これで中間テーブルのモデルを作成しなくても、
お互いを参照できるようになる (便利~)

3. 実際に登録・取得してみる

  • ItemController.php
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Item;
use Illuminate\Http\Request;

class ItemController extends Controller
{
    protected Item $item;

    /**
     * ItemController constructor.
     * @param Item $item
     */
    public function __construct(Item $item)
    {
        $this->item = $item;
    }

    /**
     * 商品登録
     */
    public function store(Request $request)
    {
        // まずは商品を登録
        $item = $this->item->create([
            'name' => 'ウィスキーボンボン'
        ]);
        // 紐づくカテゴリーを登録
        $item->categories()->sync([1, 2]);
    }

    /**
     * 商品取得
     */
    public function index()
    {
        // 全ての商品を取得
        $items = $this->item->all();
        // 1つずつ取り出す
        foreach ($items as $item) {
            // 商品名表示
            echo "<br/>{$item->name}のカテゴリたち:";
            // 商品に紐づくカテゴリーを1つずつ取り出す
            foreach ($item->categories as $category) {
                // カテゴリー名表示
                echo "{$category->name} ";
            }
        }
    }
}
  • もし登録する値を$requestから受け取る場合のコードは下記の通り
    (※ formから受け取るデータは「name」に商品名、「category_ids」にカテゴリIDが配列で入ってる前提とする)
public function store(Request $request)
{
    $item = $this->item->create([
        'name' => $request->get('name'),
    ]);
    $item->categories()->sync($request->get('category_ids', []));
}

登録に関して補足

多対多の登録&更新には、sync()メソッドを使用すると便利

sync()メソッドには、
中間テーブルに保存する(=紐付ける) IDの配列を引数に渡す

例えば、$itemidが1,2$categoryを紐付けたい場合はこんな感じ
$item->categories()->sync([1, 2]);

ちなみに指定した配列に無いIDは、中間テーブルから削除される
なので、重複チェックを行う必要もなく更新時でも気にせず利用できる優れもの

取得に関して補足

ModelでbelongsToManyのリレーションを設定しているので、
関連データは$item->categories()$category->items()のように取得できる

// PHP
foreach ($item->categories as $category) {
  echo "{$category->name} ";
}

// view
@foreach ($item->categories as $category)
  {{ $category->name }} <br>
@endforeach

実際のフォームの作成や、リレーションがされているかどうかのチェック、
where検索などの応用編は下記記事にて

\ 案件のご依頼・ご相談はこちらから /