こんにちは!Laravelでの開発を楽しんでいますか?
LaravelのEloquent ORMはリレーションを非常に簡単に扱えますが、「1対多」や「多対多」の関連データをフォームから受け取って登録・更新する処理は、意外と面倒に感じることがあります。特に、チェックボックスで選択されたカテゴリIDを中間テーブルに保存するようなケースです。
「既存の関連を全部消して、新しいのを登録し直す?」「いや、差分だけ計算して追加・削除?」
そんな悩みを一発で解決してくれるのが、今回ご紹介する sync() メソッドです。この記事では、SEOキーワード「Laravel 1対多 更新」で検索してたどり着いたあなたにも、sync() の圧倒的な利便性をお伝えします。
あれ? 1対多? 多対多?
「Laravel 1対多 更新」で検索されたかもしれませんが、sync() メソッドが本領を発揮するのは、実は「多対多 (belongsToMany)」リレーションシップです。
例えば「1つの商品 (Product) が複数のカテゴリ (Category) に属し、1つのカテゴリにも複数の商品が属する」といった関係です。この関係は「中間テーブル」を使って実現されます。
もしあなたのケースが「1対多(例: 1ユーザーが複数の投稿を持つ)」ではなく、この「多対多」に当てはまるなら、sync() は最強の武器になります!
`sync()` メソッドはなぜこんなに便利なのか?
結論から言うと、sync() は「中間テーブルの状態を、渡した配列と全く同じ状態に同期してくれる」からです。
例えば、ある商品 (ID: 1) に関連するカテゴリIDを [1, 3, 5] にしたい場合を考えます。
従来の面倒な処理
sync() を使わない場合、以下のような複雑な処理が必要でした。
// 1. フォームから新しいカテゴリIDの配列を取得
$newCategoryIds = $request->input('category_list', []); // 例: [1, 3, 5]
// 2. 現在の商品を取得
$product = Product::find(1);
// 3. 現在関連付いているカテゴリIDを取得
$currentCategoryIds = $product->categories()->pluck('id')->toArray(); // 例: [1, 2, 4]
// 4. 削除すべきIDを計算 (現在にあって、新しいにない)
$detachIds = array_diff($currentCategoryIds, $newCategoryIds); // [2, 4]
// 5. 追加すべきIDを計算 (新しいにあって、現在にない)
$attachIds = array_diff($newCategoryIds, $currentCategoryIds); // [3, 5]
// 6. 削除実行
if (!empty($detachIds)) {
$product->categories()->detach($detachIds);
}
// 7. 追加実行
if (!empty($attachIds)) {
$product->categories()->attach($attachIds);
}
差分を計算して、detach() (削除) と attach() (追加) を呼び分ける…。非常に面倒ですね。
`sync()` を使った場合の処理
これが sync() を使うと、たった1行になります。
魔法の1行:
$product = Product::find(1);
$newCategoryIds = $request->input('category_list', []); // 例: [1, 3, 5]
// これだけ!
$product->categories()->sync($newCategoryIds);
この1行で、Laravelは裏側で以下の処理を自動的に実行してくれます。
- 渡された配列
[1, 3, 5]に含まれていない既存の関連 (例: ID 2, 4) を中間テーブルから削除 (detach) します。 - 渡された配列
[1, 3, 5]のうち、まだ中間テーブルに存在しない関連 (例: ID 3, 5) を追加 (attach) します。 - 渡された配列
[1, 3, 5]のうち、すでに存在する関連 (例: ID 1) はそのまま保持します。
まさに「同期」。必要なIDの配列を渡すだけで、中間テーブルを最新の状態にしてくれるのです。
具体的な使用例(テーブル・モデル・コントローラ)
「商品 (Product)」と「カテゴリ (Category)」の多対多リレーションを例に見てみましょう。
1. テーブル構成例
多対多リレーションには3つのテーブルが必要です。
`products` テーブル
| id | name | created_at | updated_at |
|---|---|---|---|
| 1 | すごい商品A | … | … |
`categories` テーブル
| id | name | created_at | updated_at |
|---|---|---|---|
| 1 | 家電 | … | … |
| 2 | 食品 | … | … |
| 3 | 書籍 | … | … |
`category_product` テーブル (中間テーブル)
これが「多対多」のキモです。Laravelの規約では、モデル名の単数形をアルファベット順に並べます (category_product)。
| id | product_id | category_id |
|---|---|---|
| 101 | 1 | 1 |
| 102 | 1 | 2 |
(この状態は「すごい商品A」が「家電」と「食品」に属していることを示します)
2. モデル (Model) の設定
belongsToMany を使ってリレーションを定義します。
app/Product.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
/**
* この商品が属するカテゴリ (多対多)
*/
public function categories()
{
// 'category_product' 中間テーブルを経由して
// 'App\Category' モデルに関連付く
return $this->belongsToMany('App\Category', 'category_product', 'product_id', 'category_id');
// 規約通りなら、引数は省略して以下でもOK
// return $this->belongsToMany('App\Category');
}
}
app/Category.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
/**
* このカテゴリに属する商品 (多対多)
*/
public function products()
{
return $this->belongsToMany('App\Product', 'category_product', 'category_id', 'product_id');
}
}
3. コントローラ (Controller) での更新処理
商品編集フォーム(カテゴリをチェックボックスで選択)からの更新処理です。
app/Http/Controllers/ProductController.php
<?php
namespace App\Http\Controllers;
use App\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
/**
* 商品情報を更新します。
*/
public function update(Request $request, $id)
{
// 1. 更新対象の商品を取得
$product = Product::findOrFail($id);
// 2. 商品名など、他の情報を更新
$product->name = $request->input('name');
$product->save();
// 3. カテゴリ情報を同期
// フォームから 'category_list' という名前でカテゴリIDの配列が送られてくると仮定
// (例: [1, 3] ... "家電" と "書籍" が選択された)
// チェックボックスが空の場合も考慮し、デフォルト値 [] を指定
$categoryIds = $request->input('category_list', []);
$product->categories()->sync($categoryIds);
// ---
// もし sync([1, 3]) が実行されると、中間テーブルは...
//
// 削除: (product_id: 1, category_id: 2) ... 食品
// 維持: (product_id: 1, category_id: 1) ... 家電
// 追加: (product_id: 1, category_id: 3) ... 書籍
// ---
return redirect()->route('products.show', $product->id)
->with('success', '商品を更新しました。');
}
}
これで完了です! フォームから送られてきたカテゴリIDの配列 [1, 3] だけで、中間テーブルが正しく更新されました。
`sync()` を使う上での重要すぎる注意点
sync() は非常に便利ですが、その動作特性を理解していないと予期せぬデータ削除につながる可能性があります。
1. `sync()` は多対多 (belongsToMany) 専用
sync() は中間テーブルを操作するためのメソッドです。1対多 (hasMany) リレーション(例: $user->posts())では使えません。「Laravel 1対多 更新」で検索した方は、まずご自身の設計が多対多(中間テーブルがあるか)かどうかを確認してください。
2. `sync()` は「削除」も行う
これが最大の注意点です。sync() は「同期」なので、渡した配列に含まれていないIDの関連は、容赦なく削除 (detach) します。
もし「既存の関連はそのままにして、新しいものだけ追加したい」場合は、sync() ではなく syncWithoutDetaching() を使います。
// [1, 3] を追加する。既存の [1, 2] があっても 2 は削除しない。
// 結果: [1, 2, 3] (重複は自動的に無視される)
$product->categories()->syncWithoutDetaching([1, 3]);
3. 空配列 [] を渡すと、すべて削除される
$product->categories()->sync([]); を実行すると、その商品の関連カテゴリがすべて削除されます。
これは、カテゴリ選択のチェックボックスが1つもチェックされなかった場合に便利です。コントローラの例で $request->input('category_list', []) としているのは、チェックが0件のときに null ではなく空配列 [] を渡し、すべての関連を削除する(=意図した動作)ためです。
4. 中間テーブルに追加カラムがある場合
もし中間テーブル category_product に product_id, category_id 以外のカラム(例: display_order)がある場合、sync() の第2引数でキーと値のペアとして渡す必要があります。
// [カテゴリID => [追加カラム => 値]] の形式
$product->categories()->sync([
1 => ['display_order' => 10],
3 => ['display_order' => 20]
]);
まとめ
Laravelの sync() メソッドは、多対多 (belongsToMany) リレーションにおける中間テーブルの更新処理を、劇的に簡潔かつ安全にしてくれる強力な機能です。
「Laravel 1対多 更新」で調べていた方も、もし対象が中間テーブルを持つ「多対多」の更新であれば、sync() こそが求めていた答えのはずです。
その「削除も伴う同期」という動作を正しく理解し、日々の開発をスピードアップさせましょう!
※参考にされる場合は自己責任でお願いします。