CodeGym /コース /SQL SELF /エラー処理と元の状態への戻し方: EXCEPTION, RAISE

エラー処理と元の状態への戻し方: EXCEPTION, RAISE

SQL SELF
レベル 53, レッスン 3
使用可能

PostgreSQLでは、いろんな理由でエラーが発生することがあるよ。例えば制約違反(NOT NULL、UNIQUE、CHECK)、構文ミス、重複値とかね。こういうエラーをキャッチして処理しないと、外側のトランザクション全体がロールバックされちゃう。ビジネス処理を安定させるには、ちゃんとエラー処理するのが大事だよ。

PL/pgSQLには、BEGIN ... EXCEPTION ... ENDブロックっていう強力なエラーハンドリング機構がある。関数やプロシージャでエラーをキャッチして処理できるんだ。これはPythonやJavaのtry-catchに似てるけど、PostgreSQLのトランザクションの仕組みとちょっと違う動きをするから注意してね。

重要ポイント:

それぞれのBEGIN ... EXCEPTION ... ENDブロックは「仮想的なセーブポイント」みたいなもの。もし例外が発生したら、そのブロック内の変更は自動でロールバックされる。これはPL/pgSQLの関数やプロシージャで唯一正しい部分ロールバックの方法だよ。

EXCEPTIONを使ったエラー処理の構文

BEGIN
    -- メインのコード
EXCEPTION
    WHEN エラータイプ THEN
        -- 特定のエラーの処理
    WHEN 別のエラータイプ THEN
        -- 別のエラーの処理
    WHEN OTHERS THEN
        -- その他すべてのエラーの処理
END;

関数やプロシージャでの例

DO $$
BEGIN
    RAISE NOTICE '今からエラーが発生するよ...';
    PERFORM 1 / 0;
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE 'ゼロ除算をキャッチしたよ!';
END;
$$;

例: エラー処理とロールバック付きの更新

注文テーブルがあるとする:

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    amount NUMERIC NOT NULL,
    status TEXT NOT NULL
);

注文のステータスを更新して、エラーが起きたら何も変更しない関数を作るよ:

CREATE OR REPLACE FUNCTION update_order_status(order_id INT, new_status TEXT)
RETURNS VOID AS $$
BEGIN
    BEGIN
        UPDATE orders
        SET status = new_status
        WHERE id = order_id;

        -- エラーのシミュレーション
        IF new_status = 'FAIL' THEN
            RAISE EXCEPTION 'エラーをシミュレートするよ!';
        END IF;

        RAISE NOTICE '注文ステータス % を更新した', order_id;

    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE '注文 % の更新中にエラー発生: %', order_id, SQLERRM;
            -- このブロック内の変更は自動でロールバックされる!
            -- 必要なら外側で再度エラーを投げる
            RAISE;
    END;
END;
$$ LANGUAGE plpgsql;

トランザクション制御を明示的に使うプロシージャでの動作

プロシージャ(CREATE PROCEDURE)ではCOMMIT、ROLLBACK、SAVEPOINTが使えるけど、ただしROLLBACK TO SAVEPOINTは使えない。プロシージャ内で一部だけロールバックしたいなら、やっぱりBEGIN ... EXCEPTION ... ENDを使おう:

CREATE OR REPLACE PROCEDURE pay_order(order_id INT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
    -- プロシージャ全体ではCOMMIT/ROLLBACKが使えるけど、部分的なロールバックには:
    BEGIN
        UPDATE accounts
        SET balance = balance - amount
        WHERE id = (SELECT account_id FROM orders WHERE id = order_id);

        -- エラー
        IF amount < 0 THEN
            RAISE EXCEPTION '金額はマイナスにできないよ!';
        END IF;

        UPDATE orders SET status = 'PAID' WHERE id = order_id;

    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE '注文 % の支払い処理中にエラー発生: %', order_id, SQLERRM;
            -- このブロック内の変更は自動でロールバックされる
    END;

    COMMIT; -- トランザクションを明示的に終了できるのはプロシージャだけ!
END;
$$;

エラーログの記録

エラーをキャッチするだけじゃなくて、あとで分析できるように記録しておくのも大事だよ。

CREATE TABLE error_log (
    id SERIAL PRIMARY KEY,
    order_id INT,
    error_message TEXT,
    error_time TIMESTAMP DEFAULT now()
);

関数やプロシージャ内で:

EXCEPTION
    WHEN OTHERS THEN
        INSERT INTO error_log (order_id, error_message)
        VALUES (order_id, SQLERRM);
        RAISE NOTICE 'エラーをログに記録した: %', SQLERRM;
        RAISE;

PostgreSQL 17での重要な制限と注意点

関数(CREATE FUNCTION ... )の中では、トランザクション制御コマンド(BEGIN、COMMIT、ROLLBACK、SAVEPOINT、ROLLBACK TO SAVEPOINT、RELEASE SAVEPOINT)は使えないよ! 関数は全部外側のトランザクションの中で実行される。

プロシージャ(CREATE PROCEDURE ... )の中では、SAVEPOINT、RELEASE SAVEPOINT、COMMIT、ROLLBACKが明示的に書ける。でもROLLBACK TO SAVEPOINTは禁止!(PL/pgSQLプロシージャでROLLBACK TO SAVEPOINTを使おうとすると構文エラーになるよ)

関数やプロシージャ内で「一部だけロールバック」したいときは、BEGIN ... EXCEPTION ... ENDブロックを使う。 エラーが発生したら、そのブロック内の変更は自動でロールバックされて、処理を続けられる。

プロシージャ(CREATE PROCEDURE)は関数の中やSELECTで呼び出せないよ。CALL ...コマンドで単独で実行する必要がある。

PL/pgSQLで「部分ロールバック」を実現するには?

唯一ちゃんと動く方法は、BEGIN ... EXCEPTION ... ENDブロックでエラー処理すること。このブロックは自動でセーブポイントを作って、エラーが起きたらその中だけロールバックして、他の部分には影響しないよ。

EXCEPTIONを使った例(おすすめのやり方):

CREATE OR REPLACE PROCEDURE demo_savepoint()
LANGUAGE plpgsql
AS $$
BEGIN
    -- なんかのコード
    BEGIN
        -- ここでエラーが起きてもプロシージャ全体はロールバックされず、
        -- このブロックだけロールバックされる!
        INSERT INTO demo VALUES ('bad data'); -- たぶんエラーになる
    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'エラーを処理したよ、このブロック内の変更はキャンセルされた';
    END;
    -- ここから先も処理は続く!
END;
$$;

例: 全体ロールバックを防ぐバッチデータのロード

CREATE OR REPLACE PROCEDURE load_big_batch()
LANGUAGE plpgsql
AS $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN SELECT * FROM import_table LOOP
        BEGIN
            INSERT INTO target_table (col1, col2)
            VALUES (rec.col1, rec.col2);
        EXCEPTION WHEN OTHERS THEN
            INSERT INTO import_errors (err_msg)
            VALUES ('レコードのエラー: ' || rec.col1 || ': ' || SQLERRM);
            -- このブロック内の変更はキャンセルされる!
        END;
    END LOOP;
    COMMIT;  -- プロシージャが外部トランザクションなしで実行されてる場合だけOK!
END;
$$;

-- プロシージャの呼び出し
CALL load_big_batch();

注意: もしクライアント側ですでにトランザクションが始まってる状態(例: Pythonでautocommit=False)でこのプロシージャを呼ぶと、中のCOMMITやSAVEPOINTはエラーになるよ。

ネストしたセーブポイントとEXCEPTIONのコツ

  1. PL/pgSQLでROLLBACK TO SAVEPOINTは使わないで! 構文エラーになるよ。
  2. 部分ロールバックには必ずネストしたBEGIN ... EXCEPTION ... ENDブロックを使おう。
  3. プロシージャ内のCOMMITやROLLBACKはトランザクションを再スタートさせるから、autocommitモードでプロシージャを実行してるときだけ使ってね!
  4. エラーは専用のテーブルにログを残して、不正な行の情報を失わないようにしよう。
  5. ビジネス処理を絶対に全部まとめてやりたい(オール・オア・ナッシング)場合は関数で実装して、COMMIT/ROLLBACKは中で使わない。段階的に処理したいならプロシージャでやろう。

例: 部分的な処理付きバッチインポート

CREATE OR REPLACE PROCEDURE import_batch()
LANGUAGE plpgsql
AS $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN SELECT * FROM staging_table LOOP
        BEGIN
            INSERT INTO data_table (data)
            VALUES (rec.data);
        EXCEPTION
            WHEN unique_violation THEN
                INSERT INTO import_log (msg)
                VALUES ('重複: ' || rec.data);
            WHEN OTHERS THEN
                INSERT INTO import_log (msg)
                VALUES ('エラー: ' || rec.data || ' — ' || SQLERRM);
        END;
    END LOOP;
END;
$$;

PostgreSQL 17で覚えておくべきこと:

PL/pgSQLのプロシージャではSAVEPOINT、RELEASE SAVEPOINT、COMMIT、ROLLBACKが使えるけど、ROLLBACK TO SAVEPOINTはダメ。

関数やプロシージャ内の「部分ロールバック」は、ネストしたBEGIN ... EXCEPTION ... ENDブロックだけで実現できる。

トランザクションの管理は外側(接続パラメータやautocommit)でやるのがベター。プロシージャ内ではここで説明したエラー処理の仕組みを使おう。

2
タスク
SQL SELF, レベル 53, レッスン 3
ロック未解除
EXCEPTIONの利用とロールバック
EXCEPTIONの利用とロールバック
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION