たまにトリガーが予想外の動きをすることがあるんだけど、それって例えば:
- トリガーに紐づいてる関数のロジックミス
- DB制約違反(例えばユニーク制約違反やデータ型不一致とか)
- トランザクションの問題で、トリガーがエラーで変更をロールバックしちゃう場合
- トリガーが自分自身を呼び出して再帰しちゃう(よくあるうっかりミス)
こういう問題を防ぐために、PostgreSQLではトリガーやその関数の中でエラー処理ができるようになってる。使うのはEXCEPTIONブロックとRAISEステートメント。今日はこれを例で見ていくよ。
EXCEPTIONブロックでのエラー処理
EXCEPTIONブロックを使うと、エラーをキャッチしてそれに対する処理を実行できる。PythonやJavaのtry-catchみたいな感じだね。
EXCEPTIONブロックはPL/pgSQLの関数でこんな風に使う:
BEGIN
-- 関数のメインコード
EXCEPTION
WHEN <エラータイプ> THEN
-- エラー処理のコード
END;
ここで<エラータイプ>は処理したい特定のエラーやエラーグループ(例:unique_violation、division_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のメッセージタイプ:
DEBUG— デバッグ用メッセージNOTICE— 普通の情報メッセージWARNING— 警告EXCEPTION— エラーメッセージ(関数の実行を終了させる)
RAISEの書き方:
RAISE <メッセージタイプ> 'メッセージ';
変数の値も渡せるよ:
RAISE NOTICE 'NEW.idの値 = %', NEW.id;
例:トリガーで値をデバッグする
例えば、studentsテーブルの更新でエラーが出てて、NEWやOLDの値が何か知りたいとき。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();
これで、レコード更新のたびにOLDとNEWの値が見れるし、エラーが起きたら分かりやすいメッセージも出るよ。
トリガーでのトランザクション
トリガーはトランザクションの中で実行される。つまり、トリガーやその関数の中でどこかでエラーが起きたら、トランザクション全体がロールバックされる。これで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は開発・テスト中だけ使うようにする。
パフォーマンス低下。 複雑なトリガーはINSERT、UPDATE、DELETE操作を遅くしがち。解決例:トリガーのロジックは最小限にして、重いクエリは避ける。
GO TO FULL VERSION