🧭 目次
-
はじめに:なぜバッチ処理が「最強のチーム貢献」なのか?
- バッチ処理は「夜中に働く妖精さん」
- 現場(WEBサービス)でなぜ必要なのか?
-
Laravelバッチ処理の「3つの道具」
-
- Artisanコマンド(基本の実行ファイル)
-
- ジョブ(キュー)(重い処理・非同期)
-
- タスクスケジュール(自動実行)
- 現場での使い分け
-
-
ステップ・バイ・ステップ! 最初のコマンドを作ってみよう
-
make:commandで設計図を作る -
handle()に処理を書く(ログ出力) -
php artisanで実行する
-
-
現場で即役立つ! バッチ処理「10の実践パターン」
- パターン1:日次レポート生成
- パターン2:データクレンジング
- パターン3:メール一括送信
- パターン4:CSV/エクセル出力
- パターン5:外部API連携
- パターン6:画像リサイズ
- パターン7:データ同期処理
- パターン8:キャッシュウォームアップ
- パターン9:定期バックアップ
- パターン10:不正データ検知
-
【超重要】現場でコケないための「3つの黄金ルール」
- ルール1:ログを「命綱」として仕込む(真っ暗闇で作業しない)
- ルール2:大量データは「小分け(chunk)」にする(サーバーダウンさせない)
- ルール3:再実行を前提に「ステータス管理」する(データ重複させない)
-
さらに評価される!「+α」の現場テクニック7選
- テクニック1:ドライランモード (
--dry-run) - テクニック2:プログレスバー(進捗の可視化)
- テクニック3:エラーハンドリング(
try-catch,Command::FAILURE) - テクニック4:重複実行の防止(
withoutOverlapping) - テクニック5:APIリトライ処理
- テクニック6:実行時間の計測
- テクニック7:専用ログチャンネル
- テクニック1:ドライランモード (
- まとめ:バッチ処理は「縁の下のヒーロー」
1. はじめに:バッチ処理とは?
(※バッチ処理の概要を知っている方は読み飛ばしてください)
バッチ処理とは、一言で言うと「複数の処理をひとまとめにして、自動で一気に実行すること」です。
WEBサービスは、ユーザーがアクセスしている時(表)だけ動いているわけではありません。裏側では、たくさんの「地味だけど重要な仕事」が動いています。
- 毎日深夜に、昨日の売上を集計する
- 1時間ごとに、新着ニュースを外部から取ってくる
- 夜間に、溜まったメールを一斉送信する
もし、これらを表側(ユーザーがアクセスした時)にやったらどうなるでしょう?
「集計が終わるまで30秒待ってください」→ ユーザーは離脱します。
そこで**バッチ処理(Batch Processing)**の出番です。
😆 バッチ処理は「夜中に働く妖精さん」
中学生の方にも分かるように例えます。
あなたは毎日、日記、計算ドリル、漢字練習の3つの宿題を、学校から帰ってすぐにやる必要がありました。(これがWEBリクエスト=表側の処理)
でも、友達と遊びたいし、宿題も時間がかかって大変です。
そこで、あなたは「夜中に働く妖精さん」にお願いすることにしました。
「私が寝ている間に、この3つの宿題を全部やっといて!」(これがバッチ処理=裏側の処理)
朝、目が覚めると、宿題は完璧に終わっています。
あなたは日中、友達と目一杯遊べますし、宿題のクオリティもバッチリです。
🤔 現場(WEBサービス)でなぜ必要なのか?
この「妖精さん」がバッチ処理です。現場では、この妖精さんに色々な仕事をお願いしています。
-
集計・レポート:
「毎朝9時に、昨日の『どの商品が』『何個売れたか』を集計して、店長にメールで送っといて!」 -
データ連携:
「1時間に1回、天気予報サイトから最新の天気データを取ってきて、ウチのデータベースに入れといて!」 -
通知・メルマガ:
「夜中の2時に、キャンペーン情報を登録ユーザー全員にメールで送っといて!」 -
データお掃除(クリーンアップ):
「毎週日曜の朝4時に、退会したユーザーの個人情報を削除しといて!」
これらを導入することで、
- 🙆♂️ ユーザー体験の向上(お客様を待たせない)
- 🙆♂️ サーバー負荷の分散(一番混む時間帯を避けられる)
- 🙆♂️ 業務の自動化(手作業をなくし、ミスを減らせる)
が実現できます。
地味な作業に見えて、実は超実践的で組織貢献度MAXのスキルなのです。
2. Laravelバッチ処理の「3つの道具」
Laravelで「妖精さん」に仕事を頼むには、主に3つの「道具」を使います。
1. Artisanコマンド(基本の実行ファイル)
「妖精さんが読む、仕事の**『指示書』**」そのものです。
php artisan make:command DailyReport のように作ります。
「これを実行したら、この処理をする」という内容を handle() メソッドの中に書きます。この記事のメインはこれです。
2. ジョブ(キュー)(重い処理・非同期)
「妖精さんへの**『お願いメモ』**」です。
php artisan make:job SendCampaignEmail のように作ります。
メール送信のように「時間がかかる処理」や「失敗しても後でやり直したい処理」を、このメモ(ジョブ)にして、「順番待ちの箱(キュー)」に入れておきます。
こうすることで、ユーザーを待たせずに「受付だけ」済ませて、裏側で妖精さんが順番に処理してくれます。
3. タスクスケジュール(自動実行)
「妖精さんを**『叩き起こす目覚まし時計』**」です。
app/Console/Kernel.php というファイルに設定を書きます。
「Artisanコマンド(指示書)の report:daily を、毎朝5時に実行して!」とセットできます。
現場での使い分け
- 今すぐ実行したい: Artisanコマンドを直接実行
- 定期的に実行したい: Artisanコマンド + タスクスケジュール(これが王道!)
- 時間のかかる処理(ユーザーを待たせたくない): ジョブ(キュー)
3. ステップ・バイ・ステップ! 最初のコマンドを作ってみよう
お待たせしました!
Laravelで、この「指示書(Artisanコマンド)」を実際に作ってみましょう。
お題:毎日、チームに「今日も一日お疲れ様!」とログを出すバッチ
1. make:command で設計図を作る
ターミナル(黒い画面)で、あなたのLaravelプロジェクトのディレクトリに移動して、以下のコマンドを叩いてください。
php artisan make:command SendDailyGreeting
これを実行すると、app/Console/Commands/SendDailyGreeting.php というファイルが自動で作成されます。
中身を見て、以下の3箇所を修正します。
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log; // ★Logファサードを使う宣言
class SendDailyGreeting extends Command
{
/**
* コマンド名 (シグネチャ)
* ターミナルで叩く名前
*/
protected $signature = 'batch:send-greeting'; // ★分かりやすく変更
/**
* コマンドの説明
* php artisan list で一覧表示される
*/
protected $description = '毎日チームにお疲れ様のログを送るバッチ'; // ★必ず分かりやすく書く!
/**
* コマンド実行のメイン処理
* ここに「妖精さんの仕事」を書く
*/
public function handle(): void
{
// ★ここがメインの処理
Log::info('===== [start] batch:send-greeting =====');
Log::info('今日も一日お疲れ様でした! 良い一日を!');
// ターミナルにもメッセージを出す(任意)
$this->info('お疲れ様メッセージをログに出力しました。');
Log::info('===== [end] batch:send-greeting =====');
}
}
現場目線ポイント:
$description(説明)をサボってはいけません。
あなたが作った後、チームの他のメンバーが「この batch:send-greeting って何だっけ?」とならないよう、必ず分かりやすい説明を書きましょう。これが組織貢献の第一歩です。
2. php artisan で実行する
作ったコマンドを実行してみましょう。
ターミナルで、さっき決めた $signature の名前を打ちます。
php artisan batch:send-greeting
実行すると、ターミナルにこう表示されるはずです。
お疲れ様メッセージをログに出力しました。
そして、storage/logs/laravel.log ファイルを開いてみてください。
一番下に、こう書かれていますか?
[2025-11-10 19:30:00] local.INFO: ===== [start] batch:send-greeting =====
[2025-11-10 19:30:00] local.INFO: 今日も一日お疲れ様でした! 良い一日を!
[2025-11-10 19:30:00] local.INFO: ===== [end] batch:send-greeting =====
おめでとうございます!
これがあなたの作った「最初のバッチ処理」です!
4. 現場で即役立つ! バッチ処理「10の実践パターン」
基本がわかったところで、現場で「これ作って!」と言われる頻出パターン10個を、コード例と共に見ていきましょう。
パターン1:日次レポート生成
売上や登録者数などを毎日集計してSlack通知やログに出力する、最も基本的なバッチです。
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class DailyReport extends Command
{
// --date= オプションで日付指定も可能にする
protected $signature = 'report:daily {--date=}';
protected $description = '日次レポートを生成';
public function handle()
{
$this->info('レポート生成開始');
try {
// オプションで日付が指定されなければ「昨日」を対象にする
$date = $this->option('date')
? Carbon::parse($this->option('date'))
: Carbon::yesterday();
$totalSales = Order::whereDate('created_at', $date)->sum('amount');
$newUsers = User::whereDate('created_at', $date)->count();
$report = [
'日付' => $date->format('Y-m-d'),
'売上' => number_format($totalSales) . '円',
'新規登録' => $newUsers . '人',
];
// ログに出力
Log::info('日次レポート', $report);
// ターミナルにテーブル形式で表示
$this->table(['項目', '値'], collect($report)->map(fn($v, $k) => [$k, $v]));
$this->info('レポート生成完了');
return Command::SUCCESS; // 成功を返す
} catch (\Exception $e) {
Log::error('レポート生成失敗: ' . $e->getMessage());
$this->error('エラーが発生しました');
return Command::FAILURE; // 失敗を返す
}
}
}
スケジュール設定 (app/Console/Kernel.php)
protected function schedule(Schedule $schedule)
{
// 毎日 AM 0:30 に実行
$schedule->command('report:daily')->dailyAt('00:30');
}
パターン2:データクレンジング
古いデータや不要なデータを定期的に削除します。--dry-run オプション(お試しモード)の実装は必須です。
<?php
// ...
use App\Models\LoginHistory;
use Carbon\Carbon;
class CleanupData extends Command
{
protected $signature = 'data:cleanup {--dry-run}';
protected $description = '古いデータを削除(90日以上前のログイン履歴)';
public function handle()
{
$isDryRun = $this->option('dry-run');
if ($isDryRun) {
$this->warn('【ドライラン】実際には削除しません');
}
$targetDate = Carbon::now()->subDays(90);
$query = LoginHistory::where('created_at', '<', $targetDate);
$count = $query->count();
if ($count === 0) {
$this->info('削除対象データはありませんでした。');
return Command::SUCCESS;
}
$this->info("{$count}件の古いログイン履歴を削除" . ($isDryRun ? '予定' : 'します'));
if (!$isDryRun) {
// ここで黄金ルール2「chunk」の応用
// 一気にdeleteするとDBに負荷がかかるため、chunkDeleteを使う
$query->chunkById(1000, function ($histories) {
LoginHistory::whereIn('id', $histories->pluck('id'))->delete();
$this->info('1000件処理...');
sleep(1); // サーバー負荷軽減のため1秒待機
});
}
$this->info('クリーンアップ完了');
return Command::SUCCESS;
}
}
パターン3:メール一括送信
キャンペーンメールなどを一括送信します。**黄金ルール2「chunk」と「プログレスバー」**が活躍します。
<?php
// ...
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use App\Mail\CampaignMail;
class SendCampaignEmail extends Command
{
protected $signature = 'email:campaign';
protected $description = 'キャンペーンメール送信';
public function handle()
{
$this->info("メール送信を開始");
$query = User::where('email_verified_at', '!=', null)
->where('unsubscribed', false);
$totalUsers = $query->count();
if ($totalUsers === 0) {
$this->info('対象ユーザーがいません。');
return Command::SUCCESS;
}
// プログレスバー(進捗表示)を初期化
$bar = $this->output->createProgressBar($totalUsers);
$bar->start();
$successCount = 0;
$failCount = 0;
// 100人ずつ処理 (黄金ルール2)
$query->chunk(100, function ($users) use ($bar, &$successCount, &$failCount) {
foreach ($users as $user) {
try {
Mail::to($user->email)->send(new CampaignMail($user));
$successCount++;
} catch (\Exception $e) {
Log::error("メール送信失敗: {$user->email} - {$e->getMessage()}");
$failCount++;
}
$bar->advance(); // バーを1つ進める
}
usleep(500000); // 0.5秒待機(メールサーバー負荷軽減)
});
$bar->finish();
$this->newLine(2); // 2行改行
$this->info("送信成功: {$successCount}件");
if ($failCount > 0) {
$this->warn("送信失敗: {$failCount}件");
}
return Command::SUCCESS;
}
}
パターン4:CSV/エクセル出力
管理者向けにデータをエクスポートします。**黄金ルール2「chunk」**でメモリを節約します。
<?php
// ...
use App\Models\User;
class ExportUsers extends Command
{
protected $signature = 'export:users';
protected $description = 'ユーザーデータをCSVエクスポート';
public function handle()
{
$filename = 'users_' . date('Ymd_His') . '.csv';
$path = storage_path("app/exports/{$filename}");
$this->info("エクスポート開始: {$path}");
$file = fopen($path, 'w');
// BOM付与(Excelでの文字化け対策)
fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));
// ヘッダー
fputcsv($file, ['ID', '名前', 'メール', '登録日']);
// データ(チャンク処理でメモリ節約)
User::chunk(1000, function ($users) use ($file) {
foreach ($users as $user) {
fputcsv($file, [
$user->id,
$user->name,
$user->email,
$user->created_at->format('Y-m-d'),
]);
}
$this->info('1000件書き込み...');
});
fclose($file);
$this->info("エクスポート完了: {$filename}");
return Command::SUCCESS;
}
}
パターン5:外部API連携
外部API(天気予報、決済情報など)とデータを同期します。**「リトライ処理」**が重要です。
<?php
// ...
use App\Models\Payment;
use Illuminate\Support\Facades\Http;
class SyncPaymentStatus extends Command
{
protected $signature = 'payment:sync';
protected $description = '決済ステータス同期';
public function handle()
{
// 7日以内の「処理中」の決済を取得
$payments = Payment::where('status', 'pending')
->where('created_at', '>', now()->subDays(7))
->get();
$this->info("{$payments->count()}件の決済を確認");
$updatedCount = 0;
foreach ($payments as $payment) {
try {
// 1秒待機で3回リトライ (テクニック5)
$response = Http::timeout(10)
->retry(3, 1000)
->get("https://api.payment.example.com/status/{$payment->transaction_id}");
if ($response->successful()) {
$status = $response->json()['status'];
if ($payment->status !== $status) {
// 黄金ルール3「ステータス管理」の応用
$payment->status = $status;
$payment->save();
$updatedCount++;
}
}
} catch (\Exception $e) {
Log::error("決済同期失敗: {$payment->id} - {$e->getMessage()}");
}
}
$this->info("{$updatedCount}件更新しました");
return Command::SUCCESS;
}
}
パターン6〜10:その他、現場で頻出のパターン
コードは長くなるため割愛しますが、これらの処理もArtisanコマンドの応用で実現できます。
-
パターン6:画像リサイズ
- ユーザーがアップロードした元画像を、深夜バッチで「サムネイル用」「中サイズ用」などに一括リサイズする。
-
パターン7:データ同期処理
- 外部の基幹システムDBと、WebサービスのDB(顧客マスタなど)を定期的に同期する。
-
パターン8:キャッシュウォームアップ
- アクセスが集中する前に、重い集計結果(ランキングなど)を計算してキャッシュに保存しておく。
-
パターン9:定期バックアップ
-
mysqldumpなどのコマンドを実行し、データベースのバックアップを定期的に取得する。
-
-
パターン10:不正データ検知
- 異常な高額注文、短時間の大量アクセス、ログイン失敗の多発などを検知してアラートを出す。
5. 【超重要】現場でコケないための「3つの黄金ルール」
バッチ処理は強力ですが、作り方を間違えると「諸刃の剣」になります。
サービス全体を巻き込む大事故(サーバーダウン、データ破壊)につながる可能性もあるのです。
現場で「コイツは分かってるな」と評価されるために、この3つの黄金ルールだけは絶対に守ってください。
ルール1:ログを「命綱」として仕込む(真っ暗闇で作業しない)
やらかし例: handle() にいきなり処理だけ書き、ログも try-catch もない。
結果: 本番環境で、ある日突然バッチが動かなくなった。ログが何もないため、原因究明に3日かかった。
バッチ処理は「誰も見ていないところ」で動きます。
もしエラーが起きても、ログがなければ「なぜ動かないのか?」誰にも分かりません。
「ログは、未来の自分とチームメイトを救う『タイムカプセル』である」
最低限仕込むべきログ:
-
処理の開始と終了 (
Log::info('===== [start] ...'),Log::info('===== [end] ...')) -
エラー発生時 (
catch (\Exception $e)でLog::error($e)) -
処理件数 (
Log::info("対象件数: {$count}件"))
public function handle()
{
Log::info('===== [start] batch:sample =====');
try {
// メインの処理
$count = User::count();
Log::info("処理対象: {$count}件");
} catch (\Exception $e) {
// ★万が一、エラーが起きたら
Log::error('バッチ処理中に予期せぬエラーが発生しました。');
Log::error($e); // ★例外オブジェクト全体を渡すと詳細が出る
$this->error('エラーが発生しました! 詳細はログを確認してください。');
return Command::FAILURE; // ★異常終了を伝える
} finally {
// ★エラーがあってもなくても、必ず最後に実行
Log::info('===== [end] batch:sample =====');
}
return Command::SUCCESS; // ★正常終了を伝える
}
ルール2:大量データは「小分け(chunk)」にする(サーバーダウンさせない)
やらかし例: 「全ユーザーに処理」のため User::all() を実行し、100万人のユーザー情報を一気にメモリに読み込んだ。
結果: サーバーのメモリを使い果たし、バッチが強制終了。他のWebアクセスまで巻き込んでサーバーがダウンした。
大量データを扱うのがバッチ処理の役目ですが、一気に処理してはいけません。
Laravelには、大量データを「少しずつ」処理するための素晴らしい機能があります。
NG例(メモリ爆発):
$users = User::where('status', 'active')->get(); // 100万人いたら即死
foreach ($users as $user) {
// 処理
}
OK例(chunkで100人ずつ処理):
// 100人ずつDBから取得し、メモリに優しく処理する
User::where('status', 'active')->chunk(100, function ($users) {
foreach ($users as $user) {
// ここでの $users には最大100人分のデータが入っている
Log::info("Processing user ID: {$user->id}");
}
});
chunk(100, ...) と書くだけで、Laravelは100人分のデータを処理したら、一旦メモリを解放し、次の100人分のデータを取ってきてくれます。
ルール3:再実行を前提に「ステータス管理」する(データ重複させない)
やらかし例: 「注文データにポイントを付与するバッチ」が、1000件中500件処理したところでエラー停止した。
結果: 慌ててバッチを再実行。すでにポイント付与済みの500件にもう一度ポイントが付与され、データが重複(二重付与)した。
バッチ処理は、**「何回実行しても、同じ結果になる」ように作るのが理想です(これを冪等性(べきとうせい)**と言います)。
対策は**「ステータス(状態)管理」**です。
NG例(再実行で死ぬ):
$orders = Order::where('status', 'paid')->get();
foreach ($orders as $order) {
Point::create(...); // ポイント付与
}
OK例(ステータス管理で二重処理を防ぐ):
// 「支払い済み(paid)」で、かつ「まだポイント処理してない(unprocessed)」注文だけ対象にする
$orders = Order::where('status', 'paid')
->where('point_status', 'unprocessed') // ★専用カラムで状態管理
->get();
foreach ($orders as $order) {
Point::create(...); // ポイント付与
// ★重要:処理が成功したら、ステータスを「処理済み」に変える
$order->point_status = 'processed';
$order->save();
}
こうすれば、途中でコケて再実行しても、where('point_status', 'unprocessed') の条件によって、処理済みの注文は対象外になります。
6. さらに評価される!「+α」の現場テクニック7選
3つの黄金ルールを守った上で、これらのテクニックも使えると、さらにチームから信頼されます。
テクニック1:ドライランモード (--dry-run)
「パターン2:データクレンジング」で使ったテクニックです。
「削除します(フリだけする)」お試しモードは、本番データを壊さないための超重要テクニックです。
protected $signature = 'command:name {--dry-run}';
public function handle()
{
if ($this->option('dry-run')) {
$this->warn('【確認モード】実際には実行しません');
} else {
// 実際に実行
}
}
テクニック2:プログレスバー(進捗の可視化)
「パターン3:メール一括送信」で使ったテクニックです。
処理に何分もかかるバッチは、「今どこまで進んでるの?」と不安になります。プログレスバーを出すだけで、安心感が違います。
$bar = $this->output->createProgressBar($total);
$bar->start();
// (ループの中)
$bar->advance();
// (ループの後)
$bar->finish();
テクニック3:エラーハンドリング(try-catch, Command::FAILURE)
黄金ルール1でも触れましたが、try-catch は必須です。
そして、handle() メソッドの戻り値で return Command::SUCCESS;(正常)か return Command::FAILURE;(異常)を返しましょう。
こうすることで、バッチの実行結果を外部(タスクスケジューラなど)が検知しやすくなります。
テクニック4:重複実行の防止(withoutOverlapping)
処理に10分かかるバッチを、5分おきにスケジュールしたらどうなるでしょう?
処理が終わる前に次の処理が始まってしまい、大混乱です。
app/Console/Kernel.php で withoutOverlapping() を付けておけば、Laravelが「前の処理が終わるまで、次のは待っててね」と制御してくれます。
$schedule->command('payment:sync')
->everyFiveMinutes()
->withoutOverlapping(); // ★これ!
テクニック5:APIリトライ処理
「パターン5:外部API連携」で使ったテクニックです。
外部APIは一瞬のネットワークエラーで失敗することがあります。
LaravelのHttpクライアントなら retry() をチェーンするだけで、自動で再試行してくれます。
$response = Http::retry(3, 1000) // 1秒待機で3回リトライ
->get($url);
テクニック6:実行時間の計測
処理の開始と終了で時間を計測し、ログに残すと「パフォーマンス改善」に役立ちます。
public function handle()
{
$startTime = microtime(true);
Log::info('===== [start] ... =====');
// ... メイン処理 ...
$executionTime = round(microtime(true) - $startTime, 2);
Log::info("実行時間: {$executionTime}秒");
Log::info('===== [end] ... =====');
}
テクニック7:専用ログチャンネル
config/logging.php に設定を追加すれば、バッチ処理のログだけを storage/logs/batch.log のように別ファイルに分離できます。
laravel.log が他のログで埋もれなくなり、調査が爆速になります。
'channels' => [
// ...
'batch' => [
'driver' => 'daily', // 毎日ファイルを分ける
'path' => storage_path('logs/batch.log'),
'level' => 'info',
'days' => 14, // 14日分保持
],
],
Log::channel('batch')->info('バッチ処理専用ログ');
7. まとめ:バッチ処理は「縁の下のヒーロー」
お疲れ様でした!
この記事では、LaravelのArtisanコマンドを使ったバッチ処理の基本から、現場で役立つ10の実践パターン、そして「コケないため」の3つの黄金ルールまでを解説しました。
バッチ処理は、WEBサービスという「舞台」を裏側で支える、**「縁の下のヒーロー」**です。
派手なフロントエンドの技術とは違い、目立つことは少ないかもしれません。
しかし、この「裏側」がしっかりしているからこそ、サービスは安定し、会社は成長できます。
特にリソースが限られるスタートアップやベンチャーでは、
「いかに手作業を自動化し(効率化)」
「いかにサーバーに優しく(安定稼働)」
「いかにエラーを素早く察知し(信頼性)」
て仕組みを作れるかが、エンジニアの腕の見せ所です。
バッチ処理を使いこなすことは、「コードが書ける」を超えた、「事業課題を解決できる」エンジニアであることの証明であり、最強のチーム・組織貢献につながります。
今日学んだ10のパターンと3つの黄金ルールを、ぜひあなたのプロジェクトで試してみてください!