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 的触发器不支持存储过程调用,也不支持游标。复杂逻辑建议在应用层处理。
知识体系总览
下面这张图总结了触发器的核心逻辑。你可以把它当作「决策地图」——遇到问题时,对照着看就知道该用哪种触发器了。
写在最后
触发器是个好工具,但别滥用。我见过有人一个表上挂了七八个触发器,结果每次插入数据都要跑一圈,性能直接崩了。我的原则是:能用应用层逻辑解决的,就别用触发器。只有在「必须保证数据一致性」或者「需要自动响应」的场景下,才请出触发器这个杀手锏。
记住一句话:触发器是数据库的「守门员」,不是「前锋」。它负责守住底线,而不是冲锋陷阵。