编程

如何解决 Laravel 队列 "ModelNotFoundException" 问题

880 2023-04-30 07:06:00

Laravel 因为没有找到相关模型而导致队列任务失败,该怎么办?

让我们一起看看队列任务中的 ModelNotFoundExpection 异常有哪些处理方法。

Illuminate\Database\Eloquent\ModelNotFoundException: No query results for model [App\Models\User]

这是一个非常常见的错误,尤其是在大型项目中,当大量工作正在处理时,会发生意想不到的事情。发生这种情况的主要原因有两个:

  1. 在派发任务前没有将模型保存到数据库
  2. 在队列任务派发和执行期间,模型被删除了
  3. 在后来回滚的数据库事务期间触发了事件/监听器

幸运的是,这三种原因都很容易解决

队列任务排放前保存模型

请确保理解 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
    }
}