業務で、複数のデータについて、
・既存のデータがDBにあれば更新
・既存のデータがDBになければレコード追加
を行いたいという場面がありました。
そこでupdateかinsertのどちらを行うかよしなに判断してBulk insertを行ってくれるという、すごく便利なEloquentのupsertメソッドの存在を知ったので、備忘録を書いておきます。
upsertメソッドとは
Laravel8から実装された機能で、1つのクエリで複数のアップサートを実行できるメソッドです。
※アップサート(upsert)とは
update + insertのことで、データの更新(update)とデータの挿入(insert)の両方の機能を併せ持つ。
対象のレコードがあればそのレコードを更新し、レコードがなければレコードの新規追加をするという場合で使う。
Laravel7までにもupdateOrCreateメソッドやfirstOrCreateメソッドという一つのレコードをアップサートするメソッドはありましたが、upsertメソッドにより複数レコードをBulk insertできるようになったようです。
使い方
usersテーブルに複数のデータをupsertする例を書いてみます。
前提として、DBのusersテーブルには
・id:1、name:taro
・id:2、name:hanako
の2つのレコードが既に存在するとします。
これに対して以下の通りupsertメソッドを実行します。
User::upsert([
['id' => 1, 'name' => 'taro', 'age' => 20], // update
['id' => 2, 'name' => 'jiro', 'age' => 22], // update
['id' => null, 'name' => 'taro', 'age' => 24] // insert
], ['id'], ['name', 'age']);
結果は、第2引数に渡したidが一致するレコードがあった1つ目と2つ目はupdateとなり、3つ目はinsertになります。
構文
upsertメソッドは3つの引数を持ちます。
-
第1引数
updateまたはinsertを行いたい値を配列で渡す。 -
第2引数
レコードを一意に識別するカラム(プライマリーキーかユニークキー)を配列で渡す。 -
第3引数(省略可)
updateが行われる際に、更新する必要があるカラムを配列で渡す。
省略すると全カラム更新される。
注意点
色んな引数を渡して動作確認をしてみて、気を付けるべきと思ったことを挙げておきます。
第2引数に指定して意味があるのはプライマリーキーもしくはユニークキーのみ
上記の例で第2引数に['id', 'name']を渡したとします。
すると2つ目のデータ['id' => 2, 'name' => 'jiro', 'age' => 22]については、既存のDBのデータはid:2、name:hanakoでありnameが一致しないのでinsertになる…と思いきや、updateが実行されました。
Readoubleにも
2番目の引数は、関連付けられたテーブル内のレコードを一意に識別するカラムをリストします。
と書いてあるので、第2引数に渡せるのは一意に識別できるカラムに限るようです。
第2引数には第1引数に渡しているカラムを渡す
先ほどの例で、以下のように第1引数にはidを渡さず、第2引数にidを渡したとします。
User::upsert([
['name' => 'taro', 'age' => 20],
['name' => 'jiro', 'age' => 22],
['name' => 'taro', 'age' => 24]
], ['id'], ['name', 'age']);
この場合はidが一致するカラムがあったとしても全てinsertになりました。
なので第2引数に指定するカラムは、必ず第1引数にも渡しておかないといけません。
created_atとupdated_atは自動で設定される
モデルのタイムスタンプが有効になっている場合、created_atとupdated_atのカラムは自動的に値を入れてくれます。
第1引数にはcreated_atとupdated_atについて何も書かなくても、
-
update:updated_atのみ更新 -
insert:created_atとupdated_atは処理をしたときの時刻
としてくれます。
普通の insertメソッドを使ってbulk insertする場合は、created_atとupdated_atは自動入力されず明示的に含める必要があるので、これは便利だなと思いました。
【おまけ】実装例
実際のコードの書き方の例も残しておきたいと思います。
これはupsertだけでなく通常のbulk insertにも言えることですが、DBを更新するための配列をforeachやfor文を回して作る際は、以下のように書くとスッキリします。
// POSTされたデータ配列 $postedAnswers をupsertする場合
$upsertAnswers = [];
foreach ($postedAnswers as $answer) {
$upsertAnswers [] = [
'id' => $answer['id'] ?? null,
'body' => $answer['body'],
'type' => $answer['type']
];
}
// idが一致するレコードがあれば更新、無ければレコード作成する
Answer::upsert($upsertSelections, ['id']);
私ははじめarray_pushを使って配列に要素を追加していっていましたが、先輩にレビューいただいて上記の書き方を知ったのでこれもメモでした!
参考記事