エラーの詳細
トランザクション内でユーザー登録を行い、その直後に登録したユーザーへ認証情報をメールで送信するため、トランザクション内でジョブをディスパッチしていました。
try {
DB::beginTransaction();
// ユーザー登録処理
$user = User::create($params);
// 〜〜その他関連モデルの登録処理〜〜(省略)
// ジョブ内でユーザーにメール送信
AuthCredentialsNotificationJob::dispatch($user);
DB::commit();
} catch (Exception $e) {
DB::rollBack();
Log::error(__METHOD__ . ' ユーザー登録処理でエラーが発生しました。 ' . $e);
return back()->withInput()->withErrors('ユーザーの登録に失敗しました');
}
この処理を実行すると、約30%の確率で以下のようなエラーが発生していました。
Illuminate\Database\Eloquent\ModelNotFoundException: No query results for model [App\Models\User].
in /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:688
#0 Illuminate\Database\Eloquent\Builder->firstOrFail()
#1 App\Jobs\AuthCredentialsNotificationJob->restoreModel()
#2 App\Jobs\AuthCredentialsNotificationJob->__unserialize()
// 以下省略
エラーの原因
調査の結果、原因はトランザクション中にジョブをディスパッチしていたことでした。
何が起きているのか?
AuthCredentialsNotificationJob
クラスでは、次のようにUser
モデルを直接プロパティとして渡しています。
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
class AuthCredentialsNotificationJob implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(
public User $user // ← ここでUserモデルを渡している
) {
}
// 省略
}
Laravelのジョブはキュー投入時にモデルをシリアライズし、復元時にはfindOrFail($id)
のように再度DBからそのモデルを取得しようとします。
しかし、トランザクションがまだコミットされていない場合、DBにはユーザー情報が存在していません。
そのため、ジョブ復元時にModelNotFoundException
が発生していた、というわけです。
解決策
Laravelでは、ジョブのディスパッチをトランザクションのコミット後に遅らせることができます。
そのための設定が after_commit オプションです。
'sqs' => [
'driver' => 'sqs',
'key' => env('SQS_KEY', 'your-public-key'),
'secret' => env('SQS_SECRET', 'your-secret-key'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'your-queue-name'),
'region' => env('SQS_REGION', 'us-east-1'),
'after_commit' => true, // これを追加!
],
この設定により、トランザクション中にジョブをディパッチしても、トランザクションがコミットされるまではジョブが実際にキューへ送られないようになります。
この設定を適用して以降、エラーは一切発生しなくなりました。
まとめ
- トランザクション中にモデルを含んだジョブをディスパッチすると、ジョブ復元時にモデルが見つからずエラーになる可能性がある
-
after_commit: true
を設定することで、トランザクションのコミット後にジョブがキュー投入される
参考文献