Menu

SQLite 触发器:CREATE TRIGGER 与 OLD/NEW 用法

SQLite 触发器如何工作:BEFORE 和 AFTER、视图上的 INSTEAD OF、OLD 和 NEW 行引用,以及什么时候适合用触发器。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

触发器会自动执行 SQL

触发器是一段存储起来的 SQL,每当某张表上发生特定事件时就会触发。你只写一次,“什么时候执行”交给 SQLite 处理。

基本形式如下:

我们从没显式地往 price_history 里 INSERT 过数据,是触发器做的。以后每一次价格更新都会以同样的方式记录下来,不管它来自命令行、脚本还是应用。

CREATE TRIGGER 的结构

逐个部分来看这个语法:

CREATE TRIGGER trigger_name
{ BEFORE | AFTER | INSTEAD OF } { INSERT | UPDATE [ OF column_list ] | DELETE }
ON table_name
[ FOR EACH ROW ]
[ WHEN condition ]
BEGIN
    -- one or more statements
END;
  • 时机:BEFORE 在修改之前执行,AFTER 在修改之后执行,INSTEAD OF 替代这次修改(仅限视图)。
  • 事件:由哪种操作触发。UPDATE OF col1, col2 把更新范围缩小到特定的列。
  • 表:被监视的那张表。
  • FOR EACH ROW:SQLite 只支持行级触发器,所以这一项是隐含的。为了清晰可以写上,但它不会改变任何行为。
  • WHEN:可选的条件。只有条件为真时,触发器主体才会执行。
  • 主体:BEGIN 和 END 之间的一条或多条语句,每条都必须以分号结尾。

这就是全部语法。大多数实际的触发器只有五到十行。

OLD 和 NEW:正在被修改的行

在主体中,有两个伪行可以让你看到数据:

  • NEW:新传入的行。在 INSERT 和 UPDATE 触发器中可用。
  • OLD:原有的行。在 UPDATE 和 DELETE 触发器中可用。

DELETE 触发器只有 OLD,INSERT 触发器只有 NEW,UPDATE 触发器两者都有。

被删除的行已经从 accounts 中消失了,但它的数据在消失之前已被记录到 deletions 里。

BEFORE:校验或调整这一行

BEFORE 触发器在行的修改写入磁盘之前执行,很适合抛出错误或规范化数据:

第二条 INSERT 在写入任何行之前就被中止了。RAISE(ABORT, '...') 会取消当前语句,并回滚到这条语句开始时的状态;RAISE(FAIL, ...)、RAISE(ROLLBACK, ...) 和 RAISE(IGNORE) 能让你更细致地控制后续行为。

如果只是单纯的数据校验,优先使用 CHECK 约束:它是声明式的,优化器也了解它。当规则需要查看其他表,或者要做 CHECK 表达不了的事情时,再用 BEFORE 触发器。

WHEN:带条件的触发器

WHEN 子句决定哪些行的修改会真正执行主体。它在 OLD 和 NEW 绑定之后、针对每一行求值:

第一笔订单没有达到条件,另外两笔达到了。如果没有 WHEN 子句,每次插入都会写进 big_orders,你就得在读取时再去过滤。

INSTEAD OF:让视图可写

视图默认是只读的。INSTEAD OF 触发器会拦截对视图的写操作,转而执行你的 SQL,通常是把它转换成对底层表的写入:

应用把这个视图当作一张表来操作,触发器在幕后负责把姓名拆分成 first_name 和 last_name。

列出和删除触发器

触发器和表、索引一起保存在 sqlite_master 中:

DROP TRIGGER IF EXISTS name; 是稳妥的写法。删除触发器所在的表时,触发器会被自动删除,不需要事先清理。

值得了解的陷阱

有几件事第一次用时很容易踩坑:

  • 触发器按行触发,而不是按语句触发。 一条修改 1,000 行的 UPDATE 会触发 1,000 次。如果主体本身开销很大,累积起来会很快。
  • 触发器在外层事务中执行。 如果外层语句回滚,触发器的写入也会一起回滚。这通常正是你想要的,但也意味着触发器不能用来实现“无论如何都要记下来”。
  • 递归触发器默认关闭。 修改同一张表的触发器不会再次触发自己,除非你设置了 PRAGMA recursive_triggers = ON;。除非有明确的理由,否则保持关闭。
  • 应用端的写入只有绕开数据库才能绕开触发器。 只要每次写入都经过 SQLite,触发器就会执行。通过原生 SQL 批量写入的 ORM 同样会触发它们。
  • 不要把业务逻辑分散到许多触发器里。 在调用处根本看不到它们,排查“这一行为什么会出现?”的人只能去 grep sqlite_master。把触发器用在横切关注点上(审计日志、派生列、视图可写),其余逻辑留在应用代码中。

一个真实的审计日志示例

把前面的模式组合起来,记录 posts 表的每一次修改:

一个触发器在同一处既保证了 updated_at 的准确,又写入了一条审计记录。执行 UPDATE 的应用代码完全不需要知道这两件事的存在。

下一步:JSON 支持

触发器负责围绕行事件的自动化。SQLite 进阶的下一部分是你能在一行里面存储什么:JSON。SQLite 提供了一整套 JSON 函数,让你不离开 SQL 就能查询和更新结构化数据,这就是下一页的内容。

常见问题

SQLite 中的触发器是什么?

触发器是一段 SQL,当表上发生特定事件(INSERT、UPDATE 或 DELETE)时会自动执行。你只需用 CREATE TRIGGER 定义一次,之后每当事件发生,SQLite 都会替你触发它。用它可以记录审计日志、维护派生列或强制执行规则,而不必依赖应用代码记得去做。

BEFORE、AFTER 和 INSTEAD OF 触发器有什么区别?

BEFORE 在行的修改生效之前执行,适合做校验或调整这一行的数据。AFTER 在修改完成之后执行,适合记录日志或同步其他表。INSTEAD OF 只能用在视图上,它会完全替代原本要执行的操作,让视图变得可写。

在触发器里如何引用正在被修改的行?

在 INSERT 和 UPDATE 中用 NEW.column 引用新传入的行,在 UPDATE 和 DELETE 中用 OLD.column 引用原有的行。INSERT 触发器只能看到 NEW,DELETE 触发器只能看到 OLD,UPDATE 触发器两者都能看到。这些引用只作用于当前正在处理的那一行。

SQLite 中如何列出或删除触发器?

触发器保存在 sqlite_master 中,SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger'; 会列出全部触发器。要删除一个,用 DROP TRIGGER trigger_name;,不确定它是否存在时用 DROP TRIGGER IF EXISTS trigger_name;。删除一张表也会同时删除它的触发器。

Coddy 编程语言插图

和 Coddy 一起学编程

开始