よくある1対多のテーブル構造
下記のようなカテゴリと商品を繋ぐデータベースがあったとする
- categoriesテーブル
id | name |
1 | お菓子 |
2 | アルコール |
- itemsテーブル
id | category_id | name |
1 | 1 | チョコレート |
2 | 2 | ブランデー |
例えばここに、「ウィスキーボンボン」という商品を追加したいとする
そうすると、カテゴリーはお菓子 かつアルコールになるが、
上記のテーブル構造だとどちらか1つしか登録できない・・・
そこで使用するのが中間テーブル
中間テーブルを使う流れ
上記例のように、多対多の関係を実現するために、「中間テーブル」というものを使ってみる
- 中間テーブルを作成する (マイグレーション)
- 各テーブルのModelにリレーションを設定する
- 実際に登録・取得してみる
1. 中間テーブルを作成する (マイグレーション)
早速まずはテーブルから作ってみる
規則自体は簡単で下の2ポイントのみ
- 2つのテーブルをアルファベット順に並べる
- 2つのテーブル名 (※単数形) を_ (アンダーバー) で繋げる
今回はitems
テーブルとcategories
テーブルなので、上記ルールに従うと、「category_item
」テーブルとなる
ここにcategory_idとitem_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の配列を引数に渡す
例えば、$item
にidが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で多対多のリレーションを扱うにあたり、中間テーブルを導入してみる