0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravelの lazy() / cursor() にフォーカス:大量ダウンロードを軽くするにはどうすれば

Last updated at Posted at 2025-09-03
->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件ずつ処理するのが確実

頭の中ではできた!
今のタスクが終わったら実践してみよう!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?