必须同时比对OLD.status和NEW.status,仅校验NEW.status白名单会放行非法跳转;每个合法路径需独立IF分支,禁止用NOT IN或CASE模糊匹配;拦截逻辑只能放在BEFORE UPDATE中,用SIGNAL中断,不可修改NEW字段“修复”状态。

BEFORE UPDATE 必须同时比对 OLD.status 和 NEW.status
只校验 NEW.status 是否在白名单里,等于放行所有跳转——比如 'delivered' → 'paid' 也能通过。真正起作用的是组合判断:OLD.status = 'created' 且 NEW.status NOT IN ('paid', 'cancelled') 才触发拦截。
常见错误写法:IF NEW.status NOT IN ('paid', 'shipped', 'delivered') THEN SIGNAL ...——这拦不住 'shipped' → 'created' 这种逆向操作。
- 每个合法跳转路径必须独立写
IF分支,别用CASE或NOT IN模糊匹配 -
INSERT场景下OLD.status是NULL,需单独判:例如IF OLD.status IS NULL AND NEW.status != 'created' THEN SIGNAL ... - 状态字段必须是
ENUM或带CHECK约束的VARCHAR,否则拼写错误(如'payed')会让所有判断失效
禁止在触发器里修改 NEW 字段来“修复”非法值
写 SET NEW.status = OLD.status 看似让数据“安全”,实则掩盖问题:业务层收到的是成功响应,但状态没变,下游逻辑可能卡在等待状态更新上。
更危险的是,这种写法会让非法变更静默通过,审计日志里也看不出异常——你看到的是“已更新”,实际什么都没发生。
- 触发器职责只有两个:放行或报错,不能“改写”输入
-
SIGNAL SQLSTATE '45000'是唯一正确中断方式,错误信息要明确写出跳转路径,比如'Invalid transition: paid → created (cannot unpay)' - 若
NEW.status = OLD.status,建议直接LEAVE或跳过后续校验,避免无谓开销
跨状态字段联动校验要防 NULL 和索引缺失
比如订单表要求 status = 'paid' 时 payment_id 必须非空,这个判断不能只写 IF NEW.status = 'paid' AND NEW.payment_id IS NULL THEN ...,还得考虑 OLD.status 是否本来就是 'paid'——否则 UPDATE ... SET memo = 'xxx' 也会被误拦。
真正要拦的是“从非 paid 变成 paid 但没填 payment_id”,所以条件得是:OLD.status != 'paid' AND NEW.status = 'paid' AND NEW.payment_id IS NULL。
- 所有涉及
IS NULL的判断,都要确认字段定义是否允许NULL;不允许就该设NOT NULL,而不是靠触发器补漏 - 如果校验依赖其他字段(如
parent_status),确保该字段有索引,否则每次UPDATE都全表扫 - 别在触发器里查其他表做状态联动——性能差、快照不一致、还可能死锁
AFTER 触发器无法回滚,别指望它“事后纠错”
AFTER UPDATE 已经改完数据了,此时再发现状态非法,SIGNAL 也晚了。更糟的是,如果尝试在 AFTER 里反向 UPDATE 回去,MySQL 直接报 ERROR 1442:不能在触发器里更新触发它的表。
有人想用 AFTER 发消息通知人工干预,这可以,但绝不能让它承担“纠正状态”的责任——事务一旦提交,状态就不可逆。
- 所有拦截逻辑必须放在
BEFORE UPDATE,这是唯一能原子性中断的地方 -
AFTER只适合做审计日志、统计更新、异步通知等副作用操作 - 如果业务真需要多步状态校验(比如查支付表+查库存),必须由应用层在同一个事务里完成,数据库只执行最终的
UPDATE
UPDATE orders SET status = 'completed' WHERE id = 123,发现居然绕过了所有约束,原因往往是初始状态没覆盖 NULL 或终态降级没写分支。

















