唯一可靠做法是在before update中用if比对old.field与new.field,不一致时用signal sqlstate'45000'抛出明确错误;null需显式判断,json字段推荐json_equal,且须注意signal仅mysql 5.5+支持。

直接在 BEFORE UPDATE 里比对 OLD.field 和 NEW.field,不一致就用 SIGNAL 中止——这是唯一可靠、无副作用的做法。别指望靠触发器“静默还原”或“事后审计”来保护字段,那等于没锁。
MySQL 中用 SIGNAL 主动拦截修改
MySQL 5.5+ 支持 SIGNAL,它是终止更新并返回明确错误的唯一干净方式。常见错误是用插入不存在表、调用不存在函数等方式“制造失败”,结果错误信息模糊、难以定位。
-
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'salary is immutable after approval';必须写在IF OLD.salary != NEW.salary THEN ... END IF;块内 - 字段允许
NULL时,不能只写OLD.salary != NEW.salary——NULL != NULL返回UNKNOWN,条件不成立;要显式写成(OLD.salary != NEW.salary) OR (OLD.salary IS NULL) != (NEW.salary IS NULL) - 如果字段是
JSON类型,!=比较可能因格式空格/顺序不同误判,建议先用JSON_EQUAL(OLD.field, NEW.field)(MySQL 8.0.26+)
PostgreSQL 里必须用 ROW LEVEL SECURITY 配合触发器
PostgreSQL 的触发器本身无法阻止用户绕过——只要他有 UPDATE 权限,就能直接发语句。真正起效的是把权限收窄,再让触发器只处理被允许的路径。
- 先执行
REVOKE UPDATE ON sensitive_table FROM app_user;,再GRANT UPDATE (id, updated_at) ON sensitive_table TO app_user;—— 只放开非敏感字段 - 触发器里仍需校验:比如
IF OLD.status IN ('paid', 'closed') AND NEW.amount IS DISTINCT FROM OLD.amount THEN RAISE EXCEPTION 'amount locked in final state'; -
IS DISTINCT FROM自动处理NULL,比!=安全;但注意它不能用于索引,仅作逻辑判断
SQL Server 不要依赖触发器拦截主键或标识列
SQL Server 对主键、IDENTITY 列的约束检查发生在触发器之前。你写个触发器拦 UPDATE id,根本不会执行到那一步——引擎早就在解析阶段报错 Cannot update identity column 'id' 了。
- 真正需要保护的是业务字段(如
ssn,account_number),这些才走触发器流程 - 别在触发器里查
sys.dm_exec_sessions拿操作人——高并发下不可靠;要用CONTEXT_INFO()或SESSION_CONTEXT(N'userid'),且应用层必须提前设置 - 触发器里禁止写
UPDATE同表,哪怕只是更新updated_at字段——会触发嵌套触发器或死锁;真要改时间戳,只能在BEFORE里设SET NEW.updated_at = GETDATE();(仅限INSTEAD OF触发器)
最常被忽略的一点:触发器只响应 DML,对 TRUNCATE TABLE、BULK INSERT、SELECT INTO 这类操作完全无效。如果你的数据库开放了 BULK ADMIN 权限,或者应用用了 LOAD DATA INFILE,所有字段保护都形同虚设。










