タイトル: ROLLBACK
SEOタイトル: SQL ROLLBACK 完全ガイド(SAVEPOINT / 暗黙コミット / Spring・Laravel)
| この記事の要点 |
|
ROLLBACK の基本
ROLLBACK は進行中のトランザクションの変更を全て破棄し、トランザクション開始前の状態に戻すコマンドです。
BEGIN; -- または START TRANSACTION
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
-- 振込先口座が存在しないと判明
ROLLBACK;
-- → 上の 2 つの UPDATE は無かったことに
-- 正常時は COMMIT
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
COMMIT;
AutoCommit と BEGIN
多くの DB はAutoCommit が ON がデフォルトです。1 つの SQL ごとに自動的にコミットされ、ROLLBACK しても何も戻りません。明示的なトランザクションには BEGIN が必要です。
| DB | AutoCommit デフォルト | トランザクション開始 |
|---|---|---|
| MySQL | ON | START TRANSACTION; または BEGIN; |
| PostgreSQL | ON (psql) | BEGIN; |
| Oracle | OFF(明示 COMMIT 必要) | 暗黙的に開始 |
| SQL Server | ON | BEGIN TRANSACTION; |
-- MySQL: AutoCommit を OFF に
SET autocommit = 0;
UPDATE users SET status = 'active' WHERE id = 1;
ROLLBACK; -- 取り消し成功
-- 確認
SELECT status FROM users WHERE id = 1; -- 元のまま
-- 元に戻す
SET autocommit = 1;
SAVEPOINT による部分ロールバック
長いトランザクションで「ここまでは戻したいが先頭までは戻したくない」場合に SAVEPOINT を使います。
BEGIN;
INSERT INTO orders (id, customer_id) VALUES (1, 100);
SAVEPOINT after_order;
INSERT INTO order_items (order_id, sku) VALUES (1, 'A');
INSERT INTO order_items (order_id, sku) VALUES (1, 'B');
SAVEPOINT after_items;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A';
-- ★ qty が足りずビジネスルール違反と判明
ROLLBACK TO SAVEPOINT after_items;
-- → inventory UPDATE のみ取り消し。orders / order_items は残る
-- ここから別の在庫処理を試す
UPDATE inventory SET qty = qty - 1 WHERE sku = 'C';
COMMIT;
暗黙コミット (DDL)
多くの DB では DDL (CREATE / ALTER / DROP) が暗黙的にコミットを発生させます。トランザクション内で DDL を実行するとそこまでが確定し、後から ROLLBACK しても戻りません。
-- MySQL の例
BEGIN;
INSERT INTO users (name) VALUES ('Alice');
CREATE TABLE logs (id INT); -- ★ ここで暗黙 COMMIT
INSERT INTO logs (id) VALUES (1);
ROLLBACK;
-- → logs テーブルは残る、INSERT (Alice) も残る、INSERT (1) のみ取り消し
-- ※ MySQL は DDL の Atomic 性が 8.0 で改善されたが、トランザクション境界は変わる
-- PostgreSQL は DDL もトランザクション可能
BEGIN;
CREATE TABLE logs (id INT);
ROLLBACK;
-- → logs テーブルは作られていない(PG の強み)
PostgreSQL は例外的に DDL もロールバック可能です。スキーママイグレーション運用で重宝されます。
Spring @Transactional でのロールバック
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void placeOrder(Order order) {
orderRepo.save(order);
inventoryService.reserve(order); // ★ ここで例外が出ると...
paymentService.charge(order);
// → save も reserve も両方ロールバック
}
}
// rollbackFor 指定の理由:
// デフォルトでは RuntimeException / Error のみロールバック対象
// checked Exception (IOException 等) はコミットされる
// → rollbackFor = Exception.class で全例外を対象に
// 部分ロールバックには Propagation.REQUIRES_NEW でサブトランザクション化
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logFailure(String msg) { ... }
// アノテーション以外で明示ロールバック
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
Laravel でのロールバック
use Illuminate\Support\Facades\DB;
// パターン1: try / catch + 手動制御
DB::beginTransaction();
try {
$order = Order::create([...]);
OrderItem::insert([...]);
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
// パターン2: クロージャ(推奨)
// 例外が出れば自動で rollBack、正常終了で commit
DB::transaction(function () {
$order = Order::create([...]);
OrderItem::insert([...]);
}, 3); // ★ デッドロック時 3 回リトライ
// 手動 SAVEPOINT 風: ネスト transaction
DB::transaction(function () {
DB::table('outer')->insert(['v' => 1]);
try {
DB::transaction(function () {
DB::table('inner')->insert(['v' => 2]);
throw new \Exception('rollback inner');
});
} catch (\Throwable $e) {
// inner だけ rollBack。outer はそのまま COMMIT
}
});
Nested Transaction (ネストトランザクション)
多くの DB は本物のネストトランザクションを持たず、内側はSAVEPOINT で代用されます。Laravel / Spring もそうです。
BEGIN; <- レベル 1
... 処理 ...
SAVEPOINT sp1; <- レベル 2 開始(ネスト風)
... 処理 ...
SAVEPOINT sp2; <- レベル 3
... 処理 ...
RELEASE SAVEPOINT sp2;
RELEASE SAVEPOINT sp1;
COMMIT;
* RELEASE SAVEPOINT は「成功確定」
* ROLLBACK TO SAVEPOINT で個別取消
* 最終的に外側で COMMIT しない限り全て一括コミット
Distributed Transaction (XA)
複数の DB / リソースを跨ぐ場合は XA (X/Open Distributed Transaction Processing) や 2 相コミットを使います。
2 相コミット (2PC):
Phase 1 (Prepare): すべての参加 RM に「準備できる?」と聞く
全員 OK → Phase 2 へ。1 つでも NG → 全員 ROLLBACK
Phase 2 (Commit) : 全員 COMMIT
どこかで通信障害 → タイムアウトで ROLLBACK
実装:
- JTA (Java Transaction API) / Atomikos / Bitronix
- MySQL XA: XA START 'gtrid'; XA END 'gtrid'; XA PREPARE 'gtrid'; XA COMMIT 'gtrid';
- マイクロサービスでは XA を避け、Saga パターンが主流
ROLLBACK が効かないケース
- AutoCommit ON のまま単発 SQL を流した → そもそもトランザクションが無い
- DDL を挟んだ → 暗黙コミットでそこまで確定
- テーブルが非トランザクションエンジン (MySQL MyISAM 等) → 全て無効
- 外部リソース (ファイル / API 呼び出し / メール送信) → DB の ROLLBACK は影響しない
- セッションが切れて Connection が閉じた → サーバ側で自動 ROLLBACK 済み
FAQ
Q: TRUNCATE はロールバックできる?
A: PostgreSQL は可能、MySQL / Oracle は不可(DDL 扱いで暗黙コミット)。大量削除なら DELETE + LIMIT バッチ + COMMIT が安全。
Q: ストアドプロシージャ内の ROLLBACK は外側のトランザクションも巻き戻す?
A: DB によります。SQL Server / Oracle は全体を巻き戻す。MySQL は DECLARE HANDLER で挙動を制御。
Q: ROLLBACK しても物理ディスクは戻る?
A: 戻ったように見えますが、内部的には UNDO ログを使った論理復元です。物理ページは断片化が残ることもあり、長期運用では VACUUM / OPTIMIZE TABLE が必要。