这篇文章主要介绍“Laravel代码中如何正确使用数据库事务”,在日常操作中,相信很多人在Laravel代码中如何正确使用数据库事务问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Laravel代码中如何正确使用数据库事务”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
什么是数据库事务?
在我们开始研究 Laravel 的数据库事务之前,让我们先看看它们是什么以及它们如何有益。
对于什么是数据库事务,有许多听起来复杂的技术解释。但是,对于大多数 web 开发人员来说,我们只需要知道事务是完成数据库中整个工作单元的方式。
为了理解这实际上意味着什么,让我们来看一个基本的例子,它将给出一点上下文。
假设我们有一个允许用户注册的应用程序。每当用户注册时,我们都希望为他们创建一个新帐户,然后为他们分配一个默认角色“ general”。
我们的代码可能是这样的:
$user = User::create([
'email' => $request->email,
]);
$user->roles()->attach(Role::where('name', 'general')->first());
乍一看,这段代码似乎完全没问题。但是,当我们仔细观察的时候,我们可以发现实际上有一些事情可能会出错。我们可以创建用户,但是不能为他们分配角色。这可能是由许多不同的原因造成的,比如分配角色的代码中的错误,或者甚至是阻止我们到达数据库的硬件问题。
由于这种情况的发生,这将意味着系统中将有一个没有角色的用户。正如您可以想象的那样,这可能会在您的应用程序中的其他地方引起异常和 bug,因为您总是假设用户有一个角色(这是正确的)。
因此,为了解决这个问题,我们可以使用数据库事务。通过使用事务,它可以确保在执行代码时,如果出现任何错误,事务内部对数据库的任何更改都将回滚。例如,如果用户被插入到数据库中,但是由于任何原因分配角色的查询失败,那么事务将被回滚,用户行将被删除。通过这样做,它意味着我们不能创建没有分配角色的用户。
换句话说,它“要么全有,要么全没有”。
在 Laravel 中使用数据库事务
现在我们对事务是什么以及它们实现了什么有了一个简单的概念,让我们来看看如何在 Laravel 中使用它们。
在 Laravel 中,由于我们可以在
DB
门面上访问 transaction()
方法,因此开始使用事务实际上是很容易的事。继续使用之前的示例代码,让我们看看在创建用户并为其分配角色时如何使用事务。
use IlluminateSupportFacadesDB;
DB::transaction(function () use ($user, $request): void {
$user = User::create([
'email' => $request->email,
]);
$user->roles()->attach(Role::where('name', 'general')->first());
});
现在我们的代码被包裹在一个数据库事务中,如果在其中的任意一点抛出异常,对数据库的任何更改都将返回到事务开始之前的状态。
在 Laravel 中手动使用数据库事务
有时,您可能希望对事务进行更精细的控制。例如,假设您正在与第三方服务集成,比如 Mailchinp 或 Xero。我们会说,每当您创建一个新用户时,您还需要向他们的 API 发出 HTTP 请求,以将他们也创建为该系统中的用户。
我们可能想要更新我们的代码,以便如果我们无法在我们自己的系统 ** 且 ** 在第三方系统中创建用户,则两个系统都不创建用户。 如果您正在与第三方系统交互,那么您可能有一个可用于发出请求的类。 或者,可能有一个您可以使用的包。 有时,当某些请求无法完成时,发出请求的类可能会抛出异常。 然而,其中一些类可能会消除错误,而只是从您调用的方法中返回
false
,并将错误放置在类的字段中。因此,我们假设我们有以下调用 API 的基本示例类:
class ThirdPartyService
{
private $errors;
public function createUser($userData)
{
$request = $this->makeRequest($userData);
if ($request->successful()) {
return $request->body();
}
$errors = $request->errors();
return false;
}
public function getErrors()
{
return $this->errors;
}
}
当然,上面的请求类代码是不完整的,我下面的代码示例也不是很清楚,但它应该能让您大致了解我要表达的观点。所以让我们使用这个请求类并将其添加到我们之前的代码示例中:
use IlluminateSupportFacadesDB;
use AppServicesThirdPartyService;
DB::beginTransaction();
$thirdPartyService = new ThirdPartyService();
$userData = [
'email' => $request->email,
];
$user = User::create($userData);
$user->roles()->attach(Role::where('name', 'general')->first());
if ($thirdPartyService->createUser($userData)) {
DB::commit();
return;
}
DB::rollBack();
report($thirdPartyService->getErrors());
查看上面的代码,我们可以看到我们启动了一个事务,创建了用户并为他们分配了一个角色,然后我们调用了第三方服务。如果在外部服务中成功创建了用户,知道所有内容都已正确创建,我们就可以安全地提交数据库更改。但是,如果没有在外部服务中创建用户,则回滚数据库中的更改(删除用户及其角色分配),然后报告错误。
使用自动或手动事务
同样值得注意的是,因为我们最初的示例使用
DB:transaction()
方法,在抛出异常时回滚事务,所以我们也可以使用这种方法向我们的第三方服务发出请求。相反,我们可以这样更新类:
use IlluminateSupportFacadesDB;
use AppServicesThirdPartyService;
DB::transaction(function () use ($user, $request): void {
$user = User::create([
'email' => $request->email,
]);
$user->roles()->attach(Role::where('name', 'general')->first());
if (! $thirdPartyService->createUser($userData)) {
throw new Exception('User could not be created');
}
});
这绝对是一个可行的解决方案,并将按照预期成功回滚事务。事实上,就我个人的偏好而言,我实际上更喜欢这种方式,而不是手动使用事务。我认为它看起来更容易阅读和理解。
然而,与手动提交或回滚事务时使用 ‘if’ 语句相比,异常处理在时间和性能方面可能会比较昂贵。
因此,举个例子,如果这段代码用于导入包含10,000个用户数据的 CSV 文件,您可能会发现抛出异常会大大减慢导入速度。
但是,如果它只是在一个用户可以注册的简单web请求中使用,那么抛出异常可能没有问题。当然,这取决于应用程序的大小,性能是关键因素;所以你需要根据具体情况来决定。
在数据库事务中调度队列
每当您在事务中处理队列时,您都需要注意一个“陷阱”。
为了提供一些上下文,让我们继续使用之前的代码示例。我们可以想象,在我们创建了我们的用户之后,我们想要运行一个任务来提醒管理员通知他们新注册并向新用户发送欢迎电子邮件。我们将通过分派一个名为
AlertNewUser
的队列任务来做到这一点,如下所示:
use IlluminateSupportFacadesDB;
use AppJobsAlertNewUser;
use AppServicesThirdPartyService;
DB::transaction(function () use ($user, $request): void {
$user = User::create([
'email' => $request->email,
]);
$user->roles()->attach(Role::where('name', 'general')->first());
AlertNewUser::dispatch($user);
});
当您开始一个事务并对其中的任何数据进行更改时,这些更改仅对正在运行事务的请求/进程可用。对于任何其他访问您更改的数据的请求或进程,必须先提交事务。因此,这意味着如果我们从事务内部分派任何排队的队列、事件监听器、邮件,通知或广播事件。由于竞争条件,我们的数据更改可能在事务内部不可用。
如果队列在事务提交之前开始处理排队的代码,就会发生这种情况。因此,这可能导致您的排队代码可能试图访问不存在的数据,并可能导致错误。在我们的例子中,如果在事务提交之前运行队列
AlertNewUser
作业,那么我们的作业将尝试访问一个尚未实际存储在数据库中的用户。如您所料,这将导致作业失败。为了防止这种竞争条件的发生,我们可以对我们的代码和/或我们的配置进行一些更改,以确保仅在事务成功提交后才调度队列。
我们可以更新
config/queue.php
并添加 after commit
字段。让我们想象一下,我们正在使用 redis
队列驱动程序,所以我们可以这样更新配置:
[
// ...
'redis' => [
'driver' => 'redis',
// ...
'after_commit' => true,
],
// ...
],
// ...
];
通过进行此更改,如果我们尝试在事务内调度队列,则队列将在实际调度队列之前等待事务提交。 方便的是,如果事务回滚,它也会阻止队列被调度。
然而,可能有一个原因,您不希望在配置中全局设置此选项。 如果是这种情况,Laravel 仍然提供了一些很好的助手方法,我们可以根据具体情况使用它们。
如果我们想更新事务中的代码,只在任务提交后才分派任务,可以使用
afterCommit()
方法,如下所示:
use IlluminateSupportFacadesDB;
use AppJobsAlertNewUser;
use AppServicesThirdPartyService;
DB::transaction(function () use ($user, $request): void {
$user = User::create([
'email' => $request->email,
]);
$user->roles()->attach(Role::where('name', 'general')->first());
AlertNewUser::dispatch($user)->afterCommit();
});
Laravel 还提供了另一个我们可以使用的方便的
beforeCommit()
方法。 如果我们在队列配置中设置了全局after_commit => true
,但不关心等待事务被提交,就可以使用这个。 要做到这一点,我们可以简单地像这样更新我们的代码:
use IlluminateSupportFacadesDB;
use AppJobsAlertNewUser;
use AppServicesThirdPartyService;
DB::transaction(function () use ($user, $request): void {
$user = User::create([
'email' => $request->email,
]);
$user->roles()->attach(Role::where('name', 'general')->first());
AlertNewUser::dispatch($user)->beforeCommit();
});