0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Laravel】MiddlewareにTransactionを張る選択肢

Posted at

Laravel というか Database を扱う上で忘れてはいけない Transaction
ただ、n+1問題と同じくらい軽視されている印象がある...。
(開発時には、問題になりにくいからね...)

トランザクションは以下の流れで行われる。
詳しくはほかの記事へGO!

transaction.drawio.png

Laravelのトランザクション

Laravel でトランザクションを利用するには、基本的に DB ファサードを利用する。
上記図のそれぞれのメソッドが用意されているので、try-catch を利用して記述するとよい。

DB::beginTransaction();

    // DB 処理
    // Model 処理など
    
    DB::commit();

} catch (Exception $e) {
    DB::rollback();
}

これがトランザクションの基本。

ファサードのトランザクション

DB ファサードにある transaction() メソッドを利用するともっと簡単に書ける。
これは引数にクロージャーを受け、内部の処理が完了したら自動的にコミットしてくれる。
また、内部で例外が発生した場合、自動的にロールバックを実行して例外を吐きます。

とても読みやすい。

use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    // DB 処理
    // Model 処理など
});

ただこちらには try-catch 機能はないので、自身で用意してあげる必要がある。
面倒だが、エラーハンドリングがやりやすいメリットはある。

Middlewareでトランザクション

で思ったのが、毎回確実に記載するのめんどくさくない?と。
人間なので必ず実装をミスります。
なので、いっそ Middleware に配置して、すべての場合にトランザクションを張るようにすれば、初心者でも問題なく開発ができるんじゃないか?という思想です。

どんな汚いロジックでも、確実にDBに書き込まれる保証があれば要件は十分です。

app\Http\Middleware\AutoTransaction.php
<?php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class AutoTransaction
{
    protected $targetMethods = [
        'POST',
        'PUT',
        'PATCH',
        'DELETE',
    ];

    public function handle(Request $request, Closure $next)
    {
        $method = Str::upper($request->getMethod());
        if (Arr::exists($this->targetMethods, $method)) {
            DB::beginTransaction();

            $response = $next($request);

            if (data_get($response, 'exception')) {
                DB::rollBack();
            } else {
                DB::commit();
            }

            return $response;
        } else {
            return $next($request);
        }
    }
}

こんな感じの薄いミドルウェアを作成します。
ポイントは2つ。

一つ目はメソッド名の制限です。
トランザクションは、張りすぎると全体の効率が落ちてしまうので、必要なメソッドに絞って張るようにしています。
デットロックは大丈夫かという心配もありますが、各ルートはREST APIを基準に、1処理1ルートの粒度にすれば、影響はほぼありません。
長時間大量の処理が見込まれるルートは、予めミドルウェアの適用対象から外します。
withoutMiddleware()

二つ目は exception の有無で commit と rollback を切り替えている点です。
Laravel の場合、ハンドリングされていない例外は必ず exception をもって、Middleware へ帰ってきます。
それを利用して、切り替えているわけですねぇ~。

これを bootstrap に記載して完成!

bootstrap/app.php
    ->withMiddleware(function (Middleware $middleware) {
        ...
        $middlewar->append(\App\Http\Middleware\AutoTransaction::class);
        ...
    })

書き方変わってもまだ Kernel.php を探す自分がいる...。

これ、ネストさせたらどうなるの?

Laravel はネストされたトランザクションも吸収してくれます。
全体にトランザクションを張った状態で、コントローラー内部処理でトランザクションを張ると、一部は例外でスキップ、一部は正常に記録する、といった処理となります。

ただ見通しが非常に悪くなり、そもそも一つのルートに処理を書くことが間違っている例が多いかと思います。
APIのルートはシンプルに構築して、完全実行 or 何も実行しない、に寄せましょう。

基本的にはネストさせない、どうしてもの時は手動トランザクションを張るのがおすすめです。

最後に

ほかにも「ロックの概念」「分離レベル」を押さえておくと、変なデットロックを回避して運用できます!
楽しい Laravel ライフを!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?