目的
ユーザーがグループに所属する際に役割(role)を任意で設定できる
※roleはマスターでよくね?はひとまず放っておいて…
実行バージョンは Laravel 8.57.0
ドキュメントにもなかったので、書置きです!
構成例
Userテーブル
id | name | |
---|---|---|
1 | a@example.com | tanaka |
2 | b@example.com | saitou |
3 | a2@example.com | chiba |
Groupテーブル
id | name |
---|---|
1 | Aグループ |
2 | Bグループ |
中間テーブル
id | group_id | user_id | role |
---|---|---|---|
1 | 1 | 1 | リーダー |
2 | 2 | 2 | グループ長 |
モデル
use App\Models\Group;
class User extends Authenticatable
{
~諸々略~
public function groups()
{
return $this->belongsToMany(Group::class);
}
}
Groupモデルは割愛します。
結果だけで良い!って人は中間テーブルの値を更新(本題!)へ!
上記のような構成の場合、まず
中間テーブルへの追加(紐づける)
chiba
さんをAグループ
のサポーター
にする時
$user_id = 3; // chiba
$group_id = 1; // Aグループ
User::find(3)->groups()->attach($group_id, ['role' => 'サポーター']);
で、このtanaka
さんAグループ
でサポーター
という役割につくことができました。
昇格の時、経歴として考え、再度attach
メソッドでも良いかもですが、今回は更新します。
updateExistingPivotメソッド
公式だと、
updateExistingPivot
メソッドがぱっと目に入りました。
Eloquent:リレーション 8.x Laravel 中間テーブルのレコード更新
でも、既存のって書いてる…
横着したい…中間テーブル存在してなくても、作成して、存在してれば更新してほしい!!
ってので、見つけたのが
syncWithPivotValuesメソッド
Eloquent:リレーション 8.x Laravel 関連の同期
にある、syncWithPivotValues
メソッド
同期したモデルIDごとに同じ中間テーブルの値を挿入したい場合は、syncWithPivotValuesメソッドを使用できます。
落とし穴
上記の公式ドキュメントの参考ソース
$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);
この通り最初の構成を例に書き換えると
$user_id = 3; // chiba
$group_id = 1; // Aグループ
User::find(3)->groups()->syncWithPivotValues([$user_id], ['role' => 'サポーター']);
この結果…
tanaka
さんが消えた!
リーダーがいなくなった!イェーイ!
なんてこった 責任取るやつ 旗振りがいないなんて!
第一引数で渡していない外部キー(この場合ユーザーのID)たちはdetach
される…
同期処理の中にあるからね…同期だからね(ショック)
同期系メソッドでも他を残せないかsyncWithoutDetaching
メソッドみたいに!って思ったけど、
公式ドキュメントには載ってなかった…
とりあえず上記の各メソッドのソースコード実際に見てみよ…で見つけれたのが次!
※ちなみに、syncWithoutDetaching
メソッドは外部キーのみ対応しているので、シンプルな中間テーブルの場合これで事足りるはず。
中間テーブルの値を更新(本題!)
他はそのままで(上の件に書いてます!)、対象のみ(今回はchiba
さん)を
chiba
さんをAグループ
のサポーター
からサブリーダー
に更新する時
$user_id = 3; // chiba
$group_id = 1; // Aグループ
User::find(3)->groups()->syncWithPivotValues($group_id, ['role' => 'サブリーダー'], false);
結論だけ言うと、第3引数false
を利用するだけでした!
ドキュメントせめてデフォルト値あること書いてくれ!ってのと、改めてフレームワークの中身見るべきだなと…
※第1引数を配列でなくしているのは、下記の該当メソッド内で、collect
メソッドが実行されているため、単一の場合、配列からのcollection
クラスとして処理されるためです。
project\vendor\laravel\framework\src\Illuminate\Collections\Traits\EnumeratesValues.php
のgetArrayableItems
メソッドでその記述があります。
該当のメソッド
以下が該当のメソッド。
デフォルト値入ってますねー
/**
* Sync the intermediate tables with a list of IDs or collection of models with the given pivot values.
*
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
* @param array $values
* @param bool $detaching
* @return array
*/
public function syncWithPivotValues($ids, array $values, bool $detaching = true)
{
return $this->sync(collect($this->parseIds($ids))->mapWithKeys(function ($id) use ($values) {
return [$id => $values];
}), $detaching);
}
で、このメソッド内のsync
はこちら
こっちでもデフォルト値入ってますねー
/**
* Sync the intermediate tables with a list of IDs or collection of models.
*
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
* @param bool $detaching
* @return array
*/
public function sync($ids, $detaching = true)
{
$changes = [
'attached' => [], 'detached' => [], 'updated' => [],
];
// First we need to attach any of the associated models that are not currently
// in this joining table. We'll spin through the given IDs, checking to see
// if they exist in the array of current ones, and if not we will insert.
$current = $this->getCurrentlyAttachedPivots()
->pluck($this->relatedPivotKey)->all();
$detach = array_diff($current, array_keys(
$records = $this->formatRecordsList($this->parseIds($ids))
));
// Next, we will take the differences of the currents and given IDs and detach
// all of the entities that exist in the "current" array but are not in the
// array of the new IDs given to the method which will complete the sync.
if ($detaching && count($detach) > 0) {
$this->detach($detach);
$changes['detached'] = $this->castKeys($detach);
}
// Now we are finally ready to attach the new records. Note that we'll disable
// touching until after the entire operation is complete so we don't fire a
// ton of touch operations until we are totally done syncing the records.
$changes = array_merge(
$changes, $this->attachNew($records, $current, false)
);
// Once we have finished attaching or detaching the records, we will see if we
// have done any attaching or detaching, and if we have we will touch these
// relationships if they are configured to touch on any database updates.
if (count($changes['attached']) ||
count($changes['updated']) ||
count($changes['detached'])) {
$this->touchIfTouching();
}
return $changes;
}
余談
syncWithoutDetaching
メソッドは、sync
メソッドの第2引数をfalse
にして実行しているだけ
/**
* Sync the intermediate tables with a list of IDs without detaching.
*
* @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
* @return array
*/
public function syncWithoutDetaching($ids)
{
return $this->sync($ids, false);
}