必须用before insert或before update触发器,因其在数据写入前执行,可读写new字段并用signal中断语句;after触发器数据已落盘,报错无法回滚,完全无效。

必须用 BEFORE INSERT 或 BEFORE UPDATE 触发器
只有 BEFORE INSERT 和 BEFORE UPDATE 能真正阻止非法数据落库。因为它们在数据写入表之前执行,此时可读写 NEW 字段,并用 SIGNAL 中断整个语句;AFTER 类触发器完全无效——行已提交,报错只是“事后喊停”,数据照进不误。
常见错误现象:AFTER INSERT 里写 SIGNAL SQLSTATE '45000' 看似报错,但查表发现非法数据早已存在。
- Navicat / DBeaver 创建触发器时,默认常选
AFTER,务必手动切换成BEFORE - 复制环境(主从)中触发器不同步,从库需单独重建,且要确保
log_bin_trust_function_creators=1 - 触发器名、表名、数据库名大小写敏感(尤其 Linux),建完记得用
SELECT * FROM information_schema.TRIGGERS WHERE EVENT_OBJECT_TABLE = 'your_table';确认是否生效
SIGNAL SQLSTATE '45000' 是唯一干净的中断方式
SIGNAL 是 MySQL 5.5+ 唯一推荐的中断手段。低版本伪造除零或主键冲突等方式极不可靠,且错误信息混乱。
关键避坑点:
- 错误码必须用
'45000',别用'HY000'——后者是系统保留码,会和真实数据库错误混淆 - 创建前必须改
DELIMITER,否则SIGNAL后的分号会让 MySQL 提前结束解析,报ERROR 1064 -
MESSAGE_TEXT中拼接NEW.xxx前,先确认它非NULL:一旦NEW.id是NULL,整条SIGNAL会静默失效——不报错、也不拦截 - 中文报错信息支持弱,建议用英文或拼音缩写,比如
'age_out_of_range'
校验逻辑不能只看数值,得还原原始输入
对 DECIMAL 或字符串字段,重点不是“是不是数字”,而是原始输入是否合规。MySQL 会在入库前自动四舍五入或截断,导致数值比较失效。
例如校验金额最多两位小数:
- 别写
NEW.amount > 999999.99—— 这拦不住'123.456',因为已被转成123.46 - 要写
TRIM(NEW.amount)去空格,再用CAST(NEW.amount AS CHAR)+LOCATE('.', ...)+LENGTH(...)计算小数位数 - 注意无小数点情况(如
'123'),避免LOCATE返回 0 导致长度判断出错
同理,IP 校验不能只靠 INET_ATON() 是否为 0,得用正则锚定全匹配:NEW.ip_addr NOT REGEXP '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'(MySQL 8.0.4+ 支持 ^/$)
触发器不是万能兜底,它只管 SQL 写入路径
触发器只对走 SQL 引擎的写入生效,对以下场景完全无感:
-
LOAD DATA INFILE、INSERT ... SELECT、COPY(PostgreSQL)等批量导入操作完全绕过触发器 - 运维脚本、旧服务直连数据库、数据分析工具等高权限账号写入
- 应用层绕过 ORM 直接
UPDATE t SET status = 'paid',跳过业务校验逻辑
更隐蔽的问题是:触发器拿不到 HTTP Header、JWT 用户 ID、traceID 等上下文,也查不到本事务中前面插入的临时行(快照隔离)。真正难的不是写 SIGNAL,而是让所有写入口都经过它——这几乎只能靠权限管控和流程规范,而不是技术强制。











