発生した事象
Laravel Eloquentで大量レコードを取得しようと試みた。
$builder = Post::where('id', '<>' , 1);
$posts = $builder->get();
foreach ($posts as $post) {
// 各行のデータを処理する
}
get()した時、メモリ不足でエラーになった。該当するレコードは1,000,000件以上ある。
Fatal error: Allowed memory size of *** bytes exhausted in ...
実行環境
Laravel 10.x
案1: SQLの実行と結果取得でEloquentを使うのを止めて、cursor()
でデータを取得する
$builder = Post::where('id', '<>' , 1);
$cursor = DB::connection()->cursor($builder->toSql(), $builder->getBindings());
foreach ($cursor as $row) {
// $row は stdClass
$data = get_object_vars($row);
// $data は [colmun_name1 => column_value1, colmun_name2 => column_value2, …] の連想配列になる
// 各行のデータを処理する
}
欠点
- 1行のデータはEloquentのModel(ここではPost)にならない。
- Model に
HasMany
などのrelationが定義されていても、取得できない。代わりに$builder->join()
$builder->select()
などを駆使して関係するデータを取得したり、整理する必要がある。 - つまり、概ねPDOStatementによる開発に近くなる。開発者のスキルと合わなくなったり、開発効率が下がったりする可能性がある。
案2: lazyById()
などでカーソルページネーションする
$builder = Post::where('id', '<>' , 1)->lazyById(1000, $column = 'id');
foreach($posts as $post){
// 各行のデータを処理する
}
上記を実行すると、複数のSELECT文を実行してデータを取得する。
SELECT * FROM posts ORDER BY id ASC LIMIT 1000
SELECT * FROM posts WHERE id > 1000 ORDER BY id ASC LIMIT 1000
SELECT * FROM posts WHERE id > 2000 ORDER BY id ASC LIMIT 1000
・・・
lazyById()
その他の選択肢について、下記リンクが詳しい。
Laravel の取得系メソッド cursor() や chunk() を徹底比較する
https://qiita.com/nekohan/items/eba0816fe8c21e3cc077
欠点
- 同一時点のデータを取得する必要がある場合、利用できない。
- idカラムの代わりに、複合キー(2カラム以上で構成された一意キー)を指定してページネーションする方法が無い。
-
lazyById()
に限らず、EloquentのModelを使うと良く起きる話。
-