Laravel というか Database を扱う上で忘れてはいけない Transaction。
ただ、n+1問題と同じくらい軽視されている印象がある...。
(開発時には、問題になりにくいからね...)
トランザクションは以下の流れで行われる。
詳しくはほかの記事へGO!
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に書き込まれる保証があれば要件は十分です。
<?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 に記載して完成!
->withMiddleware(function (Middleware $middleware) {
...
$middlewar->append(\App\Http\Middleware\AutoTransaction::class);
...
})
書き方変わってもまだ Kernel.php を探す自分がいる...。
これ、ネストさせたらどうなるの?
Laravel はネストされたトランザクションも吸収してくれます。
全体にトランザクションを張った状態で、コントローラー内部処理でトランザクションを張ると、一部は例外でスキップ、一部は正常に記録する、といった処理となります。
ただ見通しが非常に悪くなり、そもそも一つのルートに処理を書くことが間違っている例が多いかと思います。
APIのルートはシンプルに構築して、完全実行 or 何も実行しない、に寄せましょう。
基本的にはネストさせない、どうしてもの時は手動トランザクションを張るのがおすすめです。
最後に
ほかにも「ロックの概念」「分離レベル」を押さえておくと、変なデットロックを回避して運用できます!
楽しい Laravel ライフを!
