->get();
ついにメモリオーバーになった。
Aテーブルの内容を全て取得。
Bテーブルの内容を全て取得。
AとBをくっつけて作業日順にsortする仕様にしていた。
今考えればそりゃ重いよなって思う。
そんなこんなで対応する方法を模索するのであった。
lazy() と cursor() を一言で
- lazy($chunk = 1000): 結果をチャンク単位で遅延取得しながら1件ずつ処理できる。
- cursor(): DBカーソルで1件ずつ取り出すジェネレータ(最小メモリ)。
⸻
例:CSVをストリーミングで返す
use Illuminate\Support\Facades\DB;
Route::get('/export/csv', function () {
return response()->streamDownload(function () {
$out = fopen('php://output', 'w');
fputcsv($out, ['id','name','work_date']); // ヘッダ
DB::table('works')
->orderBy('work_date')
->lazy(1000)
->each(function ($r) use ($out) {
fputcsv($out, [$r->id, $r->name, $r->work_date]);
});
fclose($out);
}, 'works.csv', ['Content-Type' => 'text/csv']);
});
ポイント
- lazy(1000):1000件ずつDBから取り、その場で1行書く
- 中間配列を作らない → メモリが膨らまない
⸻
例:Excel(XLSX)
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Illuminate\Support\Facades\DB;
Route::get('/export/xlsx', function () {
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$row = 1;
$sheet->fromArray([['id','name','work_date']], null, 'A'.$row++);
foreach (DB::table('works')->orderBy('work_date')->lazy(1000) as $r) {
$sheet->fromArray([[ $r->id, $r->name, $r->work_date ]], null, 'A'.$row++);
}
$writer = new Xlsx($spreadsheet);
$writer->setPreCalculateFormulas(false); // 計算はExcel側に任せて軽量化
return response()->streamDownload(function () use ($writer) {
$writer->save('php://output');
}, 'works.xlsx', ['Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']);
});
ポイント
- fromArray([[...]], null, 'A'.$row++) で1行ずつ
fromArrayの使い方
$sheet->fromArray(
[ [ 'A1の値', 'B1の値', 'C1の値' ] ], // 2次元配列
null, // 空セルの値(通常はnullでOK)
'A1', // 書き込み開始セル
true // 行方向に展開するか(trueで横に展開)
);
⸻
cursor()
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Illuminate\Support\Facades\DB;
Route::get('/export/xlsx-cursor', function () {
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// ヘッダ行を1行目に書く
$row = 1;
$sheet->fromArray([['id','name','work_date']], null, 'A'.$row++, true);
// cursor() で1件ずつDBから取り出して、その場でExcelに書き込む
foreach (DB::table('works')->orderBy('work_date')->cursor() as $record) {
$sheet->fromArray([[
$record->id, // A列
$record->name, // B列
$record->work_date, // C列
]], null, 'A'.$row, true);
$row++;
}
// 書き出し(プリ計算OFFで軽量化)
$writer = new Xlsx($spreadsheet);
$writer->setPreCalculateFormulas(false);
return response()->streamDownload(function () use ($writer) {
$writer->save('php://output');
}, 'works_cursor.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
});
方針
まとめ
- まず AとBのデータをそれぞれ取得する時点でメモリを食いやすい
- Laravelなら リレーションを張ったモデルを lazy() で流し込むやり方をまず試す
- もしそれでも厳しければ、少し時間はかかるけれど cursor() で1件ずつ処理するのが確実
頭の中ではできた!
今のタスクが終わったら実践してみよう!