バルクインサートの必要性
現在私はLaravel9使用の案件に参画しています。対応しているタスクにて大量のCSVデータをDBに格納する必要がありまして、N+1問題に考慮するために一回のクエリ実行でinsertを実現できるバルクインサートの方法について調査しました。
そもそもバルクインサートとは?
バルクインサートを簡単に説明すると、
「大量のデータを1回の命令でデータベースに入れられる、insert文っぽいやつ」
引用元:「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
とのことです。LaravelやPHPのコードにはfor文やforEach文が使われることがありますが、それらを使用し同じようにinsert処理を書いてしまうとinsert処理が何千・何万回実行され、処理の低速化・メモリの圧迫に繋がるために避けたいところです。
updateOrCreateメソッドの活用
今回の実装ではCSVデータ格納時にエラーが発生し、DBに途中までしかデータが格納されないケースを考慮し、同じタイムスタンプで取り込まれたデータ行が存在した場合にはレコードをupdateする実装の方が安全性が高くinsertとupdateを兼ねているメソッドを使用したかったので、以前使用したことのあるupdateOrCreateメソッドが真っ先に浮かびました。以下が使用例です。
Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99, 'discounted' => 1]
);
- 第一引数には、データベースで検索する際の条件を連想配列で指定する。
- 第二引数には、データが見つからなかった場合に新規挿入するデータ、またはデータが見つかった場合にupdateするデータを連想配列で指定する。
この場合ではDBのdeparture
とdestination
カラムに上記のそれぞれの値があった場合、price
を99・discounted
を1で更新し、なかった場合は新規でdeparture
・destination
・price
・discounted
カラムにそれぞれの値でinsertする処理になります。
ただし、多次元配列を第一引数に指定することはできないようで、forEach文を使って無理やり実装することはできますが複数のデータ行を一度に登録する肝心のバルクインサートとしては使用することができません。そこで見つけたメソッドがupsert
メソッドです。
upsertメソッドの活用
Eloquentの upsert
メソッドでは、対象となるレコードが存在する場合はupdateを、存在しない場合は新規insertするという挙動を実現し、第一引数には二次元の連想配列が入るため、今回の実装条件にぴったりです。
upsert
の使用例を以下に示します。
Flight::upsert([
['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], ['departure', 'destination'], ['price']);
- 第一引数には、挿入または更新する値を連想配列で指定します。
- 第二引数には、関連付けられたテーブル内のレコードを一意に識別するカラムを配列で指定します。(複数指定可)
- 第三引数には、一致するレコードがDBにすでに存在する場合に更新する必要があるカラムを配列で指定します。(省略可)
この場合ではdeparture
とdestination
の値が、第一引数で指定したそれぞれの二次元連想配列の値と同じレコードがDBにあった場合、price
の値のみ更新するといった処理となります。第三引数を省略すると、第一引数のデータが全てupdateで反映されます。
なんと、upsert
でのinsertの場合はcreated_at
とupdated_at
のタイムスタンプも自動的に設定され、updateの場合はupdated_at
のタイムスタンプのみ更新が入ります。LaravelerでありEloquenterにはたまらないメソッドとなっています。
注意点
ただし、検索するカラム(第二引数に指定するもの)はmigrationファイルで適切にprimaryキーまたはuniqueキーを設定しないと全てinsertとして処理されてしまいます。(私はデータがuniqueであればいいと勝手に思いmigrationファイルをいじらなかったため少し詰まりました。)
upsert
はLaravel8から実装されたようですが、少しでもupdateされる可能性があるバルクインサート文を記述したい場合は、今後upsert
文を使ってもいいかもしれません。