在 web 開發中,資料的完整性和準確性非常重要。因此,必須確保我們編寫的程式碼能夠以安全的方式儲存、更新和刪除資料庫中的資料。
在本文中,我們將看看什麼是資料庫事務,為什麼它們很重要,以及如何在 Laravel 開始使用它們。我們還將研究一個常見的“問題”,涉及隊列和資料庫事務。
在我們開始研究Laravel 的資料庫事務之前,讓我們先看看它們是什麼以及它們如何有益。
對於什麼是資料庫事務,有許多聽起來複雜的技術解釋。但是,對於大多數 web 開發人員來說,我們只需要知道事務是完成資料庫中整個工作單元的方式。
為了理解這實際上意味著什麼,讓我們來看一個基本的例子,它將給出一點上下文。
假設我們有一個允許用戶註冊的應用程式。每當用戶註冊時,我們都希望為他們建立一個新帳戶,然後為他們分配一個預設角色「 general」。
我們的程式碼可能是這樣的:
$user = User::create([ 'email' => $request->email, ]); $user->roles()->attach(Role::where('name', 'general')->first());
乍一看,這段程式碼似乎完全沒問題。但是,當我們仔細觀察的時候,我們可以發現實際上有一些事情可能會出錯。我們可以創建用戶,但是不能為他們分配角色。這可能是由許多不同的原因造成的,例如分配角色的程式碼中的錯誤,甚至是阻止我們到達資料庫的硬體問題。
由於這種情況的發生,這將意味著系統中將有一個沒有角色的使用者。正如您可以想像的那樣,這可能會在您的應用程式中的其他地方引起異常和 bug,因為您總是假設用戶有一個角色(這是正確的)。
因此,為了解決這個問題,我們可以使用資料庫事務。透過使用事務,它可以確保在執行程式碼時,如果出現任何錯誤,事務內部對資料庫的任何變更都會回滾。例如,如果使用者被插入到資料庫中,但是由於任何原因分配角色的查詢失敗,那麼交易將被回滾,用戶行將被刪除。透過這樣做,它意味著我們不能創建沒有分配角色的使用者。
換句話說,它「要麼全有,要麼全沒有」。
現在我們對事務是什麼以及它們實現了什麼有了一個簡單的概念,讓我們來看看如何在Laravel 中使用它們。
在 Laravel 中,由於我們可以在 DB
門面上存取 transaction()
方法,因此開始使用事務實際上是很容易的事。繼續使用先前的範例程式碼,讓我們看看在建立使用者並為其指派角色時如何使用交易。
use Illuminate\Support\Facades\DB; DB::transaction(function () use ($user, $request): void { $user = User::create([ 'email' => $request->email, ]); $user->roles()->attach(Role::where('name', 'general')->first()); });
現在我們的程式碼被包裹在一個資料庫事務中,如果在其中的任意一點拋出異常,對資料庫的任何更改都將返回到事務開始之前的狀態。
有時,您可能希望對交易進行更精細的控制。例如,假設您正在與第三方服務集成,例如 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 Illuminate\Support\Facades\DB; use App\Services\ThirdPartyService; 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());
查看上面的程式碼,我們可以看到我們啟動了一個事務,創建了用戶並為他們分配了一個角色,然後我們呼叫了第三方服務。如果在外部服務中成功創建了用戶,知道所有內容都已正確創建,我們就可以安全地提交資料庫變更。但是,如果沒有在外部服務中建立用戶,則回滾資料庫中的變更(刪除用戶及其角色分配),然後報告錯誤。
作为一个额外的技巧,我通常建议将任何影响第三方系统、文件存储或缓存的代码放在数据库调用之后。
为了更深入地理解这一点,让我们以上面的代码示例为例。请注意,在向第三方服务发出请求之前,我们是如何首先对数据库进行所有更改的。这意味着,如果从第三方请求返回任何错误,将回滚我们自己数据库中的用户和角色分配。
然而, 如果我们反过来做,我们在修改数据库之前发出请求,那就不是这种情况了。出于任何原因,如果我们在数据库中创建用户时发生任何错误,我们会在第三方系统中创建一个新用户,但是在我们系统中却没有创建。如你所想, 这可能会导致更多问题。通过编写一个清理方法将用户从第三方系统中删除,可以降低这个问题的严重性。 但是,正如您可以想象的那样, 这可能会导致更多的问题,并导致编写、维护和测试更多的代码。
所以,我总是建议把数据库调用放在API调用之前。但并不总是这样,有时可能需要将第三方请求返回的值保存到数据库中。如果是这种情况,就需要API调用放到数据库调用之前了,只要您确保有一些代码可以处理任何失败,这是完全可以的。
同样值得注意的是,因为我们最初的示例使用DB:transaction()
方法,在抛出异常时回滚事务,所以我们也可以使用这种方法向我们的第三方服务发出请求。相反,我们可以这样更新类:
use Illuminate\Support\Facades\DB; use App\Services\ThirdPartyService; 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 Illuminate\Support\Facades\DB; use App\Jobs\AlertNewUser; use App\Services\ThirdPartyService; 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
队列驱动程序,所以我们可以这样更新配置:
<?php return [ // ... 'connections' => [ // ... 'redis' => [ 'driver' => 'redis', // ... 'after_commit' => true, ], // ... ], // ... ];
通过进行此更改,如果我们尝试在事务内调度队列,则队列将在实际调度队列之前等待事务提交。 方便的是,如果事务回滚,它也会阻止队列被调度。
然而,可能有一个原因,您不希望在配置中全局设置此选项。 如果是这种情况,Laravel 仍然提供了一些很好的助手方法,我们可以根据具体情况使用它们。
如果我们想更新事务中的代码,只在任务提交后才分派任务,可以使用afterCommit()
方法,如下所示:
use Illuminate\Support\Facades\DB; use App\Jobs\AlertNewUser; use App\Services\ThirdPartyService; 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 Illuminate\Support\Facades\DB; use App\Jobs\AlertNewUser; use App\Services\ThirdPartyService; 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(); });
希望本文能让您大致了解什么是数据库事务以及如何在 Laravel 中使用它们。 它还向您展示了如何在从内部事务调度队列时避免“陷阱”。
原文地址:https://dev.to/ashallendesign/using-database-transactions-to-write-safer-laravel-code-13ek
译文地址:https://learnku.com/laravel/t/61575
【相关推荐:laravel视频教程】
以上是聊聊Laravel程式碼中怎麼正確地使用資料庫事務的詳細內容。更多資訊請關注PHP中文網其他相關文章!