在数据库开发中,有些操作需要"连带执行":比如往订单表插入一条记录时,希望商品表的库存自动减少;再比如删除一条用户数据前,希望先把这条数据备份到另一张表里。这类联动逻辑如果全部放在应用层代码里写,不仅容易遗漏,还可能在多人协作时被绕过。MySQL提供的触发器机制,就是为了解决这类问题而生的——它把联动逻辑绑定在表的事件上,只要事件发生,逻辑就一定会执行。

一、触发器的基本概念和语法
触发器(Trigger)是一种与表绑定的数据库对象,它会在指定的表上发生指定事件时被自动激活执行。MySQL支持三种触发事件:INSERT、UPDATE和DELETE,而触发时机分为两类:BEFORE(事件发生前触发)和AFTER(事件发生后触发)。两者组合起来,一张表最多可以有六种触发器。
创建触发器的基本语法如下:
DELIMITER $$
CREATE TRIGGER trigger_name
BEFORE INSERT ON table_name
FOR EACH ROW
BEGIN
-- 这里写触发器要执行的SQL逻辑
END$$
DELIMITER ;
这里有几个要点需要理解。第一,DELIMITER $$的作用是把语句结束符临时改为$$,因为触发器体内部包含分号,如果不修改结束符,MySQL会在遇到第一个分号时就认为语句结束了,导致语法错误。第二,FOR EACH ROW表示行级触发,也就是说影响的每一行都会触发一次,比如一条UPDATE语句更新了10行,触发器逻辑就会执行10次。第三,触发器体内不能使用动态SQL,也不能调用返回结果集的语句。
二、NEW和OLD关键字的使用
在触发器体内,NEW和OLD是两个非常重要的关键字,用来引用行的旧数据和新数据。它们的取值与触发事件有关,具体规则如下表所示:
| 触发事件 | OLD的含义 | NEW的含义 |
|---|---|---|
| INSERT | 不可用 | 即将插入的新行 |
| UPDATE | 更新前的旧行 | 更新后的新行 |
| DELETE | 即将删除的旧行 | 不可用 |
举个例子,假设有一张商品表goods,包含字段id、name、stock,其中stock表示库存。我们希望在订单产生时自动扣减对应商品的库存,订单表orders中记录了goods_id和购买数量num。可以这样写触发器:
-- 创建示例表
CREATE TABLE goods (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
stock INT NOT NULL DEFAULT 0
);
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
goods_id INT NOT NULL,
num INT NOT NULL
);
-- 插入订单后自动扣库存
DELIMITER $$
CREATE TRIGGER trg_after_order
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
UPDATE goods SET stock = stock - NEW.num WHERE id = NEW.goods_id;
END$$
DELIMITER ;
这段代码中,NEW.goods_id和NEW.num分别取的是刚插入的订单中的商品编号和数量。插入一条订单后,商品表对应的库存会立刻减少,应用层不需要再写任何扣库存的代码。需要注意,如果库存可能被扣成负数,可以加一层判断,或者在插入前用BEFORE触发器先校验库存是否充足。
三、BEFORE和AFTER的实际区别
BEFORE触发器在事件真正执行之前运行,此时可以修改NEW的值,也可以用SIGNAL语句主动中断操作;AFTER触发器在事件执行完成之后运行,适合做日志记录、联动更新其他表这类事情。一个简单的记忆方法是:要"修改或拦截本次操作"用BEFORE,要"事后做额外的事"用AFTER。
下面看一个BEFORE INSERT的例子,实现插入时自动补全字段。假设用户表中的用户名要求统一转为小写存储:
DELIMITER $$
CREATE TRIGGER trg_before_user_insert
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
SET NEW.username = LOWER(NEW.username);
END$$
DELIMITER ;
如果把这段逻辑放在AFTER触发器里就不行了,因为插入动作已经完成,再修改NEW已经没有任何意义。再比如数据备份的场景,删除用户前先把数据存到备份表,用BEFORE DELETE配合OLD就能实现:
DELIMITER $$
CREATE TRIGGER trg_before_user_delete
BEFORE DELETE ON users
FOR EACH ROW
BEGIN
INSERT INTO users_backup (id, username, email, deleted_at)
VALUES (OLD.id, OLD.username, OLD.email, NOW());
END$$
DELIMITER ;
这里必须用OLD而不是NEW,因为DELETE事件没有新数据,OLD中保存的正是即将被删除的那一行。
四、触发器的查看、删除和常见坑
管理触发器主要用到三条语句。查看当前数据库中所有的触发器:SHOW TRIGGERS;,也可以查information_schema库中的TRIGGERS表获取更详细的信息。删除触发器使用DROP TRIGGER IF EXISTS trigger_name;。触发器不能修改,只能删除后重建,这一点和存储过程不同。
使用触发器时还有几个常见的坑需要注意。首先是MySQL不允许同一张表的同一事件、同一时机创建多个相同的触发器,重复创建会报错。其次是触发器会隐式地让操作变慢,尤其是每行触发一次的逻辑中包含UPDATE其他表时,批量操作的耗时会明显增加。再次是触发器对错误的处理比较简单,逻辑出错时整个操作会回滚,排查问题时日志不太直观,建议触发器逻辑尽量保持轻量。最后,触发器过多会让业务逻辑变得隐蔽,后来维护的人可能不清楚哪些联动在数据库层执行,因此大型项目中通常会权衡触发器与应用层代码的边界,把核心业务逻辑放在明处。
总体来说,触发器适合处理数据一致性要求高、必须强制执行的联动逻辑,比如审计日志、自动补全、级联统计等场景。掌握好NEW和OLD的用法、BEFORE和AFTER的时机选择,再配合文中这几个实例动手练习,就能快速把触发器用起来了。