before触发器中校验旧值需用old伪记录,仅before update可用old与new比对,insert无old;校验须用raise(abort, 'msg')中断事务,禁用select或insert副作用;跨表查需子查询且被查字段须有索引。

BEFORE触发器里怎么拿旧值做校验
SQLite 的 BEFORE INSERT 或 BEFORE UPDATE 触发器能访问 OLD 和 NEW 伪记录,这是校验的核心。比如想禁止把 status 从 'active' 改成 'deleted',就得在 BEFORE UPDATE 里比对 OLD.status 和 NEW.status。
注意:OLD 在 INSERT 中不可用(没旧值),NEW 在 DELETE 中不可用(没新值);写错会直接报错 no such column: OLD.xxx。
- 只允许单字段校验时,用
WHEN子句最简洁,避免进 BEGIN 块 - 多条件组合校验(比如 status 变更 + amount 超限),建议用
RAISE(ABORT, '...')中断事务,不能用SELECT报错 -
RAISE(IGNORE)会静默跳过当前行(仅适用于FOR EACH ROW),但 INSERT/UPDATE 语句本身仍可能成功,容易误判
RAISE(ABORT, ...) 是唯一可靠的中断方式
触发器里没法用 RETURN 或 EXIT,唯一能终止当前 DML 操作并回滚事务的,是 RAISE(ABORT, 'msg')。它会让整个 INSERT/UPDATE/DELETE 语句失败,并返回错误信息。
常见错误是写 SELECT RAISE(...) —— SQLite 不允许在 SELECT 中调用 RAISE;也有人试 INSERT INTO error_log ... 再 RAISE,这会导致事务里混入副作用,违背原子性。
- 必须直接写
RAISE(ABORT, 'status cannot downgrade from active') - 错误消息会原样返回给应用层,建议带具体字段和约束名,方便排查
- 不要在
FOR EACH STATEMENT触发器里用 RAISE —— 它无法定位到哪一行出错,且批量操作时行为不可控
跨表校验必须用子查询,不能 JOIN
SQLite 触发器里不支持 UPDATE ... FROM 或 JOIN 语法,所以要查其他表做校验,只能靠子查询或 WITH。比如限制用户订单总额不能超信用额度,就得在 BEFORE INSERT ON orders 里写:
SELECT CASE WHEN (SELECT COALESCE(SUM(amount), 0) FROM orders WHERE user_id = NEW.user_id) + NEW.amount > (SELECT credit_limit FROM users WHERE id = NEW.user_id) THEN RAISE(ABORT, 'order amount exceeds credit limit') END;
这种写法性能敏感:如果 users 表没对 id 建主键或索引,子查询会全表扫描。
- 确保被查字段有索引,尤其是外键列
- 避免在子查询里再嵌套复杂逻辑,SQLite 解析深度有限
- 不要用
EXISTS替代数值比较——它只能判断存在性,没法算总和或取最大值
时间戳/版本号类自动字段别硬编码
像 created_at、updated_at、version 这类字段,常被误写成触发器里 UPDATE ... SET updated_at = datetime('now')。问题在于:这会触发自身再次执行(如果该表还有别的 BEFORE UPDATE 触发器),造成无限递归。
正确做法是只在 NEW.updated_at IS NULL 时赋默认值,或用 COALESCE(NEW.updated_at, datetime('now')) 在 INSERT 或显式未设值时生效。
- 所有自动填充字段都应检查
NEW.xxx IS NULL,而不是无条件覆盖 -
CURRENT_TIMESTAMP比datetime('now')更轻量,推荐优先用 - 版本号递增(如
version = OLD.version + 1)必须加WHEN NEW.version IS NULL条件,否则 UPDATE 语句带 version 字段时会被覆盖










