2.

SQL ROLLBACK 完全ガイド(SAVEPOINT / 暗黙コミット / Spring・Laravel)

編集
この記事の要点
  • ROLLBACK はトランザクション内の変更をすべて取り消す
  • SAVEPOINT + ROLLBACK TO SAVEPOINT で部分ロールバックが可能
  • DDL (CREATE / ALTER / DROP) は暗黙コミットを発生させる DB が多い → ROLLBACK 不能
  • 多くの DB は AutoCommit ON がデフォルト → トランザクションには BEGIN が必要
  • Spring: @Transactional(rollbackFor = Exception.class) でチェック例外もロールバック対象に
  • Laravel: DB::beginTransaction() / DB::rollBack() または DB::transaction() クロージャ

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 が必要です。

DBAutoCommit デフォルトトランザクション開始
MySQLONSTART TRANSACTION; または BEGIN;
PostgreSQLON (psql)BEGIN;
OracleOFF(明示 COMMIT 必要)暗黙的に開始
SQL ServerONBEGIN 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 が必要。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. COMMIT (トランザクション制御)
  2. ROLLBACK
  3. SAVEPOINT

最近更新/作成されたページ