はい、承知いたしました。PostgreSQLのトリガーについて、基本から応用、使い方、管理方法、注意点まで、入門者にも分かりやすく詳細に解説する記事を作成します。約5000語を目指し、内容を直接記述します。
PostgreSQLトリガー入門:データベースの自動化と整合性を保つ強力な仕組み
データベースは、私たちがアプリケーションを開発する上で欠かせない要素です。データの格納、検索、更新、削除といった基本的な操作はもちろん、データの整合性を保ち、特定のイベントに対して自動的に処理を実行したいという要件も頻繁に発生します。このようなニーズに応えるための強力な仕組みの一つが、「トリガー(Trigger)」です。
PostgreSQLにおけるトリガーは、特定のテーブルで INSERT、UPDATE、DELETE、TRUNCATE といったイベントが発生した際に、自動的に定義された処理(トリガー関数)を実行させる機能です。これにより、アプリケーションコードから独立してデータベースレベルでの制御が可能になり、データの整合性維持、ログ記録、複雑なビジネスルールの適用などを効率的に実現できます。
この記事では、PostgreSQLのトリガーについて、その基本的な考え方から、作成方法、トリガー関数内で使用できる特殊変数、タイミングや粒度の違い、実際の使用例、管理方法、そして使用する上での注意点やベストプラクティスまでを、入門者の方にも分かりやすく詳細に解説していきます。
1. トリガーとは何か? なぜ使うのか?
トリガーは、データベースシステムにおけるイベント駆動型の仕組みです。特定のイベント(例えば、あるテーブルに行が追加された)が発生した際に、「引き金(トリガー)」が引かれ、あらかじめ定義しておいた一連の処理が自動的に実行されます。
トリガーを構成する要素:
- イベント (Event): トリガーが発動するきっかけとなる操作です。主に
INSERT、UPDATE、DELETE、TRUNCATEがあります。 - テーブル/ビュー (Table/View): イベントが発生する対象です。トリガーはこのテーブルやビューに対して定義されます。
- トリガー関数 (Trigger Function): イベント発生時に実行される処理の本体です。PL/pgSQLなどの手続き言語で記述されます。
- タイミング (Timing): イベントの「前 (BEFORE)」、「後 (AFTER)」、または特定の操作の「代わり (INSTEAD OF)」にトリガー関数を実行するかを指定します。
- 粒度 (Granularity): トリガー関数をイベントによって影響を受ける「各行 (ROW)」に対して実行するか、「ステートメント全体 (STATEMENT)」に対して一度だけ実行するかを指定します。
- 条件 (Condition – Optional): トリガー関数を実行するかどうかの条件を指定できます。指定しない場合は、イベントが発生するたびにトリガー関数が実行されます。
トリガーを使う主な目的:
- データの整合性維持: アプリケーションコードでチェックするだけでなく、データベースレベルで常にデータの整合性を強制したい場合に有効です。例えば、あるカラムの値に基づいて別のカラムの値を自動的に設定したり、不正なデータの挿入や更新を防いだりします。
- 関連データの自動更新: あるテーブルのデータが変更されたときに、それに関連する別のテーブルのデータを自動的に更新する場合に使用します。例えば、注文テーブルに行が追加されたら、在庫テーブルの数量を減らすといった処理です。
- ログ記録: データの変更履歴(いつ、誰が、どのようにデータを変更したか)を自動的に別のテーブルに記録する場合に利用します。
- 複雑なビジネスルールの適用: アプリケーションコードでは管理しきれない、またはデータベースレベルで一元管理したい複雑なビジネスルールを実装します。
- ビューに対する更新操作: PostgreSQLのビューは通常、複雑な構造(JOINなど)を持つ場合、直接
INSERT、UPDATE、DELETEできません。INSTEAD OFトリガーを使用することで、ビューに対するこれらの操作を、基になるテーブルへの適切な操作に変換して実行できます。
トリガーを適切に使用することで、データベース操作の自動化と、アプリケーションコードのシンプル化、そしてデータの信頼性向上に貢献できます。しかし、トリガーはデータベースのバックグラウンドで自動実行されるため、その動作を理解し、適切に管理することが重要です。
2. トリガーの基本的な構成要素と作成方法
PostgreSQLでトリガーを作成するには、主に以下の2つのステップが必要です。
- トリガー関数の作成: トリガーが発動したときに実行される処理(ビジネスロジック)を記述した関数を作成します。
- トリガーの定義: 作成したトリガー関数を、特定のテーブルの特定のイベントに関連付けます。
これらのステップで使用する基本的な構文を見ていきましょう。
2.1. トリガー関数の作成 (CREATE FUNCTION)
トリガー関数は、通常の関数とは異なり、特定の戻り値型 (TRIGGER または EVENT_TRIGGER ですが、テーブルトリガーの場合は常に TRIGGER) を持ち、トリガーシステムから呼び出される際に特定のコンテキスト情報を受け取ります。
最も一般的なトリガー関数の記述には、PostgreSQLで広く使われている手続き言語である PL/pgSQL を使用します。
基本的なPL/pgSQLトリガー関数の構文は以下のようになります。
“`sql
CREATE OR REPLACE FUNCTION function_name()
RETURNS TRIGGER AS $$
DECLARE
— 変数の宣言(必要に応じて)
BEGIN
— ここにトリガーで実行したい処理を記述する
— 例: データの検証、変更、ログ記録など
-- 重要な戻り値:
-- BEFORE ROW トリガーの場合:
-- RETURN NEW; -- 変更後の行を処理に進める
-- RETURN OLD; -- 変更前の行を処理に進める(ほぼ使わない)
-- RETURN NULL; -- その行に対する操作をキャンセルする
-- AFTER ROW トリガーの場合:
-- RETURN NEW; または RETURN OLD; -- いずれでも良い (AFTERトリガーの戻り値は無視される)
-- INSTEAD OF ROW トリガーの場合:
-- RETURN NEW; または RETURN OLD; -- いずれでも良い (処理の成功を示す)
-- STATEMENT トリガーの場合:
-- RETURN NULL; -- STATEMENTトリガーの戻り値は常に無視されるため NULL を返すのが慣例
END;
$$ LANGUAGE plpgsql;
“`
CREATE OR REPLACE FUNCTION function_name():function_nameという名前の関数を作成または置き換えます。引数は持ちません。RETURNS TRIGGER: この関数がトリガー関数であることを示します。AS $$ ... $$ LANGUAGE plpgsql;: 関数の本体をPL/pgSQLで記述します。
関数本体内では、トリガーのコンテキストに関する特別な変数にアクセスできます。これらは、トリガーがどの操作(INSERT、UPDATE、DELETE)で発動したか、影響を受ける行のデータ(変更前と変更後)、対象のテーブル名などの情報を提供します。これらの特殊変数については、後ほど詳しく解説します。
2.2. トリガーの定義 (CREATE TRIGGER)
トリガー関数を作成したら、次に CREATE TRIGGER ステートメントを使って、どのテーブルで、どのようなイベントに対して、いつ(タイミング)、どの粒度で、どのトリガー関数を実行するかを定義します。
基本的な CREATE TRIGGER 構文は以下のようになります。
sql
CREATE TRIGGER trigger_name
{ BEFORE | AFTER | INSTEAD OF } -- タイミング
{ INSERT | UPDATE [ OF column_name [, ...] ] | DELETE | TRUNCATE } -- イベント
ON table_name -- 対象テーブル
[ FROM referenced_table_name ] -- UPDATE OF 句で使用
[ FOR EACH { ROW | STATEMENT } ] -- 粒度
[ WHEN ( condition ) ] -- 条件(省略可能)
EXECUTE FUNCTION function_name( arguments ); -- 実行するトリガー関数と引数(関数に引数を渡す場合)
CREATE TRIGGER trigger_name: 作成するトリガーの名前を指定します。データベース内で一意である必要があります。{ BEFORE | AFTER | INSTEAD OF }:BEFORE: イベント(INSERT、UPDATE、DELETE)が実行される前にトリガー関数が実行されます。BEFORE ROWトリガーでは、影響を受ける行のデータを変更したり、操作そのものをキャンセルしたりできます。AFTER: イベントが実行された後(ただし、トランザクションがコミットされる前)にトリガー関数が実行されます。データのログ記録や、関連テーブルの更新などに使用されます。イベントの完了後に実行されるため、影響を受けた行の最終的な状態(AFTER ROW)や、ステートメントによって影響を受けた行数などの情報を利用できますが、操作そのものを変更したりキャンセルしたりはできません。INSTEAD OF: 主にビューに対して使用され、ビューに対するINSERT、UPDATE、DELETE操作の代わりにトリガー関数が実行されます。これにより、複雑なビューに対する更新操作を定義できます。テーブルに対してINSTEAD OFトリガーは定義できません。
{ INSERT | UPDATE [ OF column_name [, ...] ] | DELETE | TRUNCATE }: トリガーが発動するイベントを指定します。INSERT: 行が挿入されたとき。UPDATE: 行が更新されたとき。OF column_name [, ...]を指定すると、特定のカラムが更新された場合にのみトリガーが発動します。DELETE: 行が削除されたとき。TRUNCATE: テーブルの内容がすべて削除されたとき(DELETEと異なり、通常ログは記録されず、高速です)。TRUNCATEイベントはBEFOREまたはAFTERのSTATEMENTトリガーでのみ有効です。
ON table_name: トリガーを定義する対象のテーブルまたはビューの名前を指定します。[ FROM referenced_table_name ]:UPDATE OF句と組み合わせて使用されることがありますが、PostgreSQLの標準的なトリガーではあまり一般的ではありません。[ FOR EACH { ROW | STATEMENT } ]:FOR EACH ROW: イベントによって影響を受ける各行ごとにトリガー関数が実行されます。行レベルのトリガーでは、処理対象の行のデータ(変更前/後)にアクセスできます。最も一般的なトリガーの粒度です。FOR EACH STATEMENT: イベントを実行するステートメント全体に対して、トリガー関数が一度だけ実行されます。例えば、INSERT INTO my_table ...という一つのINSERTステートメントが複数の行を挿入した場合でも、トリガー関数は挿入が完了する前に一度(BEFOREの場合)、または完了した後に一度(AFTERの場合)だけ実行されます。行レベルのデータには直接アクセスできませんが、ステートメント全体のコンテキスト(影響を受けた行数など)を利用できる場合があります。
[ WHEN ( condition ) ]: これはFOR EACH ROWトリガーでのみ指定可能なオプションです。指定されたconditionがtrueと評価された場合にのみ、その行に対してトリガー関数が実行されます。条件内ではNEWおよびOLD特殊変数を使用できます。トリガー関数内でIF文を使って条件分岐するよりも、ここで条件を指定する方が効率的です。EXECUTE FUNCTION function_name( arguments );: イベント発生時に実行するトリガー関数の名前を指定します。関数名に括弧()を付ける必要があります。関数が引数を取る場合は、括弧内に引数を指定します。トリガー関数には、後述する特殊変数を通じて様々な情報が自動的に渡されるため、通常は引数を指定する必要はありません。
作成ステップの例:
従業員の給与が更新された際に、変更履歴をログテーブルに記録するトリガーを作成してみましょう。
まず、従業員テーブルと給与ログテーブルを作成します。
“`sql
— 従業員テーブル
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
salary DECIMAL(10, 2) NOT NULL,
hire_date DATE
);
— 給与ログテーブル
CREATE TABLE salary_logs (
log_id SERIAL PRIMARY KEY,
employee_id INTEGER,
old_salary DECIMAL(10, 2),
new_salary DECIMAL(10, 2),
change_date TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
“`
次に、給与更新時にログを記録するトリガー関数を作成します。
“`sql
— 給与更新ログ記録トリガー関数
CREATE OR REPLACE FUNCTION log_salary_update()
RETURNS TRIGGER AS $$
BEGIN
— UPDATE 操作かつ給与が変更された場合のみログを記録
— OLD は変更前の行データ、NEW は変更後の行データ
IF TG_OP = ‘UPDATE’ AND OLD.salary IS DISTINCT FROM NEW.salary THEN
INSERT INTO salary_logs (employee_id, old_salary, new_salary)
VALUES (OLD.id, OLD.salary, NEW.salary);
END IF;
-- AFTER ROW トリガーなので、元の操作に影響を与えず、常に NEW か OLD を返す(戻り値は無視されるが慣例)
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
“`
最後に、このトリガー関数を employees テーブルの UPDATE イベントに関連付けるトリガーを定義します。給与が更新された「後」で、各「行」に対して発動させたいので、AFTER UPDATE FOR EACH ROW とします。
sql
-- employees テーブルにトリガーを定義
CREATE TRIGGER employee_salary_update_trigger
AFTER UPDATE ON employees
FOR EACH ROW
EXECUTE FUNCTION log_salary_update();
これで、employees テーブルのいずれかの行が更新され、かつ salary カラムの値が変更された場合に、自動的に log_salary_update 関数が実行され、salary_logs テーブルにログが記録されるようになります。
試しにデータを挿入し、更新してみましょう。
“`sql
— データの挿入
INSERT INTO employees (name, salary, hire_date) VALUES (‘Alice’, 50000.00, ‘2020-01-15’);
INSERT INTO employees (name, salary, hire_date) VALUES (‘Bob’, 60000.00, ‘2019-05-20’);
— データの更新 (給与変更なし) – トリガーは発動しない(WHEN句や関数内のIFで判定)
UPDATE employees SET name = ‘Alicia’ WHERE name = ‘Alice’; — salary 変更なし
— データの更新 (給与変更あり) – トリガーが発動する
UPDATE employees SET salary = 55000.00 WHERE name = ‘Alicia’; — salary 変更あり
— salary_logs テーブルの内容を確認
SELECT * FROM salary_logs;
“`
上記の例では、最初の UPDATE では salary が変わらないためトリガー関数内の IF 条件を満たさずログは記録されません。2番目の UPDATE では salary が変わるため IF 条件を満たし、salary_logs に1行が挿入されます。
3. トリガー関数内で使用できる特殊変数
トリガー関数内で、トリガーが発動したときのコンテキスト情報を取得するために、PostgreSQLはいくつかの特別な変数を自動的に提供します。これらの変数はPL/pgSQL関数内で TG_ というプレフィックスでアクセスできます。最も重要なものを以下に示します。
NEW:INSERTまたはUPDATE操作によって新しく挿入される、または変更後の行全体のデータを含む特殊変数です。これはRECORD型です。BEFORE ROWまたはINSTEAD OF ROWトリガー関数内でNEW.column_nameの形式でカラムの値にアクセスしたり、その値を変更したりできます。AFTER ROWトリガーではNEWの値を変更しても効果はありません。DELETEトリガーではNEWはNULLです。OLD:UPDATEまたはDELETE操作によって変更される前、または削除される行全体のデータを含む特殊変数です。これもRECORD型です。BEFORE ROWまたはAFTER ROWのUPDATEおよびDELETEトリガー関数内でOLD.column_nameの形式でカラムの値にアクセスできます。INSERTトリガーではOLDはNULLです。TG_OP: トリガーを発動させた操作(イベント)を示す文字列です。値は'INSERT'、'UPDATE'、'DELETE'、または'TRUNCATE'のいずれかです。トリガー関数内で、どの操作で呼び出されたかに応じて処理を分岐させるために使用します。TG_WHEN: トリガーが発動したタイミングを示す文字列です。値は'BEFORE'、'AFTER'、または'INSTEAD OF'のいずれかです。TG_LEVEL: トリガーの粒度を示す文字列です。値は'ROW'または'STATEMENT'のいずれかです。TG_TABLE_NAME: トリガーが定義されているテーブルまたはビューの名前を示す文字列です。TG_TABLE_SCHEMA: トリガーが定義されているテーブルまたはビューが含まれるスキーマの名前を示す文字列です。TG_ARGV:CREATE TRIGGERステートメントでトリガー関数に渡された引数の配列(文字列の配列)です。引数が指定されていない場合は空の配列です。引数はTG_ARGV[0],TG_ARGV[1]のようにアクセスできます。
これらの特殊変数を使用することで、トリガー関数は実行時の状況に応じて柔軟な処理を行うことができます。
特殊変数の使用例(PL/pgSQL関数内):
“`sql
— 例: ログ関数の一部
BEGIN
— どの操作でトリガーが発動したか確認
RAISE NOTICE ‘Trigger fired by operation: %’, TG_OP; — デバッグ用
-- 行レベルかステートメントレベルか確認
RAISE NOTICE 'Trigger level: %', TG_LEVEL;
-- BEFORE ROW トリガーの場合の例
IF TG_WHEN = 'BEFORE' AND TG_LEVEL = 'ROW' THEN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
-- 新しい/更新後の行データ (NEW) にアクセス・変更
NEW.update_timestamp = CURRENT_TIMESTAMP; -- タイムスタンプカラムを自動更新
IF NEW.salary < 0 THEN
RAISE EXCEPTION 'Salary cannot be negative'; -- エラーを発生させて操作をキャンセル
END IF;
ELSIF TG_OP = 'DELETE' THEN
-- 削除される前の行データ (OLD) にアクセス
RAISE NOTICE 'Deleting row with id: %', OLD.id;
END IF;
END IF;
-- AFTER ROW トリガーの場合の例
IF TG_WHEN = 'AFTER' AND TG_LEVEL = 'ROW' THEN
IF TG_OP = 'UPDATE' THEN
-- 変更前 (OLD) と変更後 (NEW) のデータにアクセス
RAISE NOTICE 'Row % updated. Salary changed from % to %.', OLD.id, OLD.salary, NEW.salary;
-- ログ記録などの副作用を実行
END IF;
END IF;
-- BEFORE/AFTER STATEMENT トリガーの場合の例
IF TG_LEVEL = 'STATEMENT' THEN
IF TG_WHEN = 'BEFORE' AND TG_OP = 'INSERT' THEN
RAISE NOTICE 'Starting a bulk insert operation on table %', TG_TABLE_NAME;
ELSIF TG_WHEN = 'AFTER' AND TG_OP = 'DELETE' THEN
-- pg_trigger_depth() などでトランザクション深度を確認することも可能
RAISE NOTICE 'Finished a delete operation on table %. Check pg_stat_all_tables for row count.', TG_TABLE_NAME;
END IF;
-- STATEMENTトリガーは行データ (OLD, NEW) に直接アクセスできない
-- ただし、TG_OP, TG_WHEN, TG_TABLE_NAME などは使用可能
END IF;
-- トリガー関数の戻り値
IF TG_WHEN = 'BEFORE' AND TG_LEVEL = 'ROW' THEN
-- BEFORE ROW トリガーでは NEW, OLD, または NULL を返す
RETURN NEW; -- 通常は変更後の NEW を返す
ELSE
-- その他のトリガー (AFTER, INSTEAD OF, STATEMENT) では NEW または OLD を返す(戻り値は無視されるが慣例)
RETURN NULL; -- または RETURN NEW;
END IF;
END;
“`
この例は様々な特殊変数の使い方を示していますが、実際のトリガー関数は特定の目的のために記述されるため、これほど多くの変数や条件分岐を含むことは少ないでしょう。重要なのは、TG_OP や TG_WHEN、TG_LEVEL を使ってトリガーが発動した状況を判断し、NEW や OLD を使って影響を受ける行のデータにアクセス(または変更)することです。
トリガー関数の戻り値の重要性
特に BEFORE ROW トリガー関数における戻り値は、データベース操作の挙動に大きな影響を与えます。
RETURN NEW;: トリガー関数によって変更された可能性のあるNEW行が、そのままデータベース操作(INSERTまたはUPDATE)に使用されます。これが最も一般的な戻り値です。RETURN OLD;:UPDATE操作の場合にのみ意味を持ちます。元のOLD行がデータベース操作に使用されます。これはトリガー関数内でのNEW行の変更を無視したい場合に理論上使用できますが、通常はNEW行を返すべきであり、ほとんど使われません。RETURN NULL;: その行に対する操作(INSERT、UPDATE、DELETE)を完全にキャンセルします。INSERTの場合は行が挿入されず、UPDATEの場合は行が更新されず、DELETEの場合は行が削除されません。他の行に対する操作は影響を受けません(ただし、STATEMENTトリガーがキャンセルされない限り)。データ検証でエラーを検出した場合などに使用します。
AFTER トリガー、INSTEAD OF トリガー、および STATEMENT トリガーの戻り値は、システムによって無視されます。慣例として AFTER ROW および INSTEAD OF ROW トリガーは NEW または OLD を返し、STATEMENT トリガーは NULL を返します。
4. トリガーのタイミング (BEFORE, AFTER, INSTEAD OF) と粒度 (ROW, STATEMENT) の詳細
トリガーを定義する際に指定する「タイミング」と「粒度」は、トリガーの動作方法を決定する非常に重要な要素です。
4.1. タイミング (BEFORE, AFTER, INSTEAD OF)
-
BEFOREトリガー:- イベント(
INSERT、UPDATE、DELETE)が実行される直前に発動します。 BEFORE ROWトリガーは、処理対象の行データが実際に書き込まれる前にアクセスできます。BEFORE ROWトリガー関数内では、NEW行のデータを変更できます。例えば、挿入/更新前にカラムの値を正規化したり、デフォルト値を設定したりできます。BEFORE ROWトリガー関数がNULLを返すと、その行に対する操作(INSERT、UPDATE、DELETE)はキャンセルされます。これは、データの検証を行い、不正な操作を防ぐのに非常に強力です。BEFORE STATEMENTトリガーは、ステートメント全体が始まる前に一度だけ発動します。行データにはアクセスできませんが、ステートメント全体のコンテキスト(誰が、いつ操作を実行しようとしているかなど)を利用できます。- 制約(
CHECK制約や外部キー制約など)は、通常BEFORE ROWトリガーが実行された後に評価されます。
- イベント(
-
AFTERトリガー:- イベント(
INSERT、UPDATE、DELETE、TRUNCATE)が実行された後に発動します。ただし、トリガー関数は、操作が含まれるトランザクションがコミットされる前に実行されます。 AFTER ROWトリガーは、影響を受ける行がデータベースに書き込まれた後に発動します。OLDとNEWデータにアクセスできますが、これらの値を変更してもデータベースには反映されません。AFTER ROWトリガーは、データの変更履歴を記録したり、関連するテーブルのデータを更新したりといった、「副作用」を引き起こすのに適しています。AFTER STATEMENTトリガーは、ステートメント全体が完了した後に一度だけ発動します。行データにはアクセスできませんが、ステートメントによって影響を受けた行数などの情報を利用できる場合があります(例えば、GET DIAGNOSTICS var = ROW_COUNT;など)。AFTERトリガーは、BEFOREトリガーや制約のチェックがすべて完了した後に実行されます。
- イベント(
-
INSTEAD OFトリガー:- ビューに対してのみ定義可能です。
- ビューに対する
INSERT、UPDATE、DELETEステートメントの代わりにトリガー関数が実行されます。 - トリガー関数内で、基になるテーブルに対する適切な操作(
INSERT、UPDATE、DELETE)を記述することで、ビューに対する更新操作を実現します。 INSTEAD OFトリガーは常にROWレベルで、BEFOREやAFTERの概念はありません。
4.2. 粒度 (ROW, STATEMENT)
-
FOR EACH ROW(行レベルトリガー):- イベントによって影響を受ける各行ごとにトリガー関数が実行されます。
- 例えば、10行を
INSERTする1つのステートメントを実行した場合、FOR EACH ROWトリガーは10回発動します。 - トリガー関数内で
OLDとNEW特殊変数を使用して、処理対象の行のデータにアクセスできます。 BEFORE ROWトリガーではNEWの値を変更したり、NULLを返して操作をキャンセルしたりできます。- データ検証、行ごとの計算、行ごとのログ記録など、個々の行のデータに基づいて処理を行う場合に主に使用されます。
TRUNCATEイベントでは使用できません。
-
FOR EACH STATEMENT(ステートメントレベルトリガー):- イベントを実行するステートメント全体に対して、トリガー関数が一度だけ実行されます。
- 例えば、10行を
INSERTする1つのステートメントを実行した場合、FOR EACH STATEMENTトリガーは挿入の前または後に一度だけ発動します。 - トリガー関数内で行データ (
OLD,NEW) に直接アクセスすることはできません。 - テーブル全体の操作に関連する処理(例えば、テーブルが更新されたこと自体のログ記録、ステートメント全体の開始/終了処理など)に使用されます。
TRUNCATEイベントに対してトリガーを定義できるのは、STATEMENTレベルのみです。
4.3. タイミングと粒度の組み合わせ
これらの組み合わせによって、トリガーの振る舞いが決まります。一般的な組み合わせと用途は以下の通りです。
-
BEFORE ROW INSERT / UPDATE:- 用途: 挿入/更新される行のデータの検証、正規化、自動設定(デフォルト値など)。不正な行の挿入/更新のキャンセル。
- アクセス可能な特殊変数:
NEW(変更・検証可能),OLD(INSERTの場合はNULL) - 戻り値:
NEW(変更後の行を反映),OLD(稀),NULL(操作をキャンセル)
-
BEFORE ROW DELETE:- 用途: 削除される行のデータの検証。特定の条件を満たす行の削除のキャンセル。削除前に関連データを記録。
- アクセス可能な特殊変数:
OLD(検証可能),NEW(NULL) - 戻り値:
OLD(無視されるが慣例),NULL(操作をキャンセル)
-
AFTER ROW INSERT / UPDATE / DELETE:- 用途: データの変更履歴のログ記録。関連テーブルの集計値の更新。他のシステムへの通知(関数内で外部サービスを呼び出す場合など)。
- アクセス可能な特殊変数:
OLD,NEW(変更は反映されない) - 戻り値:
NEWまたはOLD(無視される)
-
BEFORE STATEMENT INSERT / UPDATE / DELETE / TRUNCATE:- 用途: 操作の開始前に、特定の権限チェックや状態チェックを行う。大規模操作の開始を記録。
- アクセス可能な特殊変数: なし (
OLD,NEWはNULL)。TG_OP,TG_WHEN,TG_LEVEL,TG_TABLE_NAMEなどは使用可能。 - 戻り値:
NULL(無視される)
-
AFTER STATEMENT INSERT / UPDATE / DELETE / TRUNCATE:- 用途: 操作の完了後に、操作の成功や影響行数などを記録。テーブル全体の統計情報の更新。
- アクセス可能な特殊変数: なし (
OLD,NEWはNULL)。TG_OP,TG_WHEN,TG_LEVEL,TG_TABLE_NAMEなどは使用可能。影響行数はGET DIAGNOSTICSで取得可能。 - 戻り値:
NULL(無視される)
-
INSTEAD OF ROW INSERT / UPDATE / DELETE(ビューのみ):- 用途: ビューへの更新操作を、基になるテーブルへの適切な操作に変換。
- アクセス可能な特殊変数:
OLD,NEW - 戻り値:
NEWまたはOLD(処理の成功を示す)
どのタイミングと粒度を選択するかは、トリガーで実現したい処理の内容に依存します。データを変更する可能性がある場合は BEFORE ROW、副作用やログ記録の場合は AFTER ROW、操作全体に関する処理の場合は STATEMENT を検討するのが一般的です。
5. より実践的なトリガーの使用例
いくつかの一般的なトリガーの使用例をコードとともに見ていきましょう。
例1:データ検証と正規化 (BEFORE ROW INSERT/UPDATE)
商品の価格が常に正であり、かつ小数点以下が2桁になるように正規化するトリガー。
“`sql
— 商品テーブル
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
— 価格検証・正規化トリガー関数
CREATE OR REPLACE FUNCTION validate_and_normalize_price()
RETURNS TRIGGER AS $$
BEGIN
— 価格が負でないことをチェック
IF NEW.price < 0 THEN
RAISE EXCEPTION ‘Product price cannot be negative.’;
END IF;
-- 価格を小数点以下2桁に丸める(すでにDECIMAL(10,2)なので通常不要だが、念のため)
-- NEW.price = ROUND(NEW.price, 2); -- DECIMAL型は自動的にスケール調整されることが多い
-- トリガー関数によって変更された NEW 行を返す
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
— products テーブルにトリガーを定義
CREATE TRIGGER products_price_check_trigger
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION validate_and_normalize_price();
— 動作確認
INSERT INTO products (name, price) VALUES (‘Laptop’, 1200.55); — OK
INSERT INTO products (name, price) VALUES (‘Keyboard’, 75); — OK (75.00として挿入される)
— INSERT INTO products (name, price) VALUES (‘Mouse’, -25.00); — エラー発生 Expected: Product price cannot be negative.
UPDATE products SET price = 1300.991 WHERE name = ‘Laptop’; — OK (1300.99として更新される)
UPDATE products SET price = -10 WHERE name = ‘Keyboard’; — エラー発生 Expected: Product price cannot be negative.
SELECT * FROM products;
“`
この例では、BEFORE ROW INSERT OR UPDATE トリガーを使用しています。NEW.price < 0 の条件が満たされた場合に RAISE EXCEPTION を発行し、操作全体をロールバックさせています。これにより、データベースレベルで不正な価格のデータ挿入/更新を防ぐことができます。NEW.price = ROUND(NEW.price, 2) のような処理は、BEFORE ROW トリガーで NEW 行の値を変更する典型的な例です。
例2:データの自動設定 (BEFORE ROW INSERT)
新しいユーザーが登録された際に、登録日時を自動的に設定するトリガー。(デフォルト値でも可能ですが、複雑な設定や複数カラムの連動設定などにトリガーが使えます)
“`sql
— ユーザーテーブル
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100),
registration_date TIMESTAMP WITH TIME ZONE
);
— 登録日時自動設定トリガー関数
CREATE OR REPLACE FUNCTION set_registration_date()
RETURNS TRIGGER AS $$
BEGIN
— NEW 行の registration_date が NULL の場合、現在日時を設定
IF NEW.registration_date IS NULL THEN
NEW.registration_date = CURRENT_TIMESTAMP;
END IF;
-- 変更された NEW 行を返す
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
— users テーブルにトリガーを定義
CREATE TRIGGER users_set_registration_date_trigger
BEFORE INSERT ON users
FOR EACH ROW
EXECUTE FUNCTION set_registration_date();
— 動作確認
INSERT INTO users (username, email) VALUES (‘john_doe’, ‘[email protected]’); — registration_date は NULL で挿入
INSERT INTO users (username, email, registration_date) VALUES (‘jane_smith’, ‘[email protected]’, ‘2023-01-01 10:00:00+09’); — registration_date を指定して挿入
SELECT * FROM users; — john_doe の registration_date が自動設定されていることを確認
“`
この例では、BEFORE INSERT FOR EACH ROW トリガーを使用して、挿入される行の registration_date カラムに値を自動設定しています。NEW.registration_date IS NULL の条件で、もしアプリケーション側で日時が指定されていなければ現在日時をセットするというロジックも可能です。
例3:関連テーブルの集計値更新 (AFTER ROW INSERT/UPDATE/DELETE)
注文が追加、更新、削除された際に、顧客の合計注文金額を更新するトリガー。
“`sql
— 顧客テーブル
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
total_order_amount DECIMAL(10, 2) DEFAULT 0.00
);
— 注文テーブル
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
amount DECIMAL(10, 2) NOT NULL,
order_date DATE
);
— 顧客の合計注文金額更新トリガー関数
CREATE OR REPLACE FUNCTION update_customer_total_order_amount()
RETURNS TRIGGER AS $$
BEGIN
— TG_OP は AFTER トリガーでも参照可能
IF TG_OP = ‘INSERT’ THEN
— 新しい注文が追加された場合
UPDATE customers
SET total_order_amount = total_order_amount + NEW.amount
WHERE id = NEW.customer_id;
ELSIF TG_OP = ‘UPDATE’ THEN
— 注文が更新された場合 (金額または顧客IDが変更された可能性)
— 元の顧客から古い金額を減算
UPDATE customers
SET total_order_amount = total_order_amount – OLD.amount
WHERE id = OLD.customer_id;
— 新しい顧客(または同じ顧客)に新しい金額を加算
UPDATE customers
SET total_order_amount = total_order_amount + NEW.amount
WHERE id = NEW.customer_id;
ELSIF TG_OP = ‘DELETE’ THEN
— 注文が削除された場合
UPDATE customers
SET total_order_amount = total_order_amount – OLD.amount
WHERE id = OLD.customer_id;
END IF;
-- AFTER ROW トリガーなので戻り値は無視されるが、慣例として何かを返す
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
— orders テーブルにトリガーを定義
CREATE TRIGGER orders_total_amount_update_trigger
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW
EXECUTE FUNCTION update_customer_total_order_amount();
— 動作確認
INSERT INTO customers (name) VALUES (‘Alice’); — id=1
INSERT INTO customers (name) VALUES (‘Bob’); — id=2
INSERT INTO orders (customer_id, amount, order_date) VALUES (1, 100.00, ‘2023-10-01’);
— customers (id=1) の total_order_amount が 100.00 になる
INSERT INTO orders (customer_id, amount, order_date) VALUES (1, 50.00, ‘2023-10-05’);
— customers (id=1) の total_order_amount が 150.00 になる
INSERT INTO orders (customer_id, amount, order_date) VALUES (2, 200.00, ‘2023-10-10’);
— customers (id=2) の total_order_amount が 200.00 になる
UPDATE orders SET amount = 120.00 WHERE id = 1; — Alice の最初の注文を更新 (100 -> 120)
— customers (id=1) の total_order_amount が 150 – 100 + 120 = 170.00 になる
DELETE FROM orders WHERE id = 2; — Alice の2番目の注文を削除
— customers (id=1) の total_order_amount が 170 – 50 = 120.00 になる
SELECT * FROM customers;
SELECT * FROM orders;
“`
この例では、AFTER ROW トリガーを使用して、orders テーブルの変更に連動して customers テーブルの集計値を更新しています。TG_OP を確認することで、INSERT、UPDATE、DELETE のそれぞれに対応したロジックを記述しています。UPDATE の場合は、OLD と NEW の両方の情報を使って、元の金額を減らし、新しい金額を加えるという処理が必要です。もし顧客IDも変更される可能性がある場合は、それも考慮に入れる必要があります(上記の例では OLD.customer_id と NEW.customer_id を使って対応しています)。
例4:INTO OF トリガー (ビューに対する更新)
複数のテーブルを結合したビューに対して、挿入操作を可能にする INSTEAD OF トリガー。
“`sql
— 部署テーブル
CREATE TABLE departments (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
— 従業員テーブル (department_id を追加)
CREATE TABLE employees_with_dept (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
salary DECIMAL(10, 2) NOT NULL,
department_id INTEGER REFERENCES departments(id)
);
— 部署名を含む従業員ビュー
CREATE VIEW employee_details AS
SELECT
e.id,
e.name,
e.salary,
d.name AS department_name
FROM employees_with_dept e
JOIN departments d ON e.department_id = d.id;
— ビューに対する INSERT 操作を処理するトリガー関数
CREATE OR REPLACE FUNCTION insert_employee_details()
RETURNS TRIGGER AS $$
DECLARE
dept_id INTEGER;
BEGIN
— 部署名 (NEW.department_name) から部署IDを取得
SELECT id INTO dept_id
FROM departments
WHERE name = NEW.department_name;
-- 部署が見つからない場合はエラー
IF dept_id IS NULL THEN
RAISE EXCEPTION 'Department "%" not found.', NEW.department_name;
END IF;
-- 基になる employees_with_dept テーブルに挿入
INSERT INTO employees_with_dept (name, salary, department_id)
VALUES (NEW.name, NEW.salary, dept_id);
-- INSTEAD OF トリガーは NEW または OLD を返す
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
— ビューに INSTEAD OF INSERT トリガーを定義
CREATE TRIGGER employee_details_insert_trigger
INSTEAD OF INSERT ON employee_details
FOR EACH ROW
EXECUTE FUNCTION insert_employee_details();
— 動作確認
INSERT INTO departments (name) VALUES (‘Sales’); — id=1
INSERT INTO departments (name) VALUES (‘IT’); — id=2
— ビューに対して INSERT を実行
INSERT INTO employee_details (name, salary, department_name) VALUES (‘Charlie’, 70000.00, ‘IT’);
— INSERT INTO employee_details (name, salary, department_name) VALUES (‘David’, 65000.00, ‘Marketing’); — ‘Marketing’ 部署がないためエラー発生
— 基のテーブルとビューの内容を確認
SELECT * FROM employees_with_dept;
SELECT * FROM employee_details; — Charlie が IT 部署として表示されることを確認
“`
この例では、employees_with_dept と departments テーブルを結合した employee_details ビューを作成し、そのビューに対する INSTEAD OF INSERT トリガーを定義しています。トリガー関数 insert_employee_details は、ビューに挿入されようとしているデータ (NEW) を受け取り、部署名から部署IDを検索し、最終的に基になる employees_with_dept テーブルにデータを挿入しています。これにより、ビューに対する INSERT 操作が可能になります。UPDATE や DELETE に対しても同様に INSTEAD OF トリガーを定義することで、ビューの更新を実装できます。
6. トリガーの管理
トリガーを作成した後の管理方法についても理解しておきましょう。
6.1. トリガーの一覧表示
特定のテーブルに定義されているトリガーや、データベース全体のトリガー情報を確認する方法です。
- 特定のテーブルのトリガー: psql コマンドで
\d table_nameを実行すると、テーブル定義の一部としてトリガー情報が表示されます。
sql
\d employees -
データベース全体のトリガー情報:
pg_triggerシステムカタログビューから情報を取得できます。
“`sql
SELECT
tgname, — トリガー名
relname, — 対象テーブル名
tgfoid::regproc, — 実行関数名
tgtype — トリガーのタイプ (数値フラグ、後述)
FROM pg_trigger t
JOIN pg_class c ON t.tgrelid = c.oid
WHERE tgisinternal = false; — 内部的に使用されるトリガーを除く— tgtype のフラグの意味 (例: 16 = BEFORE, 2 = INSERT, 4 = UPDATE, 8 = DELETE, 1 = ROW, 32 = STATEMENT)
— 例: tgtype & 16 > 0 は BEFORE トリガー
— より詳細な情報を見るには pg_get_triggerdef() を使用
SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgisinternal = false;
“`
6.2. トリガーの有効化/無効化
一時的にトリガーの実行を停止したい場合があります。そのような場合は、トリガーを削除するのではなく、有効/無効を切り替えることができます。
“`sql
— 特定のトリガーを無効にする
ALTER TABLE table_name DISABLE TRIGGER trigger_name;
— 特定のトリガーを有効にする
ALTER TABLE table_name ENABLE TRIGGER trigger_name;
— 特定のテーブルのすべてのユーザー定義トリガーを無効にする
ALTER TABLE table_name DISABLE TRIGGER USER;
— 特定のテーブルのすべてのユーザー定義トリガーを有効にする
ALTER TABLE table_name ENABLE TRIGGER USER;
— 特定のテーブルのすべてのトリガー (制約トリガーを含む) を無効にする
ALTER TABLE table_name DISABLE TRIGGER ALL;
— 特定のテーブルのすべてのトリガーを有効にする
ALTER TABLE table_name ENABLE TRIGGER ALL;
“`
DISABLE/ENABLE TRIGGER ALL は、ユーザーが CREATE TRIGGER で作成したトリガーだけでなく、外部キー制約や一意性制約、CHECK 制約などを実装するためにPostgreSQLが内部的に使用するトリガー(Constraint Triggers)も無効化します。これを誤って使うとデータ整合性が損なわれる可能性があるため、通常は DISABLE/ENABLE TRIGGER USER を使用するのが安全です。
6.3. トリガーの削除
不要になったトリガーは削除できます。
sql
DROP TRIGGER trigger_name ON table_name;
トリガーを削除しても、関連付けられていたトリガー関数はそのまま残ります。トリガー関数も不要な場合は、別途 DROP FUNCTION で削除する必要があります。
sql
DROP FUNCTION function_name(); -- 引数リストは不要、RETURNS TRIGGER も不要
7. トリガーを使用する上での注意点とベストプラクティス
トリガーは強力な機能ですが、その性質上、安易な使用はパフォーマンス問題やデバッグの困難さを招く可能性があります。以下の注意点とベストプラクティスを考慮して使用しましょう。
- 過度なトリガーの使用は避ける: トリガーは各データ操作に対して追加の処理を実行するため、多すぎるトリガーや、トリガー関数内で複雑/時間のかかる処理を行うと、データベース全体のパフォーマンスが低下します。特に
FOR EACH ROWトリガーは、大量のデータ操作時に繰り返し実行されるため、処理内容には十分注意が必要です。 - トリガー関数内の処理をシンプルに保つ: トリガー関数は可能な限り高速に実行されるように設計すべきです。時間のかかる処理(例: 大量のデータ集計、外部サービスの呼び出しなど)は、トリガー関数内ではなく、別の方法(バッチ処理、キューイングなど)で実現することを検討してください。
- 循環トリガーに注意: トリガー関数があるテーブルを変更し、その変更が別のトリガーを発動させ、それがさらに別のトリガーを… と連鎖していく中で、最終的に元のトリガー自身、またはすでに発動済みのトリガーを再び発動させてしまう「循環トリガー」が発生しないように設計します。PostgreSQLはトリガーの再帰深度を制限していますが、意図しない無限ループやエラーを引き起こす可能性があります。
pg_trigger_depth()関数を使って現在のトリガー呼び出し深度を確認することもできます。 - デバッグが難しい: トリガーはバックグラウンドで自動実行されるため、問題が発生した場合の原因特定(デバッグ)が難しいことがあります。トリガー関数内で
RAISE NOTICEやRAISE WARNINGを使用してログを出力したり、テスト環境で十分に検証したりすることが重要です。 - エラーハンドリング: トリガー関数内でエラー(
RAISE EXCEPTION)を発生させると、そのエラーを捕捉しない限り、トリガーを発動させた元の操作全体がロールバックされます。意図しないデータ損失を防ぐため、トリガー関数のロジックは慎重に記述し、エラーが発生しうる箇所を特定しておく必要があります。 - 代替手段の検討: トリガーで実現しようとしていることが、よりシンプルな他の手段(例えば、
DEFAULT値、CHECK制約、外部キー制約、またはアプリケーションコードでのバリデーションや関連データ更新)で実現できないか検討します。トリガーは最後の手段、あるいは他の方法では実現困難な場合に使うべき強力なツールと考えるのが良いでしょう。 - トランザクションとの関連: トリガーは、発動元のステートメントと同じトランザクション内で実行されます。トリガー関数内で実行された処理(
INSERT、UPDATE、DELETEなど)も、元の操作と同様にトランザクションの一部として扱われ、ロールバックの対象となります。AFTERトリガーで発生したエラーは、元の操作が成功した後でも全体のトランザクションをロールバックさせます。 WHEN句の活用:FOR EACH ROWトリガーの場合、トリガー関数内でIF文を使って条件分岐するよりも、CREATE TRIGGERステートメントのWHEN句で条件を指定する方が効率的です。WHEN句の条件が満たされない場合、トリガー関数自体が呼び出されないため、オーバーヘッドを減らすことができます。
トリガーはデータベース設計において非常に有用なツールですが、その特性をよく理解し、適切に計画、実装、管理することが成功の鍵となります。
8. まとめ
この記事では、PostgreSQLのトリガーについて、その基本的な概念から始まり、トリガー関数とトリガー定義の作成方法、PL/pgSQL関数内で使用できる特殊変数、トリガーのタイミング (BEFORE, AFTER, INSTEAD OF) と粒度 (ROW, STATEMENT) の違い、実践的な使用例、そしてトリガーの管理方法と使用上の注意点について詳細に解説しました。
トリガーは、データの自動化、整合性維持、複雑なビジネスルールの実装など、様々な場面でその能力を発揮します。しかし、そのパワーゆえに、不注意な使用はパフォーマンス問題やデバッグの困難さを招く可能性もあります。トリガーの仕組みを深く理解し、適切な設計のもとで使用することが重要です。
この記事が、PostgreSQLトリガーを学び始める方々にとって、その基本をしっかりと把握し、自信を持って実際の開発に活用するための手助けとなれば幸いです。ぜひ、実際に簡単なトリガーを作成してみて、その動作を確認しながら理解を深めてください。