如何解决 Laravel 队列 "ModelNotFoundException" 问题
Laravel 因为没有找到相关模型而导致队列任务失败,该怎么办?
让我们一起看看队列任务中的 ModelNotFoundExpection
异常有哪些处理方法。
Illuminate\Database\Eloquent\ModelNotFoundException: No query results for model [App\Models\User]
这是一个非常常见的错误,尤其是在大型项目中,当大量工作正在处理时,会发生意想不到的事情。发生这种情况的主要原因有两个:
- 在派发任务前没有将模型保存到数据库
- 在队列任务派发和执行期间,模型被删除了
- 在后来回滚的数据库事务期间触发了事件/监听器
幸运的是,这三种原因都很容易解决
队列任务排放前保存模型
请确保理解 Model::create()
、Model::make()
以及 new Model
的不同支持。
$model = new Model
创建了一个新的模型实例,不过还未保存到数据库。当你完成对属性赋值后,请确保调用 ->save()
方法,将其保存到数据库中。
<?php
$user = new User;
$user->name = 'Arunas';
// At this point, the model is not saved yet.
// Make sure to call the ->save() method to save it to the database
$user->save();
当通过 Model::make()
创建实例时,也是一样:
<?php
$user = User::make(['name' => 'Arunas']);
// at this point, the model is not saved yet.
// Make sure to call the ->save() method to save it to the database
$user->save();
在队列任务派发和执行期间,模型被删除了
除非你的队列驱动设置成了同步,你的队列任务会在将来的某个时间点执行。在这段时间里可能会发生很多事情,哪怕只是一秒钟。数据库事务回滚、异常或其他会导致您的模型被删除的情况。当你的队列 worker 操作任务时,模型已经全部消失,作业失败,并出现 ModelNotFoundException
。
这种情况下,有两个方法应对。
a. 模型缺失时,忽略队列任务
这通常是想要的行为。例如,如果你有一份向新用户发送欢迎电子邮件的工作,但用户注册失败,那么你不希望发送该电子邮件。
你可以添加 public $deleteWhenMissingModels = true;
属性到队列任务中,实现该目标(Laravel 5.7+):
<?php
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* @var User
*/
public $user;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
...
}
b. 不要序列化模型并将整个实例存储在工作队列中
通过这种方式,整个对象按原样存储在 worker 队列中。当 worker 调用队列任务时,它不再需要从数据库加载模型,因为它已经有了模型!
要做到这一点,只需从作业中删除 SeralizesModels
trait:
<?php
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
/**
* @var User
*/
public $user;
...
}
将任务/监听器延迟到提交数据库事务之后
如果你在数据库事务中调度队列任务或事件,那么将面临事务回滚和队列作业没有任何数据可供使用的风险。你可以在这里了解更多关于延迟工作和听众的信息,但我也会在本文中给你一个快速的解决方案。
在提交事务之后派发任务
有好几种方法实现,不过这里有一些比较有用的方法
派发任务时,使用 ->afterCommit()
方法:
DB::transaction(function () {
// Perform database queries here
dispatch(new MyJob())->afterCommit();
// alternatively, if the job uses the Dispatchable trait:
// MyJob::dispatch($data)->afterCommit();
// Perform other operations that could potentially fail
// and roll back the transaction.
});
或者,当事务被提交时,可以使用 DB::afterCommit()
钩子来运行函数:
DB::transaction(function () {
// Perform database queries here
DB::afterCommit(function () {
dispatch(new MyJob($data));
});
// Perform other operations that could potentially fail
// and roll back the transaction.
});
如果你要执行多个语句而不只是派发任务,使用 DB::afterCommit()
会是很好的选择。
延迟事件监听到事务提交之后执行
将 $afterCommit = true
属性添加到事件监听器,使其只有在事务完成后执行,无论异步或者同步。
class SendNotificationListener
{
public $afterCommit = true;
public function handle(MyEvent $event)
{
// Send notification email here
}
}