【Laravel】DB::rollbackでデータ不整合を防ぐ!安全なデータベース操作のためのトランザクション入門
はじめに
ユーザーが商品を購入したとき、「購入履歴」テーブルにレコードを追加し、次に「在庫」テーブルの数を減らす… このような一連のデータベース操作は、アプリケーション開発で頻繁に登場します。
しかし、もし購入履歴の保存には成功したのに、在庫を減らす処理でエラーが起きたらどうなるでしょうか?
購入されたはずの商品の在庫が減っていない、というデータの不整合が発生してしまいます。
今回は、このような事態を防ぎ、アプリケーションの信頼性を高めるデータベーストランザクションと、Laravelでの実装方法(特にDB::rollback
の役割)について詳しく解説します。
データベーストランザクションとは?
トランザクションとは、一言でいうと「すべての処理が成功するか、さもなければすべて失敗させるか」を保証するための仕組みです。関連する複数のデータベース操作を一つの処理グループとして扱い、そのグループ内の処理が一つでも失敗した場合、すべての処理を開始前の状態に巻き戻してくれます。
銀行の振込処理がよく例えられます。Aさんの口座からBさんの口座へ1万円を振り込む場合、
- Aさんの口座残高を1万円減らす
- Bさんの口座残高を1万円増やす
この2つの処理は必ずセットで行われなければなりません。1.だけ成功して2.で失敗すると、1万円が消えてしまいますよね。トランザクションは、このような事態が絶対に起こらないようにデータベースの整合性を保つための重要な機能です。
Laravelでのトランザクション実装
Laravelでは、DB
ファサードを使って簡単にトランザクションを実装できます。主に使うのは以下の3つのメソッドです。
-
DB::beginTransaction()
: 「ここからトランザクションを開始します」という宣言。 -
DB::commit()
: グループ内のすべての処理が成功した場合に、変更をデータベースに正式に保存(確定)します。 -
DB::rollback()
: グループ内のどこかでエラーが発生した場合に、beginTransaction
以降のすべての変更を取り消し、処理が始まる前の状態に巻き戻します。
これらのメソッドをtry...catch
構文と組み合わせるのが一般的な使い方です。
実践的なコード例
複数の商品を購入する処理を例に見てみましょう。
// app/Http/Controllers/PurchaseController.php
public function store(StorePurchaseRequest $request)
{
// ① トランザクション開始を宣言
DB::beginTransaction();
try {
// --- ここからがトランザクションの対象となる一連の処理 ---
// 1. purchasesテーブルに購入記録を作成
$purchase = Purchase::create([
'customer_id' => $request->customer_id,
'status' => $request->status,
]);
// 2. 関連する商品を中間テーブル(item_purchase)に登録
// もし、このループの途中で存在しないitem_idが渡されるなどのエラーが起きると...
foreach ($request->items as $item) {
$purchase->items()->attach($purchase->id, [
'item_id' => $item['id'],
'quantity' => $item['quantity'],
]);
}
// --- すべての処理が正常に完了したら ---
// ② 変更をデータベースに確定する
DB::commit();
return to_route('dashboard')->with('success', '購入処理が完了しました。');
} catch (\Exception $e) {
// ③ tryブロック内でエラー(例外)が発生したら、処理がここへ飛ぶ
// ④ beginTransaction以降のデータベース操作をすべて取り消す
DB::rollback();
// エラーログを記録したり、ユーザーにエラーを通知したりする
\Log::error('購入処理中にエラーが発生しました: ' . $e->getMessage());
return back()->withErrors('購入処理に失敗しました。しばらくしてから再度お試しください。');
}
}
コードの流れ
-
DB::beginTransaction()
でトランザクションを開始します。 -
try
ブロックの中で、購入記録の作成(Purchase::create
)と、関連商品の登録(attach
)という2つの重要な処理を実行します。 - もし
try
ブロック内の全ての処理が何事もなく完了すれば、DB::commit()
が呼ばれ、データベースへの変更が確定します。 - もし
try
ブロックの途中で何らかのエラー(例えば、$item['id']
が存在しない、データベースの制約違反など)が発生すると、PHPは例外(Exception
)を投げます。 - 例外が投げられると、処理は即座に
catch
ブロックに移ります。 -
catch
ブロック内でDB::rollback()
が呼ばれます。これにより、beginTransaction
以降に行われた全てのデータベース操作(この例ではPurchase::create
や成功した分のattach
)が綺麗さっぱり取り消され、データベースは処理が始まる直前の状態に戻ります。
この仕組みのおかげで、「購入記録だけが作られて、中身の商品が登録されていない」といった中途半端なデータが残ることを防げるのです。
もっと便利な DB::transaction()
クロージャ
Laravelには、try...catch
ブロックを自分で書かなくても、もっと簡潔にトランザクションを記述する方法が用意されています。それがDB::transaction()
メソッドです。
public function store(StorePurchaseRequest $request)
{
try {
DB::transaction(function () use ($request) {
// purchasesテーブルに購入記録を作成
$purchase = Purchase::create([
'customer_id' => $request->customer_id,
'status' => $request->status,
]);
// 関連する商品を中間テーブルに登録
foreach ($request->items as $item) {
$purchase->items()->attach($purchase->id, [
'item_id' => $item['id'],
'quantity' => $item['quantity'],
]);
}
});
} catch (\Exception $e) {
\Log::error('購入処理中にエラーが発生しました: ' . $e->getMessage());
return back()->withErrors('購入処理に失敗しました。');
}
return to_route('dashboard')->with('success', '購入処理が完了しました。');
}
この書き方では、クロージャ(無名関数)の中で例外が発生すればLaravelが**自動でrollback
してくれ、正常に完了すれば自動でcommit
**してくれます。コードがよりシンプルになり、commit
やrollback
の呼び出し忘れも防げるため、基本的にはこちらの利用が推奨されます。
まとめ
- トランザクションは、複数のデータベース操作を「全部成功 or 全部失敗」にするための仕組みです。
- Laravelでは
DB::beginTransaction()
で開始し、成功すればDB::commit()
、失敗すればDB::rollback()
で変更を取り消します。 -
DB::rollback()
は、データの不整合を防ぎ、アプリケーションの信頼性を保つための最後の砦です。 - 複数のテーブルにまたがる書き込み処理を行う際は、必ずトランザクションを利用する習慣をつけましょう。
安全で堅牢なアプリケーションを開発するために、トランザクションをぜひ使いこなしてください。