【PL/SQL】SAVEPOINTを使った部分ロールバックの実装方法

【PL/SQL】SAVEPOINTを使った部分ロールバックの実装方法 PL/SQL

トランザクションの一部だけを取り消して処理を継続したい時、PL/SQLではSAVEPOINTとROLLBACK TO SAVEPOINTを組み合わせることで安全に実現できます。Oracleのトランザクションはネストできませんが、名前付きのセーブポイントを打っておくと、その地点まで部分的に巻き戻すことができます。ここでは基本構文から入れ子の扱い、例外処理テンプレート、ループ処理での実践パターン、DDLと暗黙コミットの注意点、FORALLのSAVE EXCEPTIONSとの違いまでを整理します。

SAVEPOINTの基本構文と効果

SAVEPOINTは現在のトランザクション内に「戻り先」を作る命令です。自動的にコミットは行われず、ROLLBACK TO SAVEPOINT nameでその位置まで変更を取り消します。COMMITやトランザクション終了時にはセーブポイントはすべて消えます。同名で複数回SAVEPOINTすると、より新しい位置に上書きされます。

BEGIN
  INSERT INTO t VALUES (1, 'A');
  SAVEPOINT sp1;                           -- ここが戻り先
  INSERT INTO t VALUES (2, 'B');
  ROLLBACK TO SAVEPOINT sp1;               -- (2, 'B') だけ取り消される
  INSERT INTO t VALUES (3, 'C');
  COMMIT;                                  -- (1, 'A') と (3, 'C') が確定
END;
/

部分ロールバックの基本パターン

業務処理の中で一部のデータだけが検証に失敗した場合、その対象行だけ巻き戻しつつ処理全体は継続させたいことがあります。SAVEPOINTを直前に設定し、失敗時にそこまで戻すのが最小手数です。

DECLARE
  v_ok BOOLEAN := TRUE;
BEGIN
  SAVEPOINT step_begin;
  INSERT INTO orders(id, amount) VALUES(1001, 5000);

  -- ビジネス検証など
  IF 5000 < 0 THEN
    v_ok := FALSE;
  END IF;

  IF v_ok THEN
    UPDATE accounts SET balance = balance - 5000 WHERE id = 10;
  ELSE
    ROLLBACK TO SAVEPOINT step_begin;      -- 注文の挿入だけ取り消す
  END IF;

  COMMIT;
END;
/

入れ子のセーブポイントと上書きの挙動

Oracleはネストトランザクションをサポートしませんが、セーブポイントは階層的に機能します。内側のROLLBACKは外側の変更には影響しません。同じ名前でSAVEPOINTを再実行すると、戻り先が新しい位置に置き換わる点に注意します。

BEGIN
  INSERT INTO t VALUES (1, 'A');
  SAVEPOINT p;                   -- p(1)
  INSERT INTO t VALUES (2, 'B');
  SAVEPOINT p;                   -- p(2) に上書き
  INSERT INTO t VALUES (3, 'C');
  ROLLBACK TO SAVEPOINT p;       -- (3, 'C')のみ取り消し、(1)(2)は残る
  COMMIT;
END;
/

例外処理と組み合わせるテンプレート

セーブポイントは例外節と合わせると効果的です。到達点ごとに戻り先を用意し、部分失敗はROLLBACK TO、致命的失敗は全体ROLLBACKという方針を明確にします。

DECLARE
  e_business EXCEPTION;
BEGIN
  SAVEPOINT s0;

  INSERT INTO t1 ...;  -- ステップ1
  SAVEPOINT s1;

  BEGIN
    INSERT INTO t2 ...;  -- ステップ2(失敗の可能性)
    IF some_condition THEN
      RAISE e_business;
    END IF;
  EXCEPTION
    WHEN e_business THEN
      ROLLBACK TO SAVEPOINT s1;     -- ステップ2だけ取り消し
  END;

  UPDATE t3 ...;  -- ステップ3

  COMMIT;
EXCEPTION
  WHEN OTHERS THEN
    ROLLBACK;                          -- 全体取り消し
    RAISE;
END;
/

ループ処理での実践パターン

多数レコードの一括処理では、一件のエラーで全体を中断せず、問題のある行だけ巻き戻してログに残し、次へ進めたいケースが多くあります。各反復の先頭でSAVEPOINTを取り、行単位でROLLBACK TOする構成がシンプルです。

DECLARE
  CURSOR c IS SELECT id, val FROM staging ORDER BY id;
BEGIN
  FOR r IN c LOOP
    SAVEPOINT row_begin;
    BEGIN
      INSERT INTO fact(id, val) VALUES (r.id, r.val);
      UPDATE staging SET processed = 'Y' WHERE id = r.id;
    EXCEPTION
      WHEN DUP_VAL_ON_INDEX THEN
        ROLLBACK TO SAVEPOINT row_begin;   -- この行の挿入だけ取り消し
        INSERT INTO err_log(id, reason) VALUES (r.id, 'duplicate'); -- ログだけ残す
    END;
  END LOOP;
  COMMIT;
END;
/

DDLと暗黙コミットの注意点

DDLは実行前後に暗黙コミットを発生させます。トランザクション中にCREATEやALTER、DROPを混在させると、それ以前に設定した全てのSAVEPOINTが消え、ROLLBACK TOが効かなくなります。部分ロールバックを前提にする処理ではDDLは切り離す設計が必要です。

FORALLのSAVE EXCEPTIONSとの違い

FORALLのSAVE EXCEPTIONSは「一括DMLの行別例外収集」を行う機能で、セーブポイントとは別物です。行単位で例外内容をSQL%BULK_EXCEPTIONSに蓄え、失敗行だけ再処理する手法に向きます。一方でSAVEPOINTは任意の位置に戻る命令であり、FORALLブロックの中で部分的にROLLBACK TOしてもFORALLの内部バッチには作用しません。大量処理では用途に応じて使い分けます。

ベストプラクティスと設計の勘所

セーブポイントは軽量ですが、細かく打ち過ぎると可読性が落ちます。業務上の論理単位ごとに名前を付け、戻り先を最小限に保つと保守しやすくなります。ROLLBACK TOの後は副作用のある処理順序を見直し、再実行の可否と重複の影響を明確にします。致命的例外は全体ROLLBACK、想定内のビジネス例外は部分ROLLBACKという役割分担を決め、ログと監査テーブルで経緯を残すと運用が安定します。

まとめ

SAVEPOINTは「失敗しても先へ進む」堅牢なトランザクション設計の要です。戻り先の位置付けを業務単位で整理し、例外処理と組み合わせて部分ロールバックを実装すれば、整合性を守りながら耐障害性の高いバッチやAPIを構築できます。DDLの暗黙コミットやFORALLの挙動差を理解し、適材適所で用いることが成功の近道です。