🌊

【ハンズオン】Laravel 13 ループで1件ずつ?もう古い!大量ジョブを一括ディスパッチする

に公開

この記事を読み終えると何ができるか

  • Bus::bulk() を使って複数のジョブを一括でキューにディスパッチできるようになる
  • 従来のループによる個別ディスパッチとの違いと使い分けが理解できる

前提条件・動作環境

この記事では以下の環境を想定しています。

項目 バージョン・内容
OS Windows(WSL2)
Docker インストール済み
PHP 8.4以上(コンテナ内)
Laravel 13.13.0以上

全体の流れ

この記事では次のステップで進めます。

  1. プロジェクトの作成(Laravelスターターキット + Docker/Sail環境の構築)
  2. Jobクラスの作成
  3. Controllerの作成(従来方法 / Bus::bulk() 方法)
  4. Routeの設定
  5. Viewの作成(2つのボタンを配置)
  6. 動作確認

ステップ1: プロジェクトの作成

目的: Laravelの公式スターターキットを使ってプロジェクトを作成し、Docker(Sail)で開発環境を立ち上げる。

1-1. Laravel インストーラーでプロジェクトを作成する

ターミナルを開き、プロジェクトを作成したいディレクトリに移動してから次のコマンドを実行します。

laravel new bus-bulk-demo

コマンドを実行するといくつか質問が表示されます。この記事では次のように選択してください。

Which starter kit would you like to install?
> None        ← 今回はスターターキットなしを選択

Which database will your application use?
> SQLite

Would you like to run npm install and npm run build?
> Yes

1-2. プロジェクトディレクトリに移動する

cd bus-bulk-demo

1-3. Laravel Sail(DockerベースのLaravel開発環境)を導入する

Laravel Sail(セイル)は、DockerでLaravel開発環境を簡単に起動できる公式ツールです。composer require で追加します。

composer require laravel/sail --dev

次に、Sailの設定ファイルを発行します。使用するサービスを選ぶ画面が出るので、今回は mysql のみ選択します。

php artisan sail:install
Which services would you like to install? (Comma-separated list of services)
> mysql

1-4. Sailのシェルエイリアスを設定する(任意・推奨)

毎回 ./vendor/bin/sail と打つのは手間なので、エイリアスを設定しておくと便利です。

# ~/.bashrc または ~/.zshrc に追記する
alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'

追記後はターミナルを再起動するか source ~/.zshrc(または source ~/.bashrc)を実行してください。

以降の手順では sail コマンドで統一して説明します。

1-5. phpMyAdminをSailに追加する

目的: ブラウザからデータベースの中身を視覚的に確認できるようにする。

phpMyAdmin(ピーエイチピーマイアドミン)は、ブラウザ上でMySQLのテーブルやレコードを確認・操作できるツールです。jobs テーブルにジョブが積まれる様子を目で見て確認できるので、ハンズオンの理解が深まります。

Sailの設定ファイル docker-compose.yml を開き、services: セクションの末尾に次のブロックを追加します。

compose.yml
services:
    laravel.test:
        # (既存の設定はそのまま)

    mysql:
        # (既存の設定はそのまま)

    # ↓ここから追加
    phpmyadmin:
        image: phpmyadmin/phpmyadmin
        links:
            - mysql:mysql
        ports:
            - 8888:80
        environment:
            PMA_USER: "${DB_USERNAME}"
            PMA_PASSWORD: "${DB_PASSWORD}"
            PMA_HOST: mysql
        networks:
            - sail
    # ↑ここまで追加

1-6. Dockerコンテナを起動する

sail up -d

-d オプションでバックグラウンド起動になります。初回はDockerイメージのダウンロードが走るため、数分かかります。

起動が完了したら、コンテナが正常に動いているか確認しましょう。

sail ps

laravel.testmysqlphpmyadmin の3つのコンテナが Up 状態になっていればOKです。

1-7. phpMyAdminにログインする

ブラウザで http://localhost:8888 を開くと、phpMyAdminのログイン画面が表示されます。次の情報でログインしてください。

項目 入力値
サーバー mysql
ユーザー名 sail
パスワード password

1-8. .env のDB設定を確認する

Sailを使う場合、.env のDB接続情報はSailが自動で設定してくれます。念のため確認しておきましょう。

.env
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=sail
DB_PASSWORD=password

DB_HOST=mysql はDockerのサービス名です。127.0.0.1 ではないことに注意してください。

1-9. 初期マイグレーションを実行する

Laravelが標準で持っているテーブル(userssessionsjobs など)を作成します。

sail artisan migrate

次のように出力されれば成功です。

   INFO  Running migrations.

  2014_10_12_000000_create_users_table ................... 28ms DONE
  2014_10_12_100000_create_password_reset_tokens_table ... 12ms DONE
  (以下略)

ブラウザで http://localhost を開き、Laravelのトップ画面が表示されれば環境構築は完了です。

phpMyAdminで laravel データベースを選択すると、jobs テーブルが作成されていることも確認できます。

ステップ2: Jobクラスの作成

目的: キューに積まれたときに実行される「処理の単位」を定義する。

今回は複数ユーザーを処理する想定のシンプルなJobを作成します。

sail artisan make:job ProcessUser

生成されたファイルを次のように編集します。

app/Jobs/ProcessUser.php
<?php

namespace App\Jobs;

use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;

class ProcessUser implements ShouldQueue
{
    use Queueable;

    /**
     * ジョブに渡すUserモデルのインスタンス
     */
    public function __construct(
        public User $user
    ) {}

    /**
     * キューワーカーがジョブを処理するときに呼ばれるメソッド
     */
    public function handle(): void
    {
        // 実際の処理をここに書く(今回は確認用にログ出力のみ)
        Log::info("ProcessUser job executed for user ID: {$this->user->id}");
    }
}

ステップ3: Controllerの作成

目的: 従来のループ方式と Bus::bulk() 方式を、それぞれ独立したアクションとして実装して比較できるようにする。

sail artisan make:controller JobDispatchController

生成されたファイルを次のように編集します。

app/Http/Controllers/JobDispatchController.php
<?php

namespace App\Http\Controllers;

use App\Jobs\ProcessUser;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Bus;

class JobDispatchController extends Controller
{
    /**
     * 【従来方法】ループを使ってジョブを1件ずつディスパッチする
     */
    public function dispatchLegacy(): RedirectResponse
    {
        $users = User::all();

        foreach ($users as $user) {
            ProcessUser::dispatch($user);
        }

        return back()->with('message', '【従来方法】' . $users->count() . ' 件のジョブを個別にディスパッチしました。');
    }

    /**
     * 【Bus::bulk()方法】配列をまとめて一括ディスパッチする
     */
    public function dispatchBulk(): RedirectResponse
    {
        $users = User::all();

        Bus::bulk(
            array_map(fn (User $user) => new ProcessUser($user), $users->all())
        );

        return back()->with('message', '【Bus::bulk()方法】' . $users->count() . ' 件のジョブをまとめてディスパッチしました。');
    }
}

ステップ3の補足:従来方法と Bus::bulk() の違いを整理する

従来方法とBus::bulk()の内部動作の違い(URLの記載に基づく)

従来のループディスパッチ(ProcessUser::dispatch($user)):

ループ1回目 → キューに1件プッシュ
ループ2回目 → キューに1件プッシュ
ループ3回目 → キューに1件プッシュ

ユーザーが100人いれば → 100回キュー操作が発生

Bus::bulk() によるディスパッチ:

配列を渡す

接続(connection)とキュー(queue)ごとにジョブをグループ化

グループ単位でキューの bulk() メソッドを使って一括プッシュ

Laravel News の記事では、Bus::bulk()「ジョブを接続とキューごとにグループ化してからキューの bulk() メソッドを使ってプッシュする」 と説明されています。

また、Bus::batch() との違いとして、「データベースでバッチの進捗を追跡しない」 ため、その分のオーバーヘッドがないとされています。

ステップ4: Routeの設定

目的: ブラウザから各ディスパッチメソッドを呼び出せるようにURLを割り当てる。

routes/web.php
<?php

use App\Http\Controllers\JobDispatchController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('dispatch');
});

Route::post('/dispatch/legacy', [JobDispatchController::class, 'dispatchLegacy'])
    ->name('dispatch.legacy');

Route::post('/dispatch/bulk', [JobDispatchController::class, 'dispatchBulk'])
    ->name('dispatch.bulk');

ステップ5: Viewの作成

目的: 「従来方法ボタン」と「Bus::bulk()ボタン」を並べて、どちらの方法でも手軽に試せる画面を作る。

resources/views/dispatch.blade.php を新規作成します。

touch resources/views/dispatch.blade.php
resources/views/dispatch.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Job Dispatch 比較</title>
    <style>
        body {
            font-family: sans-serif;
            max-width: 640px;
            margin: 60px auto;
            padding: 0 20px;
            background: #f5f5f5;
            color: #333;
        }
        h1 { font-size: 1.4rem; margin-bottom: 8px; }
        p.description { font-size: 0.9rem; color: #666; margin-bottom: 32px; }
        .card {
            background: #fff;
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 24px;
            margin-bottom: 16px;
        }
        .card h2 { font-size: 1rem; margin: 0 0 8px; }
        .card p { font-size: 0.85rem; color: #666; margin: 0 0 16px; }
        button {
            display: inline-block;
            padding: 10px 20px;
            border: none;
            border-radius: 6px;
            font-size: 0.9rem;
            cursor: pointer;
        }
        .btn-legacy { background: #e8e8e8; color: #333; }
        .btn-legacy:hover { background: #d0d0d0; }
        .btn-bulk { background: #3b82f6; color: #fff; }
        .btn-bulk:hover { background: #2563eb; }
        .alert {
            padding: 12px 16px;
            border-radius: 6px;
            margin-bottom: 24px;
            font-size: 0.9rem;
        }
        .alert-success { background: #d1fae5; border: 1px solid #6ee7b7; color: #065f46; }
    </style>
</head>
<body>

    <h1>🚀 Job Dispatch 比較デモ</h1>
    <p class="description">
        従来のループディスパッチと、Laravel 13.13 <code>Bus::bulk()</code> を比較できます。<br>
        ボタンを押すと <code>jobs</code> テーブルにジョブが積まれます。
    </p>

    {{-- 結果メッセージ --}}
    @if (session('message'))
        <div class="alert alert-success">
 {{ session('message') }}
        </div>
    @endif

    {{-- 従来方法カード --}}
    <div class="card">
        <h2>📦 従来方法(ループで個別ディスパッチ)</h2>
        <p>
            <code>foreach</code> ループで <code>ProcessUser::dispatch($user)</code> を繰り返し呼び出します。<br>
            ユーザーの数だけキュー操作が個別に発生します。
        </p>
        <form action="{{ route('dispatch.legacy') }}" method="POST">
            @csrf
            <button type="submit" class="btn-legacy">従来方法でディスパッチ</button>
        </form>
    </div>

    {{-- Bus::bulk()カード --}}
    <div class="card">
        <h2> Bus::bulk()(一括ディスパッチ)</h2>
        <p>
            <code>Bus::bulk()</code> にJobオブジェクトの配列を渡します。<br>
            接続とキューごとにジョブがグループ化され、まとめてプッシュされます。<br>
            バッチ進捗のDB追跡は行いません(<code>Bus::batch()</code> との違い)。
        </p>
        <form action="{{ route('dispatch.bulk') }}" method="POST">
            @csrf
            <button type="submit" class="btn-bulk">Bus::bulk()でディスパッチ</button>
        </form>
    </div>

</body>
</html>

ステップ6: 動作確認

目的: 実際にボタンを押してジョブがキューに積まれることを確認する。

6-1. テスト用ユーザーを用意する

まだユーザーが存在しない場合は、tinker を使って作成します。

sail artisan tinker
App\Models\User::factory(10000)->create();

10000人のユーザーが作成されます。exit で終了してください。
ここで大量のユーザーを作成しないと効果が分からないです。

6-2. ブラウザで画面を開く

Sailを起動していれば http://localhost でアクセスできます(ポートは80番)。比較デモ画面が表示されます。

6-3. ジョブがキューに積まれることをphpMyAdminで確認する

「従来方法でディスパッチ」ボタン または 「Bus::bulk()でディスパッチ」ボタン を押した後、http://localhost:8888 を開きます。

左サイドバーから laraveljobs テーブルを選択すると、ディスパッチされたジョブのレコードが確認できます。ボタンを押すたびにレコードが増えていく様子を目で確認してみてください。

6-4. キューワーカーでジョブを実際に処理する

別のターミナルタブを開いて、キューワーカーを起動します。

sail artisan queue:work

ターミナルに次のような出力が流れればジョブの処理成功です。

  INFO  Processing jobs from the [default] queue.

  App\Jobs\ProcessUser .............................. RUNNING
  App\Jobs\ProcessUser .............................. DONE 12ms
  App\Jobs\ProcessUser .............................. RUNNING
  App\Jobs\ProcessUser .............................. DONE 11ms
  (ユーザーの数だけ繰り返される)

ワーカーの処理が完了したら、phpMyAdminで jobs テーブルを再確認してみましょう。処理済みのレコードが削除されて、テーブルが空になっていれば成功です。

また、storage/logs/laravel.log を開くと、Jobの handle() メソッドで書いたログが確認できます。

[...] local.INFO: ProcessUser job executed for user ID: 1
[...] local.INFO: ProcessUser job executed for user ID: 2
[...] local.INFO: ProcessUser job executed for user ID: 3
(以下続く)

6-5. 作業が終わったらコンテナを停止する

sail down

次回作業を再開するときは sail up -d で起動します。

完成後の動作イメージ

画面上には次の2枚のカードが並んで表示されます。

まとめ:使い分けのポイント

比較項目 従来のループ Bus::bulk() Bus::batch()
ディスパッチ方式 1件ずつ個別 まとめて一括 まとめて一括
バッチ進捗のDB追跡 なし なし あり
使いどころ 少数のジョブ 多数のジョブをまとめて流したいとき 完了・失敗をトラッキングしたいとき

Bus::bulk() は、ジョブを接続とキューごとにグループ化してキューの bulk() メソッドを使って一括プッシュします。Bus::batch() とは異なり、バッチの進捗をデータベースで追跡しないため、その分のオーバーヘッドがなく、単純に多数のジョブをキューに積みたいケースに向いています。

次のステップ

この記事では Bus::bulk() の基本的な使い方を学びました。次は以下のテーマに取り組むと、キューへの理解がさらに深まります。

  • Bus::batch() を試してみる:ジョブの完了・失敗をトラッキングしたい場合は Bus::batch() が向いています。then()catch()finally() といったコールバックを使って、バッチ処理後の後続処理を柔軟に制御できます
  • 失敗したジョブのリトライを学ぶsail artisan queue:failed で失敗したジョブを確認し、sail artisan queue:retry で再実行する運用フローを把握しておくと、本番環境での障害対応に役立ちます
  • キュードライバーをRedisに切り替える:今回は database ドライバーを使いましたが、本番環境では Redis が一般的です。QUEUE_CONNECTION=redis に変更し、Sailの compose.ymlredis サービスを追加するだけで切り替えられます
  • Horizon(ホライズン)を導入する:Laravelの公式キューダッシュボードである Horizon を使うと、キューの処理状況・スループット・失敗ジョブをブラウザから一元管理できます。Redisドライバーが前提となります

参考リンク

この記事を作成するにあたり、以下を参照しました。

Discussion