15、触发器(Trigger)实现:CREATE TRIGGER、触发时机(BEFORE/AFTER)、触发事件(INSERT/UPDATE/DELETE)、实际应用场景

触发器这东西,说白了就是数据库里的「自动小机器人」。你给它设定好规则,一旦表里发生增删改操作,它就会自动跳出来干活。我早年做嵌入式项目时,最怕的就是数据不一致——这边插一条记录,那边忘了更新统计表,查出来的数据全是错的。后来用了触发器,这些问题基本就绝迹了。

什么是触发器?

触发器是一种特殊的存储过程。它不由程序主动调用,而是由数据库事件自动触发。你想想看,这就像给表装了个「监控探头」——只要有人动数据,它就能做出反应。

触发器的核心三要素:

  • 触发时机:BEFORE(操作前)或 AFTER(操作后)
  • 触发事件:INSERT、UPDATE、DELETE 三种操作
  • 触发动作:要执行的 SQL 语句或存储过程

核心理解:触发器不是用来查询数据的,而是用来「响应变化」的。它最适合做数据校验、自动计算、日志记录这类事情。

CREATE TRIGGER 语法

不同数据库的触发器语法略有差异,但核心结构大同小异。我以 SQLite 为例——嵌入式场景下它用得最多。下面是标准语法:

CREATE TRIGGER [IF NOT EXISTS] 触发器名称
  [BEFORE | AFTER] [INSERT | UPDATE | DELETE] ON 表名
  [FOR EACH ROW]  -- 行级触发器,每条受影响的行都会触发
  [WHEN 条件]     -- 可选,满足条件才触发
BEGIN
  -- 要执行的 SQL 语句
  -- 可以访问 OLD.列名(更新/删除前的值)
  -- 可以访问 NEW.列名(插入/更新后的值)
END;

嗯,这里要注意:FOR EACH ROW 表示行级触发器。如果不写,有些数据库默认是语句级触发器(整个语句只触发一次)。嵌入式数据库通常只支持行级触发器,我个人觉得这样更精细、更好控制。

触发时机:BEFORE vs AFTER

这个选择其实挺讲究的。我简单说说我的经验:

触发时机 适用场景 我踩过的坑
BEFORE 数据校验、默认值填充、字段格式化 曾经在 BEFORE 触发器里修改 NEW 值,结果忘了考虑已有数据,导致旧数据校验不通过
AFTER 日志记录、级联更新、统计汇总 有一次用 AFTER 做级联删除,结果触发了死循环——两个表互相删,数据库直接卡死

BEFORE 触发器:在数据真正写入磁盘之前执行。你可以在这里修改 NEW 的值,或者阻止非法数据写入。比如用户输入了负数,你可以在 BEFORE INSERT 里直接把它改成 0。

AFTER 触发器:数据已经写入成功后才执行。这时候不能再修改当前行的数据了,但可以做日志、发通知、更新其他表。我习惯把「副作用」类的操作放在 AFTER 里——数据已经落盘了,出问题也不影响主数据。

我的建议:校验类逻辑用 BEFORE,日志类逻辑用 AFTER。别混着用,否则调试起来你会想砸键盘。

触发事件:INSERT / UPDATE / DELETE

你可以让触发器只响应某一种操作,也可以组合多个事件。比如:

-- 只响应 INSERT
CREATE TRIGGER trg_after_insert
  AFTER INSERT ON 传感器数据
BEGIN
  INSERT INTO 数据日志(操作类型, 记录时间)
  VALUES ('INSERT', datetime('now'));
END;

-- 响应 UPDATE 和 DELETE(SQLite 支持用逗号分隔)
CREATE TRIGGER trg_before_update_delete
  BEFORE UPDATE OF 温度, 湿度 ON 传感器数据
  -- 注意:UPDATE OF 可以指定列,只有这些列被更新时才触发
BEGIN
  -- 这里可以写校验逻辑
  SELECT CASE
    WHEN NEW.温度 < -50 OR NEW.温度 > 150 THEN
      RAISE(ABORT, '温度超出合理范围')
  END;
END;

我遇到过一种情况:某个传感器偶尔会发送异常值(比如温度 999℃)。如果不用触发器做校验,这些脏数据就会污染整个统计报表。后来我在 BEFORE INSERT 里加了个范围检查,超出范围直接报错——干净利落。

OLD 和 NEW 的用法

这是触发器最强大的地方。你可以同时拿到「修改前的值」和「修改后的值」:

  • INSERT:只有 NEW(新插入的行)
  • DELETE:只有 OLD(被删除的行)
  • UPDATE:既有 OLD(修改前),也有 NEW(修改后)

举个例子,假设我们要记录每次修改的「变更历史」:

CREATE TRIGGER trg_audit_update
  AFTER UPDATE ON 设备信息
BEGIN
  INSERT INTO 变更日志(
    设备ID, 修改时间, 旧名称, 新名称, 旧状态, 新状态
  ) VALUES (
    OLD.设备ID,
    datetime('now'),
    OLD.设备名称,
    NEW.设备名称,
    OLD.状态,
    NEW.状态
  );
END;

你看,这样每次修改设备信息,系统都会自动记录「从什么改成了什么」。做审计追踪的时候,这招特别好用。

实际应用场景

光讲语法没意思,我分享几个真实项目里用过的场景:

场景一:自动更新时间戳

嵌入式设备里,每条记录都需要记录「最后修改时间」。手动写太容易忘了:

CREATE TRIGGER trg_update_timestamp
  BEFORE UPDATE ON 设备配置
BEGIN
  SET NEW.最后修改时间 = datetime('now');
END;

这样不管谁改数据,时间戳自动更新。省心。

场景二:库存自动扣减

仓库系统里,每次出库都要扣减库存。用触发器可以保证原子性:

CREATE TRIGGER trg_auto_deduct
  AFTER INSERT ON 出库记录
BEGIN
  UPDATE 库存表
  SET 数量 = 数量 - NEW.出库数量
  WHERE 商品ID = NEW.商品ID;
  
  -- 检查库存是否充足
  SELECT CASE
    WHEN (SELECT 数量 FROM 库存表 WHERE 商品ID = NEW.商品ID) < 0 THEN
      RAISE(ABORT, '库存不足,无法出库')
  END;
END;

我曾经在一个项目中用这个方案,替代了原本在应用层做的库存检查。结果呢?并发问题彻底消失了——因为触发器在数据库内部执行,天然就是事务安全的。

场景三:数据归档

有些历史数据需要定期清理,但直接 DELETE 又怕误删。用触发器可以自动备份:

CREATE TRIGGER trg_archive_before_delete
  BEFORE DELETE ON 日志表
BEGIN
  INSERT INTO 日志归档(日志ID, 内容, 创建时间, 删除时间)
  VALUES (OLD.日志ID, OLD.内容, OLD.创建时间, datetime('now'));
END;

这样每次删除日志,都会先备份到归档表。万一需要回溯,还能找回来。

触发器的注意事项

警告:触发器不是万能的,用不好反而会惹麻烦。

  • 避免递归触发:触发器 A 更新表 B,表 B 的触发器又更新表 A,形成死循环。我遇到过,数据库直接无响应。
  • 不要做耗时操作:触发器里别写复杂的查询或循环。它是在事务里执行的,拖久了会影响整体性能。
  • 注意调试难度:触发器是隐式执行的。出问题时,你可能根本想不到是触发器在搞鬼。建议加日志输出(如果数据库支持的话)。
  • 嵌入式环境限制:SQLite 的触发器不支持存储过程调用,也不支持游标。复杂逻辑建议在应用层处理。

知识体系总览

下面这张图总结了触发器的核心逻辑。你可以把它当作「决策地图」——遇到问题时,对照着看就知道该用哪种触发器了。

触发器知识体系 触发器 触发时机 BEFORE AFTER 触发事件 INSERT UPDATE DELETE 触发动作 SQL 语句 存储过程 典型应用场景:数据校验 · 自动计算 · 日志审计 · 级联操作 · 数据归档 ⚠ 注意事项 避免递归 · 不要耗时操作 · 注意调试难度 · 嵌入式环境限制

写在最后

触发器是个好工具,但别滥用。我见过有人一个表上挂了七八个触发器,结果每次插入数据都要跑一圈,性能直接崩了。我的原则是:能用应用层逻辑解决的,就别用触发器。只有在「必须保证数据一致性」或者「需要自动响应」的场景下,才请出触发器这个杀手锏。

记住一句话:触发器是数据库的「守门员」,不是「前锋」。它负责守住底线,而不是冲锋陷阵。


公众号:蓝海资料掘金营,微信deep3321