Laravel業務システムでExcel一括インポートを実装したら、大容量ファイルでメモリ不足エラーが発生していませんか?実案件での失敗体験を基に、効果的な大容量処理の実装術をお伝えします。
こんな悩みありませんか?
Laravelで業務システムを構築し、Excel一括インポート機能を実装したものの、数万行の大容量ファイルを処理しようとすると「Fatal error: Allowed memory size exhausted」というメモリ不足エラーが発生してしまう。開発時は100行程度のテストデータでうまく動作していたのに、実際の運用データでは全く使い物にならない...
このような課題に直面している開発者の方は多いのではないでしょうか。私たちFivenine Designでも、あるクライアントの在庫管理システムで同様の問題に遭遇しました。月次の商品データ更新で5万行のExcelファイルをインポートする必要があったのですが、従来の実装では処理が途中で止まってしまい、データの整合性に問題が生じていました。
なぜメモリ不足が発生するのか?
一般的な実装の問題点
多くの開発者が最初に実装するExcelインポート処理は、以下のようなものです:
use PhpOffice\PhpSpreadsheet\IOFactory;
public function import(Request $request)
{
$file = $request->file('excel');
$spreadsheet = IOFactory::load($file->getPathname());
$worksheet = $spreadsheet->getActiveSheet();
$rows = $worksheet->toArray();
foreach ($rows as $row) {
Product::create([
'name' => $row[0],
'price' => $row[1],
'stock' => $row[2],
]);
}
}
この実装の問題点は、ファイル全体をメモリに展開してしまうことです。5万行のExcelファイルの場合、全データをメモリ上に保持しようとすると、PHPのデフォルトメモリ制限(128MB〜512MB)を簡単に超えてしまいます。
メモリ不足の主な原因
- 全データの一括読み込み: Excelファイル全体をメモリに展開
- オブジェクトの蓄積: Eloquentモデルインスタンスがメモリに残存
- トランザクション処理: 大量のデータを1つのトランザクションで処理
- ガベージコレクションの遅延: 不要なオブジェクトの解放が追いつかない
大容量処理の実装術
私たちが実案件で成功した解決策をご紹介します。この方法により、5万行のExcelファイルを安定して処理できるようになり、クライアントの業務効率が大幅に改善されました。
1. チャンク処理とストリーミング読み込み
LaravelのExcelパッケージ(Laravel Excel)を使用し、チャンク単位での処理を実装します:
use Maatwebsite\Excel\Facades\Excel;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithBatchInserts;
class ProductImport implements ToModel, WithChunkReading, WithBatchInserts
{
public function model(array $row)
{
return new Product([
'name' => $row[0],
'price' => $row[1],
'stock' => $row[2],
]);
}
public function chunkSize(): int
{
return 1000; // 1000行ずつ処理
}
public function batchSize(): int
{
return 500; // 500件ずつバッチ挿入
}
}
2. キュー処理による非同期実行
大容量ファイルの処理は時間がかかるため、キューを使用して非同期で実行します:
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessExcelImport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $filePath;
protected $userId;
public function __construct($filePath, $userId)
{
$this->filePath = $filePath;
$this->userId = $userId;
}
public function handle()
{
try {
Excel::import(new ProductImport(), $this->filePath);
// 処理完了通知
$user = User::find($this->userId);
$user->notify(new ImportCompletedNotification());
} catch (Exception $e) {
Log::error('Excel import failed: ' . $e->getMessage());
throw $e;
} finally {
// 一時ファイルの削除
if (file_exists($this->filePath)) {
unlink($this->filePath);
}
}
}
}
3. メモリ効率的なコントローラー実装
public function import(Request $request)
{
$request->validate([
'excel_file' => 'required|file|mimes:xlsx,xls|max:10240', // 10MB制限
]);
// 一時保存
$file = $request->file('excel_file');
$filename = 'imports/' . uniqid() . '.' . $file->getClientOriginalExtension();
$path = Storage::disk('local')->putFileAs('', $file, $filename);
$fullPath = Storage::disk('local')->path($path);
// キューに投入
ProcessExcelImport::dispatch($fullPath, auth()->id());
return response()->json([
'message' => 'インポート処理を開始しました。完了時に通知いたします。',
'status' => 'processing'
]);
}
4. 進捗表示とエラーハンドリング
use Illuminate\Support\Facades\Cache;
class ProductImport implements ToModel, WithChunkReading, WithBatchInserts, WithProgressBar
{
private $processedRows = 0;
private $totalRows;
private $importId;
public function __construct($importId)
{
$this->importId = $importId;
}
public function model(array $row)
{
$this->processedRows++;
// 進捗をキャッシュに保存
if ($this->processedRows % 100 === 0) {
$progress = ($this->processedRows / $this->totalRows) * 100;
Cache::put("import_progress_{$this->importId}", $progress, 300);
}
return new Product([
'name' => $row[0],
'price' => $row[1],
'stock' => $row[2],
]);
}
}
よくある失敗パターンと対処法
実案件での経験から、よく遭遇する失敗パターンをご紹介します。
失敗パターン1: チャンクサイズが適切でない
症状: チャンクサイズを小さくしすぎて処理が遅い、または大きすぎてメモリエラーが発生
対処法: サーバー環境に応じて適切なチャンクサイズを設定
public function chunkSize(): int
{
$memoryLimit = ini_get('memory_limit');
$memoryBytes = $this->convertToBytes($memoryLimit);
// 利用可能メモリの1/4程度を目安にチャンクサイズを決定
if ($memoryBytes < 268435456) { // 256MB未満
return 500;
} elseif ($memoryBytes < 536870912) { // 512MB未満
return 1000;
} else {
return 2000;
}
}
失敗パターン2: データ検証を怠る
症状: 不正なデータが混入してインポート処理が途中で止まる
対処法: 事前検証とスキップ機能の実装
use Maatwebsite\Excel\Concerns\WithValidation;
use Maatwebsite\Excel\Concerns\SkipsOnFailure;
use Maatwebsite\Excel\Validators\Failure;
class ProductImport implements ToModel, WithValidation, SkipsOnFailure
{
public function rules(): array
{
return [
'0' => 'required|string|max:255', // 商品名
'1' => 'required|numeric|min:0', // 価格
'2' => 'required|integer|min:0', // 在庫
];
}
public function onFailure(Failure ...$failures)
{
foreach ($failures as $failure) {
Log::warning('Import validation failed', [
'row' => $failure->row(),
'attribute' => $failure->attribute(),
'errors' => $failure->errors()
]);
}
}
}
失敗パターン3: タイムアウト対策を忘れる
症状: Webサーバーのタイムアウトで処理が中断される
対処法: 適切なタイムアウト設定とキュー処理
// config/queue.php
'connections' => [
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 7200, // 2時間
],
],
実装による効果と改善結果
私たちのクライアントでは、この実装により以下の改善が実現されました:
- 処理時間: 5万行のインポートが従来の45分から12分に短縮
- エラー率: メモリ不足エラーが100%解消
- ユーザー体験: 非同期処理により、処理中も他の業務が可能
- 運用コスト: 手動でのデータ入力作業が完全に自動化
この技術、御社の業務にも活かせます
業務システム開発
Laravelを使った業務システムの構築・改修を20年以上の経験でサポートします
※ 通常1営業日以内にご返信します
まとめと次のステップ
Laravelでの大容量Excelインポート処理は、適切な実装により確実に実現できます。重要なのは、メモリ効率を考慮したチャンク処理、非同期キュー、そして適切なエラーハンドリングです。
実装を成功させるためには、以下の点を段階的に進めることをお勧めします:
大容量データ処理は、業務システムの価値を大きく左右する重要な機能です。適切に実装されれば、ユーザーの業務効率を劇的に改善し、システムの信頼性も向上します。
神奈川でWeb制作をお探しの方、Laravel業務システムの開発や既存システムの改善をご検討の方は、ぜひFivenine Designまでお気軽にご相談ください。20年以上の実績を活かし、お客様のビジネス課題を技術で解決いたします。