mysql中必须用before update触发器配合signal sqlstate '45000'报错来阻止敏感列修改,因不支持rollback;sql server可用after update触发器结合update()和deleted表判断值变化后rollback。

直接用 BEFORE UPDATE 触发器配合 SIGNAL 是最可靠的方式,ROLLBACK 在 SQL Server 里有效,但在 MySQL 中无法在触发器内显式回滚事务——它只能靠报错中断执行。
MySQL 必须用 SIGNAL 报错中断更新
MySQL 的触发器不支持 ROLLBACK TRANSACTION;一旦 UPDATE 开始,触发器只能通过抛出异常让整个语句失败。关键点是使用标准 SQLSTATE 码和明确的错误信息:
-
SQLSTATE '45000'是用户自定义错误的通用码,所有 MySQL 版本都兼容 - 必须写
BEFORE UPDATE,不能用AFTER—— 后者执行时数据已改,再报错也晚了 - 比较要用
NEW.column_name OLD.column_name,注意 NULL 比较需用IS DISTINCT FROM(MySQL 8.0.22+)或手动判空
示例:禁止改 id 和 created_at
DELIMITER $$
CREATE TRIGGER prevent_protected_cols
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
IF NEW.id != OLD.id THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'id is immutable';
END IF;
IF NEW.created_at != OLD.created_at THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'created_at cannot be modified';
END IF;
END$$
DELIMITER ;
SQL Server 能用 ROLLBACK,但要注意 deleted 表逻辑
SQL Server 触发器中 ROLLBACK TRANSACTION 可立即终止当前事务,但它依赖于 IF UPDATE(column) 判断是否真被修改过。这个函数只检测语句里是否出现了该列,不关心值是否变化——哪怕 SET username = username 也会触发。
- 若只想拦截“值变了”的情况,得结合
deleted表查原始值:WHERE inserted.username != deleted.username -
UPDATE()函数返回 true 不代表数据变了,仅表示该列出现在 SET 子句里 - 触发器里不能对原表做 DML(如
UPDATE admin SET ...),否则可能引发递归或死锁
安全写法示例:
CREATE TRIGGER tr_prevent_username_change
ON users
AFTER UPDATE
AS
IF UPDATE(username)
BEGIN
IF EXISTS (
SELECT 1 FROM inserted i
JOIN deleted d ON i.id = d.id
WHERE i.username != d.username
)
BEGIN
RAISERROR('username cannot be changed', 16, 1);
ROLLBACK TRANSACTION;
END
END
跨数据库统一防护要避开语法陷阱
同一个业务规则,在 MySQL 和 SQL Server 上不能共用一套触发器 SQL。最易踩的坑是:
- MySQL 不认
RAISERROR或ROLLBACK在触发器里生效;SQL Server 不支持SIGNAL - PostgreSQL 用
RAISE EXCEPTION,且支持在BEFORE触发器里直接RETURN NULL阻断 - 所有数据库中,触发器都无法阻止有
SUPER/sysadmin权限的用户绕过(比如禁用触发器后再更新)
真正防住修改,还得配合权限控制:对敏感列撤销普通用户的 UPDATE 权限,只留 SELECT。触发器是第二道防线,不是唯一手段。
测试时容易漏掉的边界情况
光测 UPDATE t SET col = ? 不够,这些场景常被忽略:
- 批量更新带
WHERE条件但没命中行:触发器仍会执行,NEW/OLD为空,可能报错或跳过判断 - NULL 值更新:MySQL 中
NULL = NULL为UNKNOWN,直接用!=会失效,得写成(NEW.col IS NULL) != (OLD.col IS NULL) OR NEW.col != OLD.col - 时间戳字段用
CURRENT_TIMESTAMP自动更新:如果触发器检查updated_at,它每次都会变,必须排除该列或允许其变
最终效果取决于你拦的是“列出现在语句中”,还是“列的值实际被改”——这两者在语义上完全不同,选错就等于没拦住。










