ホームページ  >  記事  >  PHPフレームワーク  >  Laravelでのデータベーストランザクションの使い方を詳しく解説した記事

Laravelでのデータベーストランザクションの使い方を詳しく解説した記事

藏色散人
藏色散人転載
2021-12-20 10:54:142509ブラウズ

はじめに

Web開発では、データの整合性と正確性が非常に重要です。したがって、作成するコードがデータベース内のデータを安全な方法で保存、更新、削除できることを確認する必要があります。

この記事では、データベーストランザクションとは何か、なぜ重要なのか、そしてLaravelでデータベーストランザクションを使い始める方法について見ていきます。また、キューとデータベース トランザクションに関連する一般的な「問題」についても見ていきます。

データベース トランザクションとは何ですか?

Laravel のデータベース トランザクションについて説明する前に、まずトランザクションが何であり、どのような利点があるのか​​を見てみましょう。

データベース トランザクションとは何かについて、複雑に聞こえる技術的な説明がたくさんあります。ただし、ほとんどの Web 開発者にとって、知っておく必要があるのは、データベース内の作業単位全体がトランザクションによって完了する方法であるということだけです。

これが実際に何を意味するのかを理解するために、少しコンテキストを示す基本的な例を見てみましょう。

ユーザーが登録できるアプリケーションがあるとします。ユーザーがサインアップするたびに、新しいアカウントを作成し、デフォルトの役割「一般」を割り当てたいと考えています。 [関連する推奨事項: 最新の 5 つの Laravel ビデオ チュートリアル ]

私たちのコードは次のようになります:

$user = User::create([
    'email' => $request->email,
]);

$user->roles()->attach(Role::where('name', 'general')->first());

一見すると、このコードはまったく問題ないように見えます。しかし、よく見てみると、実際には問題が発生する可能性のあることがいくつかあることがわかります。ユーザーを作成することはできますが、ユーザーにロールを割り当てることはできません。これは、ロールを割り当てるコードのバグや、データベースにアクセスできないハードウェアの問題など、さまざまな理由によって発生する可能性があります。

これが発生すると、システム内にロールを持たないユーザーが存在することになります。ご想像のとおり、ユーザーにはロールがある (これは正しいことです) と常に想定されているため、アプリケーションの他の場所で例外やバグが発生する可能性があります。

したがって、この問題を解決するには、データベース トランザクションを使用できます。トランザクションを使用すると、コードの実行中にエラーが発生した場合でも、トランザクション内のデータベースへの変更はすべてロールバックされます。たとえば、ユーザーがデータベースに挿入されたものの、ロールを割り当てるクエリが何らかの理由で失敗した場合、トランザクションはロールバックされ、ユーザーの行は削除されます。これを行うと、ロールが割り当てられていないユーザーを作成できないことになります。

言い換えれば、「全か無か」です。

Laravel でのデータベース トランザクションの使用

トランザクションとは何か、トランザクションが何を実装するのかについて簡単に理解できたので、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());
});

これで、私たちのコードはデータベース トランザクションにラップされ、トランザクション内の任意の時点で例外がスローされた場合、データベースへの変更はトランザクションが開始される前の状態に戻されます。

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 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 [

    // ...

    &#39;connections&#39; => [

        // ...

        '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でのデータベーストランザクションの使い方を詳しく解説した記事の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はlearnku.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。