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

触发器这东西,说白了就是数据库里的“自动小秘书”。你给它设定好规则,一旦表里发生点什么动静——比如插入一条数据、更新某个字段、或者删掉一行记录——它就会自动跳出来干活,不需要你手动调用。

我刚开始接触触发器的时候,觉得它挺鸡肋的。心想:我直接在应用程序里写逻辑不就行了?后来在一个订单系统里栽了跟头——业务方要求“删除订单时必须同步备份到历史表”,结果开发组里三个人写了三套不同的备份逻辑,上线后漏备份了十几条关键数据。嗯,从那以后,我再也不敢小看触发器了。

13.1 什么是触发器?

触发器是一种特殊的存储过程。但它不像普通存储过程那样需要你主动调用,而是由数据库自动触发执行。它依附在某张表上,监听这张表的 INSERT、UPDATE、DELETE 操作。

触发器的核心三要素:

  • 触发事件:INSERT、UPDATE、DELETE,可以单独指定,也可以组合(比如 INSERT OR UPDATE)
  • 触发时机:BEFORE(操作前触发)或 AFTER(操作后触发)
  • 触发动作:你要执行的 SQL 语句块

核心记忆点:触发器 = 事件驱动 + 自动执行 + 事务内运行

13.2 基本语法:CREATE TRIGGER

不同数据库的触发器语法略有差异,但核心结构大同小异。我以 SQLite 为例,因为它轻量、适合嵌入式场景,而且语法最简洁明了。

CREATE TRIGGER 触发器名称
[BEFORE | AFTER] [INSERT | UPDATE | DELETE] ON 表名
[FOR EACH ROW]
BEGIN
    -- 你的 SQL 逻辑
END;

这里有个关键点:FOR EACH ROW 表示行级触发器——每一行受影响都会触发一次。如果不写,默认就是语句级触发器(整个 SQL 语句只触发一次)。在嵌入式数据库里,绝大多数场景用行级触发器。

我的习惯:给触发器命名时,我喜欢用这种格式:trg_表名_操作_时机。比如 trg_orders_after_insert,一看就知道是订单表插入后的触发器。团队协作时,这种命名能省不少沟通成本。

13.3 触发时机:BEFORE vs AFTER

这两个时机的选择,其实挺讲究的。我简单说说我的理解:

触发时机 执行顺序 典型场景
BEFORE 在 INSERT/UPDATE/DELETE 执行之前触发 数据校验、默认值填充、字段格式化
AFTER 在 INSERT/UPDATE/DELETE 执行之后触发 日志记录、同步备份、更新统计信息

BEFORE 触发器的特点:你可以在数据真正写入之前修改它。比如用户注册时,你可以在 BEFORE INSERT 触发器里把邮箱自动转成小写,或者给创建时间字段赋值。

AFTER 触发器的特点:数据已经落盘了,你不能再改它。但你可以基于这条新数据做其他事情——比如记录操作日志、更新汇总表。

我曾经踩过的坑:在 BEFORE 触发器里试图读取刚刚插入的自增主键值。这是读不到的!因为数据还没真正写入,主键还没生成。如果你需要用到新生成的主键,请用 AFTER 触发器。

13.4 触发事件:INSERT / UPDATE / DELETE

你可以让触发器监听一种操作,也可以同时监听多种。语法上直接用 OR 连接:

CREATE TRIGGER trg_orders_audit
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW
BEGIN
    -- 记录所有变更
END;

但说实话,我建议不要滥用这种“万能触发器”。为什么呢?因为不同操作需要处理的数据不一样:

  • INSERT:只有 NEW 行(新插入的数据)
  • DELETE:只有 OLD 行(被删除的数据)
  • UPDATE:既有 NEW 行(更新后的值),又有 OLD 行(更新前的值)

如果你把三种操作混在一起写,代码里全是 IF 判断,可读性会变得很差。我个人更倾向于为每种操作单独建触发器,除非逻辑真的完全一样。

13.5 实战案例:订单备份触发器

来,我们写一个完整的例子。假设有一个订单表 orders,每次删除订单时,自动把这条记录备份到 orders_archive 表。

-- 创建订单表
CREATE TABLE orders (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    product_name TEXT NOT NULL,
    quantity INTEGER NOT NULL,
    price REAL NOT NULL,
    created_at TEXT DEFAULT (datetime('now'))
);

-- 创建备份表(结构相同)
CREATE TABLE orders_archive (
    id INTEGER,
    product_name TEXT,
    quantity INTEGER,
    price REAL,
    created_at TEXT,
    deleted_at TEXT DEFAULT (datetime('now'))
);

-- 创建触发器:删除前备份
CREATE TRIGGER trg_orders_before_delete
BEFORE DELETE ON orders
FOR EACH ROW
BEGIN
    INSERT INTO orders_archive (id, product_name, quantity, price, created_at)
    VALUES (OLD.id, OLD.product_name, OLD.quantity, OLD.price, OLD.created_at);
END;

你看,这个触发器很简单。每次你执行 DELETE FROM orders WHERE id = 5 时,数据库会先执行触发器里的 INSERT,把旧数据存到归档表,然后再真正删除。整个过程在一个事务里,要么全成功,要么全回滚。

避坑指南:我曾经在生产环境遇到过一个问题——备份表里忘了加 deleted_at 字段,结果后来查历史数据时根本不知道这条记录是什么时候被删的。所以,归档表里一定要带上操作时间戳。

13.6 实战案例:自动更新时间戳

这个场景太常见了。很多表都有 updated_at 字段,每次更新记录时自动刷新。用触发器实现最靠谱:

CREATE TRIGGER trg_users_before_update
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
    SET NEW.updated_at = datetime('now');
END;

注意:SQLite 里用 SET NEW.xxx 来修改即将写入的值。MySQL 里也是类似的写法。如果你用 PostgreSQL,语法稍有不同,但思路完全一样。

你想想看,如果这个逻辑写在应用程序里,万一有另一个客户端直接连数据库改了数据,updated_at 就漏掉了。触发器是数据库层面的保障,谁也绕不过去。

13.7 实战案例:数据完整性校验

有些校验逻辑,放在应用层不够安全,放在 CHECK 约束里又不够灵活。这时候触发器就派上用场了。

我记得在一个库存管理系统里,业务要求“订单数量不能超过库存数量”。这个逻辑用 CHECK 约束写不了,因为库存数量在另一张表里。用 BEFORE INSERT 触发器就能搞定:

CREATE TRIGGER trg_orderdetails_before_insert
BEFORE INSERT ON order_details
FOR EACH ROW
BEGIN
    SELECT RAISE(ABORT, '库存不足')
    WHERE NEW.quantity > (SELECT stock FROM products WHERE id = NEW.product_id);
END;

这里用到了 RAISE(ABORT, '错误信息'),这是 SQLite 里终止操作并返回错误的方式。如果库存不够,插入操作会被直接取消,应用程序会收到一个错误。

注意:触发器里的校验逻辑一定要高效。如果校验涉及复杂的多表查询,而且表数据量很大,触发器可能会成为性能瓶颈。我曾经见过一个触发器里做了三层子查询,结果插入一条数据要等好几秒——这就不合适了。

13.8 触发器的应用场景总结

我整理了一下,触发器最适合干这几类事情:

  1. 审计日志:记录谁在什么时间改了哪条数据
  2. 数据同步:主表变更时自动更新汇总表或缓存表
  3. 数据校验:跨表的复杂约束检查
  4. 默认值/自动计算:比如自动生成编号、自动计算总价
  5. 级联操作:删除主表记录时自动清理关联数据

但触发器也不是万能的。我个人觉得,以下场景不适合用触发器:

  • 业务逻辑非常复杂,涉及外部系统调用(比如发邮件、调用 API)——触发器里做不了这些
  • 需要大量调试和频繁修改的逻辑——触发器调试起来比应用程序代码麻烦得多
  • 高并发写入场景——触发器会增加事务的执行时间

13.9 触发器的知识体系

下面这张图,是我梳理的触发器核心知识结构,你可以对照着回顾一下:

触发器 TRIGGER 触发事件 触发时机 触发动作 INSERT UPDATE DELETE BEFORE AFTER SQL 语句块 RAISE 错误 典型应用场景 审计日志 数据同步 数据校验 自动计算 级联操作

13.10 管理触发器

最后,说说怎么查看和删除触发器。这些命令虽然简单,但关键时刻能救命:

-- 查看所有触发器
SELECT name FROM sqlite_master WHERE type = 'trigger';

-- 查看某个触发器的定义
SELECT sql FROM sqlite_master WHERE name = 'trg_orders_before_delete';

-- 删除触发器
DROP TRIGGER IF EXISTS trg_orders_before_delete;

嗯,这里要注意:删除表的时候,依附在这张表上的触发器也会被自动删除。但如果你只是修改表结构(比如加个字段),触发器不会受影响,它依然正常工作。

最后说一句:触发器是个好工具,但别滥用。我的原则是——能用约束解决的不用触发器,能用应用逻辑解决的不用触发器,只有那些“必须由数据库保证”的逻辑,才交给触发器。把握好这个度,触发器会成为你数据库工具箱里的一把利器。


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