はじめに
LaravelでJobクラスを実装する際、Serviceクラスと同じ感覚でEloquentモデルをコンストラクタインジェクションしたところ、本番環境でジョブが失敗する事象に遭遇しました。
Serviceでは問題なく動いていたため、最初は理由が分からず悩みましたが、調査の結果、非同期処理特有の落とし穴であることが分かりました。
この記事では以下のポイントを解説します:
- JobでEloquentモデルを渡すと起こる問題
- Serviceとの違い
- 安全なモデルの扱い方
問題が起きたコード
以下のように、JobクラスでEloquentモデルをそのまま受け取っています。
class SomeJob implements ShouldQueue
{
use SerializesModels;
public function __construct(
public Hoge $hoge // ← Eloquentモデルを直接渡している
) {}
public function handle()
{
$this->hoge->update([...]);
}
}
このジョブは SomeJob::dispatch($hoge)
でキューに投入されましたが、実行時にエラーで落ちました。
Serviceでは問題なかったのに、なぜJobだけ落ちたのか?
以下はServiceクラスの例です:
class SomeService
{
public function process(Hoge $hoge)
{
$hoge->update([...]);
}
}
こちらは正常に動作します。
その理由は、Serviceはすぐに同期実行されるのに対し、Jobは非同期で数分後などに実行されるためです。
Laravelの裏側で何が起きているのか?
LaravelのJobでは、SerializesModels
トレイトを使うことで、Eloquentモデルをシリアライズする際にモデル本体ではなくそのIDのみを保存する仕組みになっています。
そのため、ジョブがキューから取り出されて実行される際には、IDをもとにモデルを再取得する処理が走ります。
Jobの流れを図解で説明
1. モデルを取得(Hoge::find(1))
2. dispatch() によりキューへ → IDだけが保存される
3. 数分後、Jobがワーカーで実行される
4. モデルIDを元に再取得 → 存在しないとnull
5. null->update() で実行時エラーが発生
例えば、該当のモデルが削除されていたり、関連が壊れていたりすると、find()
に失敗して null が返り、エラーに繋がります。
回避策
方法①:IDだけ渡す(推奨)
class SomeJob implements ShouldQueue
{
public function __construct(public int $hogeId) {}
public function handle()
{
$hoge = Hoge::find($this->hogeId);
if (! $hoge) {
\Log::warning("Hoge not found");
return;
}
$hoge->update([...]);
}
}
呼び出し側も dispatch($hoge->id)
に変えましょう。
方法②:モデルを渡すが null チェックを入れる
class SomeJob implements ShouldQueue
{
use SerializesModels;
public function __construct(public Hoge $hoge) {}
public function handle()
{
if (! $this->hoge) {
\Log::warning("Hoge was deleted before job execution");
return;
}
$this->hoge->update([...]);
}
}
この方法は柔軟ですが、レコードがなければ処理自体がスキップされるため、副作用を期待する処理では注意が必要です。
まとめ
比較項目 | Service | Job(Queue) |
---|---|---|
実行タイミング | 即時実行 | 遅延・非同期 |
モデルの保持 | メモリ上で保持 | IDだけ保存し、あとで再取得 |
注意点 | 特になし | モデルが削除されているとnullになる |
おわりに
LaravelのJobでは、Serviceと同じ感覚でEloquentモデルを渡すと、実行タイミングのズレにより予期せぬエラーが発生することがあります。
非同期であるという前提を意識し、基本はIDを渡す設計にしておくことで、安全で再現性のある処理を構築できます。