必须用BEFORE INSERT/UPDATE触发器配合SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT才能真正阻断非法写入,因AFTER阶段数据已落盘,SIGNAL无法回滚主表变更,且MyISAM不支持事务、InnoDB中还可能触发锁冲突错误。

BEFORE触发器中SIGNAL才能真正阻断写入
只有BEFORE INSERT和BEFORE UPDATE里用SIGNAL才安全有效。AFTER阶段数据已落盘,SIGNAL即使执行成功,也只让事务回滚——但MyISAM表不支持回滚,主表记录可能已残留;InnoDB下还可能因后续操作触发Can't update table in stored function/trigger,导致SIGNAL根本没机会运行。
常见错误是把校验逻辑全塞进AFTER触发器,以为“查完再抛错”更稳妥,结果既慢又不可靠。
SIGNAL语法必须带MESSAGE_TEXT且不能漏分号
SIGNAL SQLSTATE '45000'本身合法,但客户端收到的错误信息会是空或默认模糊提示(如ERROR 1644 (45000)),除非显式加上SET MESSAGE_TEXT = 'xxx'。这个赋值不是可选的,是强制要求。
容易忽略的细节:
-
SIGNAL语句末尾必须有分号,否则触发器创建失败,报错ERROR 1064 (42000) -
SQLSTATE推荐用'45000',避免和系统错误码冲突;传'HY000'或'23000'可能被客户端误判为约束失败 - 中文消息直接写进去即可,MySQL 5.5+原生支持UTF8MB4编码的
MESSAGE_TEXT
查重或外键预检时别用SELECT INTO,改用EXISTS
想判断邮箱是否已存在,千万别写SELECT email INTO @tmp FROM users WHERE email = NEW.email。一旦没查到,立刻触发ERROR 1329 (02000),且无法在触发器里捕获——这不是你主动抛的错,是MySQL底层报的“未找到行”,SIGNAL根本不会执行。
正确做法是用EXISTS配合索引字段:
IF EXISTS (SELECT 1 FROM users WHERE email = NEW.email) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '该邮箱已被注册'; END IF;
前提是email字段有索引,否则EXISTS变成全表扫描,每次INSERT都拖慢写入。
触发器里不能ROLLBACK,也不能调用含事务控制的存储过程
MySQL触发器天然运行在外层语句的事务中,自己无权开启、提交或回滚事务。ROLLBACK、COMMIT或调用内部含这些语句的存储过程,都会直接报ERROR 1305 (42000): FUNCTION does not exist——这不是函数不存在,是语法被禁止。
真正可靠的中断方式只有SIGNAL:它会让外层INSERT/UPDATE立即终止,并触发自动回滚。这也是为什么所有校验逻辑必须放在BEFORE阶段——等数据进了表,就再也拦不住了。
最容易被忽略的一点:SIGNAL不是“抛个错就算完”,它依赖外层事务机制生效。如果你在非事务表(如MyISAM)上用,或者应用层手动禁用了事务,那SIGNAL只会中断当前语句,不会清理已写入的数据。


















