#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のクエリスコープの応用を学びます。