Menu

SQLiteトリガー入門:CREATE TRIGGERとOLD/NEWの使い方

SQLiteのトリガーの仕組みを解説。BEFORE/AFTERの違い、ビューに使うINSTEAD OF、OLDとNEWで行を参照する方法、そしてトリガーが本当に役立つ場面まで一気に整理します。

このページのコードはエディタで実行できます — 編集してすぐに結果を確認できます。

トリガーで SQL を自動実行する

SQLite のトリガー(trigger)は、特定のテーブルで特定のイベントが起きたときに自動で走る SQL のかたまりです。一度書いておけば、「いつ実行するか」は SQLite が面倒を見てくれます。

基本の書き方はこんな感じです。

一度も price_history に対して明示的な INSERT を書いていないのに、トリガーが代わりに記録してくれました。今後は CLI からでもスクリプトからでもアプリからでも、価格を更新するたびに同じように履歴が残ります。

CREATE TRIGGER の構文を分解する

sqlite トリガーの構文を、パーツごとに見ていきましょう。

CREATE TRIGGER trigger_name
{ BEFORE | AFTER | INSTEAD OF } { INSERT | UPDATE [ OF column_list ] | DELETE }
ON table_name
[ FOR EACH ROW ]
[ WHEN condition ]
BEGIN
    -- 1つ以上のステートメント
END;
  • タイミングBEFORE は変更前、AFTER は変更後、INSTEAD OF は変更そのものを置き換えます(ビュー専用)。
  • イベント — トリガーを発火させる操作です。UPDATE OF col1, col2 と書けば、特定の列の更新だけに絞り込めます。
  • テーブル — 監視対象のテーブルです。
  • FOR EACH ROW — SQLite は行単位のトリガーしかサポートしていないため、これは暗黙的に適用されます。明示的に書いても構いませんが、挙動は変わりません。
  • WHEN — 任意の条件式です。これが真のときだけトリガー本体が実行されます。
  • 本体BEGINEND の間に書く 1 つ以上の文です。それぞれセミコロンで終える必要があります。

文法はこれだけです。実用的なトリガーはたいてい 5〜10 行で収まります。

OLD と NEW:変更対象の行を参照する

トリガー本体の中では、2 つの擬似的な行を通して対象データを参照できます。

  • NEW — 新しく入ってくる行。INSERTUPDATE のトリガーで使えます。
  • OLD — 既存の行。UPDATEDELETE のトリガーで使えます。

DELETE トリガーでは OLD のみ、INSERT トリガーでは NEW のみが使えます。UPDATE トリガーでは両方が利用可能です。

accountsからは行が消えていますが、消える直前のデータがdeletionsにしっかり残っています。

BEFORE トリガー:行を検証・補正する

BEFOREトリガーは、行の変更がディスクに書き込まれる前に発火します。エラーを発生させたり、データを正規化したりするのに便利です。

2 つ目の INSERT は、行が 1 件も書き込まれる前に中断されます。RAISE(ABORT, '...') は現在のステートメントをキャンセルして、その開始時点までロールバックします。挙動をもっと細かく制御したい場合は、RAISE(FAIL, ...)RAISE(ROLLBACK, ...)RAISE(IGNORE) を使い分けられます。

純粋なデータ検証であれば、CHECK 制約のほうがおすすめです。宣言的に書けますし、オプティマイザにも認識されます。BEFORE トリガーの出番は、他のテーブルを参照する必要があるときや、CHECK では表現できないルールを書きたいときです。

WHEN 句で条件付きトリガーを作る

WHEN 句を使うと、どの行の変更でトリガー本体を発火させるかをフィルタリングできます。判定は行ごとに、OLDNEW がバインドされた後に評価されます。

最初の注文は条件を満たしません。残り2件は通過します。WHEN句を付けないと、すべてのインサートがbig_ordersに書き込まれてしまい、読み出し側で絞り込む羽目になります。

INSTEAD OF トリガー:ビューに書き込めるようにする

ビューはデフォルトでは読み取り専用です。そこでINSTEAD OFトリガーの出番です。ビューへの書き込みを横取りして、代わりに自前のSQLを実行します。たいていは元のテーブルへの書き込みに変換する形になります。

アプリケーションからは普通のテーブルのように View にアクセスするだけで、first_namelast_name への分割はトリガーが裏で面倒を見てくれます。

トリガー一覧の確認と削除

トリガーはテーブルやインデックスと同じく sqlite_master に格納されています。

DROP TRIGGER IF EXISTS name; が安全な書き方です。トリガーが紐づいているテーブルを削除すれば、トリガーも自動的に消えるので、わざわざ先に片付ける必要はありません。

ハマりやすいポイント

最初にやらかしがちな落とし穴をいくつか挙げておきます。

  • トリガーは行単位で発火する(文単位ではない)。 1,000 行を対象にした UPDATE なら、トリガーは 1,000 回走ります。本体の処理が重いと、コストはあっという間に膨らみます。
  • トリガーは外側のトランザクションの中で動く。 外側の文がロールバックされれば、トリガーが書いた内容も一緒に巻き戻ります。普通はそれで都合がいいのですが、「何があってもログは残したい」という用途には使えないということです。
  • 再帰的トリガーはデフォルトでオフ。 同じテーブルを書き換えるトリガーは、PRAGMA recursive_triggers = ON; を指定しない限り自分自身を再発火させません。明確な理由がない限りオフのままにしておきましょう。
  • アプリ側の書き込みからすり抜けるには、DB を経由しないしかない。 SQLite を通る書き込みである以上、トリガーは必ず走ります。生 SQL でバッチ処理する ORM 経由でも発火します。
  • 業務ロジックを大量のトリガーに分散させない。 呼び出し側からは見えないので、「なんでこの行ができたんだ?」とデバッグする人は sqlite_master を grep する羽目になります。監査ログ、派生カラム、ビューへの書き込み対応といった横断的な用途に絞り、それ以外はアプリ側のコードに残しましょう。

実践的な監査ログの例

ここまでのパターンを組み合わせて、posts テーブルへの変更をすべて記録してみます。

トリガーを1つ仕込んでおけば、updated_at の更新と監査ログの書き込みを一か所にまとめられます。UPDATE を実行するアプリ側のコードは、その存在をまったく意識する必要がありません。

次回: JSON サポート

トリガーは行に対するイベントまわりの自動化を担うものでした。次に紹介する SQLite の応用機能は、行の 中身 に何を入れられるか、つまり JSON です。SQLite には JSON 用の関数が一通り揃っていて、SQL の世界から出ることなく構造化データを検索・更新できます。詳しくは次のページで見ていきましょう。

よくある質問

SQLiteのトリガーとは何ですか?

トリガーは、テーブルに対して特定のイベント(INSERTUPDATEDELETE)が起きたときに自動で実行されるSQLのかたまりです。CREATE TRIGGERで一度定義しておけば、あとはイベントが発生するたびにSQLite側が勝手に発火してくれます。監査ログを残したり、派生カラムを最新状態に保ったり、アプリ側に頼らずDBレベルでルールを徹底させたいときに便利です。

BEFORE・AFTER・INSTEAD OFトリガーは何が違うのですか?

BEFOREは行が変更される前に走るので、バリデーションや値の整形に向いています。AFTERは変更が確定したあとに走るので、ログ出力や別テーブルへの同期に使います。INSTEAD OFはビュー専用で、本来の操作を完全に置き換えてしまうのが特徴です。これを使うと、通常は更新できないビューに対しても書き込みができるようになります。

トリガーの中で変更対象の行を参照するにはどうしますか?

INSERTUPDATEでは新しい行をNEW.columnで、UPDATEDELETEでは既存の行をOLD.columnで参照します。つまりINSERTトリガーではNEWだけ、DELETEトリガーではOLDだけ、UPDATEトリガーでは両方が使えるという仕組みです。これらの参照は、いま処理中の1行に対してのみ有効です。

SQLiteでトリガーの一覧表示や削除はどうやりますか?

トリガーの定義はsqlite_masterに格納されているので、SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger';で一覧を取得できます。削除はDROP TRIGGER trigger_name;、存在するか自信がないときはDROP TRIGGER IF EXISTS trigger_name;でOKです。なお、テーブルをDROPすると、それに紐づくトリガーも一緒に消えます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める