PostgreSQL 入門

PostgreSQL ROLLBACK

トランザクション(Transactions)は、データベース内のデータ整合性を維持するために極めて重要であり、特に複雑な操作を処理する際にその真価を発揮します。トランザクションを利用することで、一連のデータベース操作を「単一の論理的な作業単位」として扱うことが可能になります。つまり、トランザクション内のすべての操作が成功してデータベースに適用されるか、あるいは一つも適用されないかのどちらかになります。

ROLLBACK(ロールバック)コマンドは、まさにこの「全か無か(All or Nothing)」のメカニズムを確実にするための強力な武器です。これは現在のトランザクション中に実行されたあらゆる変更を効率的に取り消し、データベースをトランザクション開始前の完璧な状態に復元します。本章では、PostgreSQLにおける ROLLBACK の詳細について深く掘り下げます。

1. ROLLBACKとは何か

PostgreSQLにおける ROLLBACK コマンドは、現在のトランザクションを終了させ、その開始以降に行われたすべての変更を破棄するために使用されます。これを「セーフティネット」と考えてください。システムでエラーが発生した際や、変更を永久に保存すべきではないと判断した際に、データベースを安全で一貫した状態に即座に復元することができます。

1.1 構文

ROLLBACK コマンドの基本構文は非常にシンプルです:

ROLLBACK;

また、特定の セーブポイント(Savepoint) までロールバックすることも可能です(詳細は後述します):

ROLLBACK TO savepoint_name;

1.2 ROLLBACKの核心的な原則

  • 原子性 (Atomicity): ROLLBACK はトランザクションの原子性を保証します。これは、トランザクションが分割不可能な単一の実行ユニットとして扱われることを意味します。トランザクション内の変更は、すべて適用されるか(COMMIT を実行した場合)、あるいはすべて適用されないか(ROLLBACK を実行した場合)の二択となります。
  • データ整合性 (Data Consistency): トランザクションをロールバックすることで、データベースの一貫した状態を維持できます。一連の複雑な操作の途中でエラーが発生した場合、ROLLBACK は一部の更新によるデータの破損を防止します。
  • エラーハンドリング (Error Handling): ROLLBACK はデータベースアプリケーションにおけるエラーハンドリングの中核です。トランザクション実行中にエラーが検出された際、ROLLBACK を実行することで、データベースが不完全または不整合な状態で放置されるのを防ぎます。

2. ROLLBACKの実戦例

理解を深めるために、架空のオンラインショップのデータベースを例に ROLLBACK の使い方をデモンストレーションします。ユーザーの残高を管理する accounts(口座)テーブルと、製品在庫を管理する products(製品)テーブルがあると仮定します。

2.1 基本的な例:残高不足

顧客が商品を購入しようとしたが、口座残高が不足していたというシナリオを考えます。この場合、ROLLBACK を使用して取引を取り消すことができます。

まず、トランザクションを開始します:

BEGIN;

口座残高を確認します:

SELECT balance FROM accounts WHERE account_id = 123;

クエリの結果、残高が 50ドル だったとします。
購入金額(仮に 100ドル)を差し引こうと試みます:

UPDATE accounts SET balance = balance - 100 WHERE account_id = 123;

再度、残高が十分か確認します:

SELECT balance FROM accounts WHERE account_id = 123;

この時点で残高は -50ドル になっています。これはビジネスルール上、許容されません。
ロールバックを実行し、トランザクションを取り消します:

ROLLBACK;

ROLLBACK 実行後、accounts テーブルはトランザクション開始前の状態に戻ります。顧客の残高は 50ドル のまま、安全に保たれます。

2.2 複数操作の例:注文処理

注文全体を処理するなど、複数の操作が絡む複雑なシナリオを考えます。

トランザクションを開始します:

BEGIN;

顧客の口座残高を更新します:

UPDATE accounts SET balance = balance - 50 WHERE account_id = 123;

製品在庫を更新します:

UPDATE products SET quantity = quantity - 1 WHERE product_id = 456;

注文テーブルにレコードを挿入します:

INSERT INTO orders (account_id, product_id, quantity, order_date)
VALUES (123, 456, 1, NOW());

ここで、エラーが発生したとシミュレートします:

-- エラーをシミュレート:制約違反(例:order_idカラムにNOT NULL制約がある場合)
INSERT INTO shipping_queue (order_id) VALUES (NULL);

エラーが発生したため、トランザクション全体をロールバックします:

ROLLBACK;

shipping_queue テーブルへの挿入でエラーが発生したため、ROLLBACK コマンドによってそれまでに行われたすべての操作が取り消されます。口座残高は変わらず、在庫も更新されず、orders テーブルにもレコードは残りません。すべてが何事もなかったかのように復元されます。

3. セーブポイント(Savepoints)へのロールバック

セーブポイント(Savepoints) を利用すると、トランザクション全体をロールバックするのではなく、トランザクション内の特定のチェックポイントまで戻ることができます。これは複雑なトランザクションにおいて、一部の変更だけを取り消したい場合に非常に有効です。

トランザクションを開始します:

BEGIN;

顧客の口座を更新します:

UPDATE accounts SET balance = balance - 20 WHERE account_id = 123;

セーブポイントを作成します:

SAVEPOINT after_account_update;

製品在庫を更新します:

UPDATE products SET quantity = quantity - 2 WHERE product_id = 456;

エラーが発生したとシミュレートします:

-- 日付フォーマットの検証失敗などを想定したエラー
INSERT INTO delivery_schedule (order_id, delivery_date) VALUES (999, 'Invalid Date');

指定したセーブポイントまでのみロールバックします:

ROLLBACK TO after_account_update;

この場合、UPDATE products ステートメントと INSERT INTO delivery_schedule ステートメントのみが取り消されます。その前に行われた UPDATE accounts は有効なまま保持されます。その後、エラーを修正(例:有効な日付を指定)してから、在庫の更新や配送スケジュールの挿入を再試行できます。修正が完了したら、COMMIT を実行してトランザクションを確定させます。

4. 高度な例:ネストされたトランザクションと ROLLBACK

PostgreSQLは真の「ネストされたトランザクション(トランザクションの中のトランザクション)」をサポートしていませんが、セーブポイントを使用することで完璧にシミュレート可能です。これにより、大規模なトランザクション内でロールバックの挙動をきめ細かく制御できます。

メインのトランザクションを開始します:

BEGIN;

初期操作を実行します:

UPDATE accounts SET balance = balance - 10 WHERE account_id = 123;

ネストされた」トランザクションのためにセーブポイントを作成します:

SAVEPOINT nested_transaction;

ネストされた範囲内で操作を実行します:

UPDATE products SET quantity = quantity - 1 WHERE product_id = 789;

この「ネストされた」部分だけをロールバックすると判断します:

ROLLBACK TO nested_transaction;

この時点で、製品在庫の更新のみが取り消され、口座残高の更新は保持されています。

外部のメイン・トランザクションの他の操作を続行します:

UPDATE accounts SET balance = balance + 5 WHERE account_id = 456;

メイン・トランザクションを確定します:

COMMIT;