MySQL触发器是一种与表相关联的数据库对象,它会在对表执行INSERT、UPDATE或DELETE操作时自动触发并执行预设的SQL语句。借助触发器,可以把一些通用的数据校验、日志记录、关联表同步等工作从业务代码里剥离出来,让数据库自己保证一部分数据一致性。理解触发器的创建方式,是掌握这套机制的第一步。
一、触发器的基础语法
在MySQL中,创建触发器使用CREATE TRIGGER语句。最基本的语法结构如下:
CREATE TRIGGER trigger_name
trigger_time trigger_event
ON table_name
FOR EACH ROW
BEGIN
-- 触发器执行的逻辑
END;
其中,trigger_name是触发器的名字,在一个数据库中必须唯一。trigger_time可以是BEFORE或AFTER,表示是在触发事件之前还是之后执行。trigger_event则是INSERT、UPDATE或DELETE中的一种,代表具体的操作类型。table_name表示这个触发器绑定到哪张表上。
FOR EACH ROW说明触发器是行级触发,也就是每影响一行数据就会执行一次。在MySQL里只支持行级触发器,不支持语句级。BEGIN和END之间写具体的执行体,如果只有一条SQL语句,也可以省略BEGIN END,但多语句时必须保留。另外,因为触发器体里常用分号,所以需要先用DELIMITER命令修改结束符,避免和整体语句结束符冲突。
二、BEFORE与AFTER的区别及选择
BEFORE触发器在数据真正写入或修改之前运行,适合做数据校验、清洗和拦截。比如发现金额小于零,可以直接用SIGNAL语句报错阻止插入。AFTER触发器在数据变更已经生效后运行,适合做日志记录、通知其他表更新等副作用操作,因为此时原表数据已经落库,读取到的NEW值就是最终值。
DELIMITER $$
CREATE TRIGGER before_order_insert
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
IF NEW.amount <= 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '订单金额必须大于0';
END IF;
END$$
DELIMITER ;
上面这段触发器会在插入订单前检查金额,如果不合法就抛出错误,事务会回滚。与之相对,如果我们希望在订单插入成功后自动减少库存,就应该用AFTER INSERT,因为这时订单已存在,再去更新库存表更安全。
从性能角度看,BEFORE触发器如果频繁报错,能省下写盘开销;AFTER触发器由于主操作已提交,若其内部再出错,主表数据不会自动回滚(除非显式事务控制),所以写AFTER逻辑时要更谨慎。实际开发中,校验放BEFORE,同步放AFTER是较稳的搭配。
三、NEW与OLD关键字的使用
在触发器内部,可以通过NEW和OLD访问被影响行的数据。对于INSERT操作,只能用NEW,代表即将或刚刚插入的行;对于DELETE操作,只能用OLD,代表被删除的行;对于UPDATE操作,OLD是修改前的值,NEW是修改后的值。
DELIMITER $$
CREATE TRIGGER after_order_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
UPDATE inventory
SET stock = stock - NEW.quantity
WHERE product_id = NEW.product_id;
END$$
DELIMITER ;
这个AFTER INSERT触发器在新增订单后,自动按订单中的商品数量和product_id扣减库存表的stock字段。这里NEW.quantity和NEW.product_id就是新订单行里的列值。注意,在BEFORE触发器中修改NEW.col的值会直接影响最终写入的数据,例如NEW.amount = ABS(NEW.amount)可强制转正数。
使用OLD时要小心,它指向的是已经(或即将)不存在的数据。比如在BEFORE UPDATE里用OLD.status判断原状态,再决定是否允许修改,是常见的权限类逻辑。但OLD是只读的,任何赋值给OLD的写法都会报语法错误。
四、查看、删除与调试触发器
创建完触发器后,可以用SHOW TRIGGERS查看当前库的触发器列表,或用SELECT * FROM information_schema.TRIGGERS来按条件过滤。如果触发器名写错了或逻辑要调整,必须先删除再重建,MySQL不支持ALTER TRIGGER,只能DROP TRIGGER IF EXISTS name后重新CREATE。
SHOW TRIGGERS LIKE 'orders';
DROP TRIGGER IF EXISTS after_order_insert;
DELIMITER $$
CREATE TRIGGER after_order_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
INSERT INTO order_log(user_id, action)
VALUES (NEW.user_id, 'create');
END$$
DELIMITER ;
调试触发器没有应用代码那么方便,因为错误通常发生在SQL执行阶段。建议在触发器里把关键变量先写入一张调试表,或者利用SIGNAL抛出带上下文的错误信息。另外要注意,触发器可能引发递归,比如A表触发器改B表,B表触发器又改A表,这时需要检查MySQL变量max_sp_recursion_depth,并尽量让触发逻辑保持单向。
权限方面,创建触发器需要用户有TRIGGER权限,且触发器以定义者权限运行。如果定义者账号后被删除,触发器可能执行失败。因此生产环境应统一用专有账号管理触发器,并在上线前做回归测试,避免隐藏的级联写入拖慢主流程。
五、实战注意事项与总结
触发器能减少重复代码,但也容易让数据流向变得不直观。团队开发中应在文档里标清每张表挂了哪些触发器,否则后人排查为什么改一行数据却动了别的表会非常头疼。同时,复杂触发器会增加写操作延迟,高并发表要评估是否值得下沉这部分逻辑。
DELIMITER $$
CREATE TRIGGER before_user_update
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
IF NEW.email <> OLD.email THEN
SET NEW.email_verified = 0;
END IF;
END$$
DELIMITER ;
上面例子在用户修改邮箱时自动把邮箱验证状态置为未验证,是非常适合触发器的轻量业务规则。总结来说,创建MySQL触发器并不复杂:定好名称、选对时机与事件、绑定表、用NEW和OLD取数、在BEGIN END里写逻辑,最后注意DELIMITER和权限即可。把合适的逻辑放进触发器,能让系统更健壮,但务必克制使用,避免数据库承担过多业务重量。