必须用before insert而非after insert,因前者在数据写入前校验并可signal中断,真正拦截非法数据;后者数据已落盘,报错无法回滚主表变更,导致脏数据入库。

必须用 BEFORE INSERT,不能用 AFTER INSERT —— 后者无法阻止非法数据写入,报错时主表记录已经落盘。
为什么 AFTER INSERT 校验是无效的
常见错误现象:SIGNAL SQLSTATE '45000' 看似触发了报错,但查表发现非法数据已插入成功。这是因为 AFTER 阶段执行时,INSERT 操作已完成,事务已修改数据页;此时中断只会让语句报错,不回滚主表变更(尤其在非事务引擎如 MyISAM 下更明显)。
真正能拦截的只有 BEFORE INSERT:它在行数据写入前执行,可读取并修改 NEW 值,或直接用 SIGNAL 终止整个 INSERT 语句。
-
BEFORE中可写SET NEW.phone = TRIM(NEW.phone)做自动清洗 -
AFTER中NEW是只读的,改不了数据,也拦不住脏写 - MySQL 8.0.16+ 虽支持
CHECK约束,但复杂逻辑(如跨字段计算、正则、关联查询)仍只能靠触发器
NEW 字段校验的典型写法与坑点
触发器内唯一可用的上下文是 NEW,代表即将插入的新行。所有校验都基于它,但要注意 NULL 和类型隐式转换的影响。
- 邮箱校验优先用
LIKE(如NEW.email NOT LIKE '%_@__%.__%'),比REGEXP快,且避免 NULL 导致条件失效 - 若字段允许
NULL,必须显式判断:IF NEW.email IS NOT NULL AND NEW.email NOT REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' THEN ...—— 否则NULL REGEXP xxx返回NULL,不进IF分支 - 手机号正则末尾的
\.是双重转义:MySQL 触发器体解析一次,正则引擎再解析一次,最终才匹配字面量点号 - 日期比较用
CURDATE(),别用NOW();前者只比日期,后者含时分秒,可能因精度导致误判
哪些校验适合放触发器,哪些绝对不该放
触发器不是万能胶水,加错地方会拖慢批量插入、引发死锁,甚至破坏事务一致性。
- ✅ 适合的:
NEW.status NOT IN ('draft', 'paid', 'shipped')、NEW.price > 0 AND NEW.price 、单字段格式校验(正则/LIKE)、简单计算(如 <code>NEW.total = NEW.qty * NEW.unit_price) - ❌ 绝对禁止:
SELECT balance FROM accounts WHERE user_id = NEW.user_id(大表查余额易锁表)、JOIN orders ON NEW.order_id = orders.id(JOIN 在触发器中性能极差)、调用外部 HTTP 或读文件(触发器根本不支持) - ⚠️ 高风险但有时不得不做:查主表状态(如
SELECT is_active INTO v_active FROM users WHERE id = NEW.user_id),必须确保users.id有主键或唯一索引,否则全表扫描 + 行锁会雪崩
权限、兼容性与绕过场景
即使语法完全正确,触发器也可能“静默失效”——不是代码问题,而是环境限制。
- 用户账号必须有
TRIGGER权限,否则创建成功但执行时报ERROR 1227 (42501) -
LOAD DATA INFILE默认绕过触发器(除非显式加SET FOREIGN_KEY_CHECKS=0并手动控制) - 某些 ORM 批量插入(如 Django 的
bulk_create、MyBatis 的insertBatch)生成的是单条 INSERT,但部分版本 MySQL 会跳过触发器优化路径 - 从库上触发器默认不执行(除非设置
log_bin_trust_function_creators=1且显式启用),主从数据一致性不能依赖触发器保证
最常被忽略的一点:触发器无法校验通过 INSERT ... SELECT 插入的数据是否满足业务规则——因为 SELECT 结果集不受触发器约束,除非你在源表也建对应触发器。这类跨表校验,建议收口到应用层或存储过程里做。











