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

えび

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

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

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

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

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

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

中間テーブルを使う流れ

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

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

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

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

Laravelの「中間テーブル」には命名規則があるので注意
規則自体は簡単で下の2ポイントのみ

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

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

  • マイグレーション作成
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();

        foreach ($items as $item) {
            echo "<br/>{$item->name}のカテゴリたち:";
            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検索などの応用編は下記記事にて

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