Day25 前半のゴール
「“INSERTしたら勝手に何かしてくれる”を安全にイメージできるようになる」
今日のテーマはトリガーです。
一言でいうと「テーブルに対する操作(INSERT / UPDATE / DELETE)をきっかけに、自動で何かをさせる仕組み」です。
前半のゴールは次のイメージを持つことです。
トリガーとは何かを、自分の言葉で説明できる。
トリガーが「いつ動くか」「何を呼ぶか」の関係を理解する。
シンプルな「監査ログ用トリガー」の例で、動きの流れを追える。
ここから、少しずつ分解していきます。
トリガーとは何か
「テーブルに“フック”を仕込んでおくイメージ」
トリガー(trigger)は、テーブルに対して仕込んでおく「フック」のようなものです。
ある操作が行われたときに、自動的に決められた処理を呼び出します。
PostgreSQL のトリガーは、ざっくりこういう構造です。
どのテーブルに
どの操作(INSERT / UPDATE / DELETE / TRUNCATE)で
いつ(BEFORE / AFTER)
どの関数(トリガー関数)を呼ぶか
を定義します。
重要なのは、「トリガー自体は“設定”であり、実際の処理は“トリガー関数”に書く」という分離です。
トリガーは「きっかけ」、トリガー関数は「中身のロジック」と覚えておくと整理しやすいです。
トリガーが役立つ典型パターン
「“毎回同じことをやるなら、DBに覚えさせる”」
トリガーがよく使われる場面を、イメージしやすい言葉で挙げてみます。
監査ログ
あるテーブルが更新されたら、「誰が・いつ・何を変えたか」を別テーブルに自動記録する。
自動補完
INSERT の前に、足りない値(作成日時・更新日時など)を自動で埋める。
整合性維持
あるテーブルが変わったら、関連する集計テーブルやキャッシュテーブルを更新する。
アプリ側で毎回同じ処理を書くより、
「DBに一度ルールとして覚えさせる」ほうが安全で漏れがない、というタイプの処理に向いています。
ただし、やりすぎると「何がどこで起きているか分からないブラックボックスDB」になるので、設計が大事です。
Day25 では、「どこまでをトリガーに任せるか」という感覚も一緒に育てていきます。
トリガーの基本構造をざっくり掴む
「トリガー関数+CREATE TRIGGER の二段構え」
トリガーを使うには、必ず二つのステップがあります。
一つ目は「トリガー関数」を作ること。
これは PL/pgSQL で書く、特殊な形の関数です。
戻り値の型が trigger で、NEW や OLD という特別なレコードを扱います。
二つ目は「CREATE TRIGGER」で、テーブルにトリガーを紐づけること。
どのタイミングで、どのトリガー関数を呼ぶかを設定します。
イメージとしては、
トリガー関数=「自動でやりたい処理の中身」
CREATE TRIGGER=「どのテーブルのどの操作をきっかけに、その処理を呼ぶか」
という役割分担です。
例題:更新履歴を残すシンプルな監査トリガー
「usersテーブルのUPDATEを、別テーブルに記録する」
具体例で流れを見てみましょう。
よくあるのが、「重要なテーブルの変更履歴を残したい」という要件です。
ここでは、users テーブルがあるとします。
CREATE TABLE users (
id bigserial PRIMARY KEY,
name text NOT NULL,
email text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
SQLこの users が更新されたときに、「更新前後の値」と「更新時刻」を audit_users というテーブルに記録したいとします。
まず、監査用テーブルを作ります。
CREATE TABLE audit_users (
id bigserial PRIMARY KEY,
user_id bigint NOT NULL,
old_name text,
old_email text,
new_name text,
new_email text,
changed_at timestamptz NOT NULL DEFAULT now()
);
SQLここに、「変更前(old_〜)」と「変更後(new_〜)」を保存するイメージです。
次に、この audit_users に書き込むトリガー関数を作ります。
トリガー関数の中身を理解する
「OLDとNEWという“特別なレコード”」
トリガー関数は、普通の PL/pgSQL 関数と少しだけ違います。
典型的な形はこうです。
CREATE OR REPLACE FUNCTION trg_users_audit()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO audit_users (
user_id,
old_name,
old_email,
new_name,
new_email,
changed_at
)
VALUES (
OLD.id,
OLD.name,
OLD.email,
NEW.name,
NEW.email,
now()
);
RETURN NEW;
END;
$$;
SQLここで重要なポイントを深掘りします。
戻り値の型が RETURNS trigger になっていること。
これが「トリガー関数ですよ」という印です。
関数の引数はありません。
代わりに、関数の中で OLD と NEW という特別なレコードが使えます。
UPDATE の場合、
OLD は「更新前の行」
NEW は「更新後の行」
を表します。
この関数では、
OLD から id, name, email を取り出して old_〜 に入れ、
NEW から name, email を取り出して new_〜 に入れ、
現在時刻を changed_at に入れて、audit_users に INSERT しています。
最後の RETURN NEW; は、「このトリガーの後にテーブルに反映される行」を返す、という意味です。
BEFORE トリガーの場合は、ここで NEW を書き換えることで、実際に保存される値を変えることもできます。
今回は AFTER トリガーを想定しているので、そのまま NEW を返しています。
CREATE TRIGGERで“いつ動かすか”を決める
「AFTER UPDATE ON users FOR EACH ROW」
トリガー関数ができたら、それをテーブルに紐づけます。
CREATE TRIGGER users_audit_trigger
AFTER UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION trg_users_audit();
SQLこの1文で、「トリガーとしての設定」が完了します。
AFTER UPDATE ON users
users テーブルに対する UPDATE が終わったあとで、このトリガーを動かす、という意味です。
FOR EACH ROW
UPDATE 文で影響を受けた「各行ごと」にトリガー関数を呼ぶ、という指定です。
例えば、1回の UPDATE で10行更新されたら、トリガー関数も10回呼ばれます。
EXECUTE FUNCTION trg_users_audit();
さっき作ったトリガー関数を呼びなさい、という指定です。
これで、「users が更新されるたびに、OLD / NEW を使って audit_users に履歴を残す」という自動処理が完成します。
実際に動くイメージを追ってみる
「UPDATE 1回で、裏側で何が起きているか」
ここまで来たら、頭の中で「1回の UPDATE で何が起きるか」をシミュレーションしてみましょう。
まず、アプリやコンソールから、普通に UPDATE を打ちます。
UPDATE users
SET name = '新しい名前'
WHERE id = 1;
SQLPostgreSQL の中では、次のような流れになります。
users テーブルの id=1 の行を探す。
その行の「更新前の値」を OLD として保持する。
更新後の行(name が変わったもの)を NEW として用意する。
UPDATE の処理が終わったタイミングで、「AFTER UPDATE ON users」のトリガーを探す。
users_audit_trigger が見つかるので、trg_users_audit() を呼び出す。
trg_users_audit の中で、OLD / NEW を使って audit_users に INSERT する。
トリガー関数が NEW を返すので、そのまま処理を完了する。
結果として、
users テーブルには新しい name が反映される。
audit_users テーブルには、「変更前と変更後の値+時刻」が1行追加される。
呼び出し側(アプリやSQLクライアント)は、UPDATE を1回打っただけです。
裏側でトリガーが静かに動いて、履歴を残してくれています。
トリガーの“気持ちよさ”と“怖さ”
「便利さとブラックボックス化の境界線」
ここまでの例を見ると、「トリガーめちゃくちゃ便利じゃん」と感じると思います。
実際、監査ログや自動補完などにはとても相性がいいです。
ただし、トリガーには「気持ちよさ」と同じくらい「怖さ」もあります。
コードを読んでいるだけでは、「裏で何のトリガーが動いているか」が見えにくい。
1つの操作が、複数のトリガーを連鎖的に呼び出すと、挙動が追いづらくなる。
パフォーマンス問題が起きたとき、「トリガーの中身」がボトルネックになっていることもある。
だから、プロレベル運用では、
どのテーブルにどんなトリガーが付いているかをドキュメント化する。
トリガーの中身はできるだけシンプルに保つ。
重い処理や外部サービス連携は、トリガーではなく別の仕組みに逃がす。
といった「設計と運用のルール」を一緒に考えます。
Day25 前半では、「トリガーは“テーブル操作にフックを仕込む仕組み”であり、トリガー関数+CREATE TRIGGER の二段構えで設計する」という構造が分かっていれば十分です。
Day25 前半のまとめ
トリガーは「特定のテーブルに対する INSERT / UPDATE / DELETE / TRUNCATE をきっかけに、自動でトリガー関数を呼び出す仕組み」であり、実体のロジックは RETURNS trigger な PL/pgSQL 関数の中に書き、CREATE TRIGGER で「どのテーブルの、どの操作の、どのタイミング(BEFORE / AFTER)で、その関数を呼ぶか」を結びつける。
UPDATE 時に OLD(更新前)と NEW(更新後)の行を使って監査テーブルに履歴を残す例のように、「毎回同じことをやりたい処理(監査ログ・自動補完・整合性維持など)」をDB側に一度覚えさせることで、アプリ側の書き忘れを防げる一方、やりすぎると“何がどこで起きているか分からないブラックボックスDB”になるため、トリガーは構造と役割を意識して慎重に設計する――ここまでの感覚が持てれば、Day25 前半としてはとても良い状態です。
