Day 6・責務の分離とデータ整合性 — サービス層・トランザクション・排他制御
トランザクション — 複数の更新を全部成功か全部失敗に
ACIDの考え方と、DB::transactionで整合性を守るべき場面。
なぜトランザクションが必要か
前の注文処理では『在庫を減らす』『注文を作る』『ポイントを付ける』と、複数のテーブルを続けて更新しました。もし在庫を減らした直後に例外が起きて処理が止まったら、在庫は減ったのに注文が作られない、という矛盾した状態が残ります。
とは、こうした複数の更新を『ひとかたまり』として扱い、全部成功したときだけ確定(コミット)し、途中で失敗したら全部なかったことにする(ロールバック)仕組みです。中途半端な状態を残さないのが目的です。
ACID — トランザクションが守る4性質
- Atomicity(原子性): 全部成功か、全部失敗か。中途半端がない。
- Consistency(一貫性): 実行の前後でDBの整合性ルールが保たれる。
- Isolation(独立性): 同時に走る他のトランザクションから干渉されない。
- Durability(永続性): コミットした結果は障害が起きても失われない。
この4性質をまとめて と呼びます。面接では特に原子性(全部成功か全部失敗か)を説明できると強いです。
Laravelでの書き方
use Illuminate\Support\Facades\DB;
public function place(User $user, array $data): Order
{
return DB::transaction(function () use ($user, $data) {
$product = Product::findOrFail($data['product_id']);
if ($product->stock < $data['quantity']) {
// 例外を投げると、それまでの更新はすべて取り消される
throw new OutOfStockException();
}
$product->decrement('stock', $data['quantity']);
$order = Order::create([
'user_id' => $user->id,
'product_id' => $product->id,
'quantity' => $data['quantity'],
]);
$user->increment('points', 10);
// ここまで例外なく到達すれば、まとめてコミットされる
return $order;
});
}なぜクロージャで囲むだけで安全になるのか
DB::transaction は、クロージャ内で例外が投げられると自動でロールバックし、最後まで到達すれば自動でコミットします。開発者が begin / commit / rollback を手書きすると、ロールバックの書き忘れで矛盾が残ります。囲むだけで『全部か無か』を保証できるので、事故を仕組みで防げます。
どんな場面で使うか
- 口座間の送金: Aから引いてBへ足す。片方だけ成功したら残高が壊れる。
- 在庫を減らして注文を作る: 片方だけだと在庫と注文が食い違う。
- 親レコードと複数の子レコードを一括登録: 途中で失敗したら親だけ残る。
何でも囲めばいいわけではない
トランザクション中はロックを保持するため、長時間の外部API呼び出しやメール送信をトランザクション内に入れると、他の処理を待たせて性能を落とします。DBの整合性に関わる更新だけを囲み、メール送信などはコミット後に回すのが定石です。
面接ではこう言う
「複数テーブルの更新で整合性が崩れると困る場面では、トランザクションで囲みます。要は全部成功か全部失敗かという原子性を保証したいからです。LaravelではDB::transactionのクロージャ内で例外が出れば自動ロールバックされます。ただしメール送信のような外部処理はロックを長引かせるのでコミット後に回します」
読んでも腑に落ちない所は、AIに噛み砕いてもらいましょう。
理解度チェック
答えられれば、面接で説明できる状態に一歩近づきます。
ACIDの「原子性(Atomicity)」が意味することは?
口座間の送金を例に、なぜトランザクションが必要か説明してください。
記述式です。まず自分の言葉で答えを考えてから、模範解答を開いて自己採点してください。