CodeGym /コース /SQL SELF /トリガーでのエラー処理とトランザクション管理:EXCEPTION、RAISE

トリガーでのエラー処理とトランザクション管理:EXCEPTION、RAISE

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

たまにトリガーが予想外の動きをすることがあるんだけど、それって例えば:

  • トリガーに紐づいてる関数のロジックミス
  • DB制約違反(例えばユニーク制約違反やデータ型不一致とか)
  • トランザクションの問題で、トリガーがエラーで変更をロールバックしちゃう場合
  • トリガーが自分自身を呼び出して再帰しちゃう(よくあるうっかりミス)

こういう問題を防ぐために、PostgreSQLではトリガーやその関数の中でエラー処理ができるようになってる。使うのはEXCEPTIONブロックとRAISEステートメント。今日はこれを例で見ていくよ。

EXCEPTIONブロックでのエラー処理

EXCEPTIONブロックを使うと、エラーをキャッチしてそれに対する処理を実行できる。PythonやJavaのtry-catchみたいな感じだね。

EXCEPTIONブロックはPL/pgSQLの関数でこんな風に使う:

BEGIN
    -- 関数のメインコード
EXCEPTION
    WHEN <エラータイプ> THEN
        -- エラー処理のコード
END;

ここで<エラータイプ>は処理したい特定のエラーやエラーグループ(例:unique_violationdivision_by_zeroなど)だよ。

例:トリガーでエラーをログに記録する

例えば、logsテーブルがあって、studentsテーブルにデータ挿入時のエラーをそこに記録したいとする。こんな感じ:

ログ用テーブルを作る

CREATE TABLE logs (
    id SERIAL PRIMARY KEY,
    error_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    error_message TEXT
);

エラー処理付きの関数を作る

CREATE OR REPLACE FUNCTION track_insert_errors()
RETURNS TRIGGER AS $$
BEGIN
    -- メインのコードを試す
    BEGIN
        -- "エラーになる"例:0で割る
        PERFORM 1 / (NEW.some_value - NEW.some_value);
    EXCEPTION
        WHEN division_by_zero THEN
            -- 0で割ったエラーが起きたらログに書き込む
            INSERT INTO logs (error_message) VALUES ('studentsへの挿入時に0で割り算エラー');
    END;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

トリガーを作る

CREATE TRIGGER before_insert_students
BEFORE INSERT ON students
FOR EACH ROW
EXECUTE FUNCTION track_insert_errors();

これで、studentsテーブルにデータ挿入時に0で割るエラーが起きたら、それが処理されてlogsテーブルに記録されるよ。

RAISEでデバッグや診断をする

RAISEステートメントを使うと、警告やエラー、デバッグ用のメッセージを出せる。トリガーがどう動いてるか(あるいは動いてないか!)を知りたいとき、めっちゃ便利。

RAISEのメッセージタイプ:

  1. DEBUG — デバッグ用メッセージ
  2. NOTICE — 普通の情報メッセージ
  3. WARNING — 警告
  4. EXCEPTION — エラーメッセージ(関数の実行を終了させる)

RAISEの書き方:

RAISE <メッセージタイプ> 'メッセージ';

変数の値も渡せるよ:

RAISE NOTICE 'NEW.idの値 = %', NEW.id;

例:トリガーで値をデバッグする

例えば、studentsテーブルの更新でエラーが出てて、NEWOLDの値が何か知りたいとき。RAISEを使うとこう:

CREATE OR REPLACE FUNCTION debug_student_update()
RETURNS TRIGGER AS $$
BEGIN
    RAISE NOTICE 'OLD.id = %, NEW.id = %', OLD.id, NEW.id;

    -- エラーになる条件の例:
    IF NEW.some_field IS NULL THEN
        RAISE EXCEPTION 'some_fieldフィールドはNULLにできないよ';
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER after_update_students
AFTER UPDATE ON students
FOR EACH ROW
EXECUTE FUNCTION debug_student_update();

これで、レコード更新のたびにOLDNEWの値が見れるし、エラーが起きたら分かりやすいメッセージも出るよ。

トリガーでのトランザクション

トリガーはトランザクションの中で実行される。つまり、トリガーやその関数の中でどこかでエラーが起きたら、トランザクション全体がロールバックされる。これでDBが中途半端な状態になるのを防げるんだ。

でも、この動きが逆に困ることもある:

  • トリガー内のエラーがデータのミスだった場合、一部だけロールバックしたいこともある
  • トランザクションのロールバックはトリガーだけじゃなくて、そのトリガーを呼んだ操作全体に及ぶってことを理解しておく必要がある

例:トリガーでトランザクションを使う

例えば、ビジネスロジックでstudentsテーブルの更新とlogsへのログ記録、2つの操作をしたいとする。どっちかが失敗したら、トランザクション全体がロールバックされる。

CREATE OR REPLACE FUNCTION transactional_student_update()
RETURNS TRIGGER AS $$
BEGIN
    -- 更新試行のログ記録
    INSERT INTO logs (error_message) VALUES ('id ' || NEW.id || 'の学生を更新しようとした');

    -- ビジネス条件のチェック
    IF NEW.some_value IS NULL THEN
        RAISE EXCEPTION 'some_valueフィールドはNULLにできないよ';
    END IF;

    -- うまくいったらNEWを返す
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER before_update_students
BEFORE UPDATE ON students
FOR EACH ROW
EXECUTE FUNCTION transactional_student_update();

トリガーでよくあるミスとその防ぎ方

よくある開発者のミス:

再帰的トリガー。 これはトリガーが変更を加えて、それがまた自分自身を呼び出しちゃう場合。解決例:WHEN条件を使うか、再呼び出しを防ぐフラグを追加する。

エラーでトランザクション全体がロールバックされる。 トリガーがメインデータと直接関係ない場合、これは望ましくないことが多い。解決例:EXCEPTIONブロックをうまく使う。

デバッグ情報が多すぎる。 ログがごちゃごちゃして分析しづらくなる。解決例:RAISEは開発・テスト中だけ使うようにする。

パフォーマンス低下。 複雑なトリガーはINSERTUPDATEDELETE操作を遅くしがち。解決例:トリガーのロジックは最小限にして、重いクエリは避ける。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION