必须用before insert而非after insert,因后者在数据写入后才执行,signal仅报错无法回滚,非法数据已落盘;before阶段可读写new并用signal终止语句。

必须用 BEFORE INSERT,不能用 AFTER INSERT——后者无法阻止数据写入,非法记录已经落盘。
为什么 AFTER INSERT 校验完全无效
常见错误现象:SIGNAL SQLSTATE '45000' 看似报错,但查表发现非法数据已插入成功。这是因为 AFTER INSERT 触发时,INSERT 语句早已执行完毕,事务已修改数据页;此时中断只让当前语句报错,不回滚主表变更。在 MyISAM 等非事务引擎下更危险,甚至可能留下半截脏数据。
真正能拦截的只有 BEFORE INSERT:它在行数据写入前执行,可读取并修改 NEW 值,或直接用 SIGNAL 终止整个 INSERT 语句。
-
NEW在BEFORE中是可写的临时行对象;在AFTER中是只读的,执行SET NEW.email = ...会报错ERROR 1362 (HY000) - 校验失败必须用
SIGNAL,仅写SELECT 'error'或INSERT INTO log完全没用,插入照常进行 - MySQL 5.7+ 才原生支持
SIGNAL;低版本只能靠除零等 hack 方式,不推荐
BEFORE INSERT 中校验字段值的典型写法与坑点
所有校验逻辑都基于 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,}$'—— 否则NULL REGEXP xxx返回NULL(非FALSE),IF不进分支 - 手机号正则末尾的
\.是双重转义:触发器体解析一次,正则引擎再解析一次,最终才匹配字面量点号 - 日期比较用
CURDATE(),别用NOW();前者只比日期,后者含时分秒,可能因精度导致误判
哪些校验适合放触发器,哪些绝对不该放
触发器不是万能胶水,加错地方会拖慢批量插入、引发死锁,甚至破坏事务一致性。
- ✅ 适合的:
NEW.status NOT IN ('draft', 'paid', 'shipped')、NEW.price > 0 AND NEW.price 、单字段格式校验(<code>REGEXP/LIKE)、简单计算(如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有主键或唯一索引,否则全表扫描 + 行锁会雪崩
标准化处理与绕过风险
自动清洗(如 SET NEW.email = LOWER(TRIM(NEW.email)))看着方便,但有硬性约束:
- 如果字段定义为
NOT NULL且无默认值,而你在触发器里把它设成空字符串或NULL,插入会失败 -
REGEXP_REPLACE()只在 MySQL 8.0+ 支持;清理电话里的非数字字符前,先确认版本:SELECT VERSION(); - 批量插入(如
INSERT INTO ... SELECT或 ORM 批量语句)可能绕过触发器,不是所有客户端都默认启用——需确认账号有TRIGGER权限,且操作未被优化跳过 - 真正容易被忽略的是:
LOAD DATA INFILE、某些INSERT ... ON DUPLICATE KEY UPDATE场景、以及低版本 MySQL 的正则锚点(^/$)支持都不稳定











