[Laravel] Eloquent Model用のバルクインサートTraitを作った
こんにちは、ナカエです。今回はEloquentの機能拡張のためにTraitを作ったお話です。
Eloquentとバルクインサート
Eloquent Modelのsave()を利用して大量のレコードをinsertしようとした際、実行時間が長すぎて困ることがたまにあります。
/** @var \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model[] $models */
foreach ($models as $model) {
$model->column = 'fuga';
$model->save();
}
特に、DBアクセスの通信のレイテンシが大きい場合に所要時間の増加が顕著です。 このような時は複数のレコードを1クエリにまとめて保存するバルクインサートを使って通信回数を減らすのが1つの解決法ですね。 Eloquent Modelにはバルクインサート用のメソッドが用意されていないので、ActiveRecordの機能は使わずに\Illuminate\Database\Query\Builder::insert()を使うのが一番手軽な解決になるでしょう。
/** @var \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model[] $models */
// ただの2次元配列に変換する処理
$modelsArray = myConvertToArray($models);
MyEloquentModel::query()->insert($modelsArray);
// もしくはDBファサードを使ったり
// \DB::insert($modelsArray)
Eloquentのモデルイベントを発火させたい
これにてバルクインサートが実現できてめでたしめでたし、となるのですが、foreachでループを回してModel::save()で一件ずつ挿入した場合とはほんの少し挙動が異なります。
タイムスタンプ(created_atとupdated_at)が更新されない点、およびEloquent Modelが永続化時にデフォルトで発火するイベント群(eloquent.saving, eloquent.creatingなど)が発火されない点です。前者はまあマニュアル設定でも大したことはないのですが。
この問題点を解決するために、Eloquent Modelの保存時の各イベントを発火させながらバルクインサートを行うEloquent Model用のTraitを作ったことがありました。その時のソースを元に改良したのがこちらの品です。
n1215/eloquent-bulk-insert - GitHub
使い方
// 1. Eloquent ModelでBulkInsertトレイトをuse
class YouModel extends \Illuminate\Database\Eloquent\Model
{
use \N1215\EloquentBulkSave\BulkInsert;
}
// 2. Eloquent Modelのコレクションを作る (もちろんIlluminate\Database\Eloquent\Collectionでも可)
$models = \Illuminate\Support\Collection::make([
new YourModel($attributes1),
new YourModel($attributes2),
...
]);
// 3. スタティックメソッドをコール
YourModel::bulkInsert($models);
最初はイベントの発火や$attributesの取得をリフレクションで実装していたのですが、よく考えるとEloquent Modelのメソッドがprotectedなので、traitからでもアクセス可能であることに気づきました。万歳。
バルクアップデートも欲しい
バルクアップデートはデータベースドライバごとに実装する必要がありそうなので、MySQLだけ実装して塩漬けにしています。