应选BEFORE UPDATE触发器拦截非法变更:可安全比对OLD/NEW字段、用SIGNAL强制中止并回滚事务;AFTER中报错无法回滚已落盘数据,且不能修改NEW值。

BEFORE UPDATE 是唯一能真正拦截的时机
用 AFTER UPDATE 触发器报错毫无意义——数据早已落盘,报错只是“通知失败”,无法回滚。MySQL 的 BEFORE UPDATE 才是在写入前的最后一道闸口,此时可安全读取 OLD.column 和 NEW.column 做比对。
必须注意:NEW.id != OLD.id 这类判断在字段为 NULL 时会返回 NULL(不是 TRUE 或 FALSE),导致校验失效。稳妥写法是:(NEW.id IS NULL) != (OLD.id IS NULL) OR NEW.id != OLD.id。
别在触发器里写 UPDATE 语句,否则会递归调用自己;也别试图用 SET NEW.amount = ABS(NEW.amount) 自动修正值——掩盖变更比明确拒绝更危险。
用 SIGNAL 抛出自定义错误才能中断执行
SIGNAL SQLSTATE '45000' 是 MySQL 中唯一能强制中止当前语句并回滚事务的方式。其他方式如 SELECT 1/0 或访问不存在的表,属于“绕路抛错”,行为不可控且难维护。
常见错误写法包括:
- 漏写
DELIMITER $$和结尾的$$,导致语法解析失败 - 在
SIGNAL后加END IF;却忘了前面的IF有对应END IF; - 错误使用
=当赋值(应为:=),但这是赋值场景,SIGNAL本身不涉及赋值
示例中禁止改 created_at 的完整片段:
DELIMITER $$
CREATE TRIGGER before_users_update BEFORE UPDATE ON users FOR EACH ROW
BEGIN
IF (NEW.created_at IS NULL) != (OLD.created_at IS NULL) OR NEW.created_at != OLD.created_at THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'created_at is immutable';
END IF;
END$$
DELIMITER ;哪些字段值得拦截?优先盯三类高危变更
不是所有列都需要校验,重点守住业务语义上“不该变”的字段:
-
不可变标识:如
id、invoice_no、tenant_id(租户隔离关键) -
状态机越界:比如
OLD.status = 'shipped'时,NEW.status只允许为'delivered'或'returned',其他一律SIGNAL -
数值合理性:金额小数位超标(可用
CHAR_LENGTH(SUBSTRING_INDEX(CAST(NEW.amount AS CHAR), '.', -1)) > 2判断)、余额变动超单日阈值等
避免在触发器里查其他表或调存储函数——这会拖慢所有 UPDATE,且可能因权限或死锁导致静默失败。
上线前必须验证所有写入口是否都被覆盖
触发器只对走它所依附表的 UPDATE 语句生效。如果运维脚本、ETL 工具、旧服务直连数据库用了另一个账号(比如 root 或 etl_user),而该账号被直接授予了 UPDATE 权限,那触发器就完全失效。
真正难的从来不是写几行 SIGNAL,而是确保:
- 所有应用连接都用同一个受限账号(无直接
UPDATE权限) - 所有定时任务和运维操作都走封装好的存储过程或带注释的受控语句(如
/* ALLOWED_BY_POLICY */ UPDATE ...) - 定期审计
information_schema.SCHEMA_PRIVILEGES,确认没人偷偷加了高权限
一旦有任意一个写入口绕过触发器,整套防护就形同虚设——数据库层的硬拦截,必须是全链路唯一的写入通道。

















