#13 Laravel基礎
トランザクションと排他制御
トランザクションとは
トランザクション? 複数のDB操作をひとまとまりとして扱う仕組み。途中でエラーが起きたら全部元に戻す(ロールバック)ことで、データの整合性を保つ。 は、複数のデータベース操作を「まとめて1つの処理」として扱う仕組みです。
たとえば「銀行振込」を考えてみましょう。
1. AさんのDB残高を -10,000円 更新
2. BさんのDB残高を +10,000円 更新
この2つの操作の途中でエラーが起きたら、「Aさんの残高は減ったが、Bさんには届いていない」という状態になってしまいます。
トランザクションを使うと、途中でエラーが起きた場合にすべての変更を元に戻す(ロールバック)ことができます。
DB::transaction() の使い方
最もシンプルな書き方はクロージャーを渡す方法です。
// 📁 app/Http/Controllers/TransferController.php
use Illuminate\Support\Facades\DB;
public function transfer(Request $request)
{
DB::transaction(function () use ($request) {
// この中の処理がすべて成功したときだけコミットされる
$sender = User::lockForUpdate()->find($request->sender_id);
$sender->decrement('balance', $request->amount);
$receiver = User::lockForUpdate()->find($request->receiver_id);
$receiver->increment('balance', $request->amount);
// 履歴を記録
Transfer::create([
'from_user_id' => $request->sender_id,
'to_user_id' => $request->receiver_id,
'amount' => $request->amount,
]);
});
return redirect()->back()->with('success', '送金しました');
}
クロージャー内で例外が投げられると、自動でロールバックされます。
手動でコミット・ロールバックを制御する
より細かい制御が必要な場合:
// 📁 app/Services/OrderService.php
use Illuminate\Support\Facades\DB;
public function placeOrder(array $orderData): Order
{
DB::beginTransaction(); // トランザクション開始
try {
$order = Order::create($orderData);
foreach ($orderData['items'] as $item) {
$order->items()->create($item);
// 在庫を減らす
Product::find($item['product_id'])
->decrement('stock', $item['quantity']);
}
DB::commit(); // すべて成功したらコミット
return $order;
} catch (\Exception $e) {
DB::rollBack(); // 失敗したらロールバック
throw $e; // 上位に例外を再スロー
}
}
排他ロック(悲観的ロック)
同時に複数のリクエストが同じデータを変更しようとするときの競合を防ぐには、排他ロックを使います。
DB::transaction(function () use ($productId, $quantity) {
// SELECT ... FOR UPDATE → 他のトランザクションが変更できないようにロック
$product = Product::lockForUpdate()->find($productId);
if ($product->stock < $quantity) {
throw new \Exception('在庫が不足しています');
}
$product->decrement('stock', $quantity);
});
lockForUpdate() は SELECT 文に FOR UPDATE を付け、同じ行を他のトランザクションが更新・ロックしようとするとブロックします。
読み取りだけロックしたい場合(他からの更新はブロックするが、読み取りはOK):
$product = Product::sharedLock()->find($productId); // LOCK IN SHARE MODE
楽観的ロック(バージョン管理)
Laravelには組み込みの楽観的ロックはありませんが、version カラムを使ったパターンをよく使います。
public function update(Request $request, Post $post)
{
// フォームから送られたバージョンが現在と一致するか確認
if ($post->version !== $request->version) {
return back()->withErrors(['他のユーザーが変更しました。再度編集してください。']);
}
$post->update([
'title' => $request->title,
'body' => $request->body,
'version' => $post->version + 1,
]);
}
まとめ
DB::transaction()のクロージャー内でエラーが起きると自動でロールバックされる- 手動制御は
DB::beginTransaction(),DB::commit(),DB::rollBack()の3点セット lockForUpdate()で悲観的ロック(排他ロック)を設定できる- 金額操作や在庫操作など「整合性が崩れると困る処理」には必ずトランザクションを使う
次回はページネーションとEloquentのクエリスコープの応用を学びます。