before update触发器中无法直接计算整行hash,因new.*只读且多字段拼接调用sha2()会报错;防篡改应使用set new.col=old.col或signal报错;真需哈希校验宜用generated column+check约束;审计场景才适合after触发器写入audit_log比对old_hash/new_hash。

BEFORE UPDATE 触发器中无法直接计算整行 HASH
MySQL 和 PostgreSQL 的 BEFORE UPDATE 触发器里,NEW.* 是只读的,不能在触发器逻辑里对整行字段拼接后调用 SHA2() 或 MD5() 计算哈希值——你连构造字符串都受限,更别说调用加密函数了。常见错误是写 SET @row_hash = SHA2(CONCAT(NEW.id, NEW.name, ...), 256),结果报错 ERROR 1362: Updating of NEW row is not allowed in after trigger(即使写在 BEFORE 里,某些版本也禁止在表达式中深度引用多个 NEW 字段)。
真正能稳定计算行级哈希的场景,只适合放在 AFTER INSERT/UPDATE 中,且必须搭配额外存储列或审计表,而不是用于“拦截篡改”。
防篡改 ≠ 实时校验 HASH,而是锁定关键字段
想防止某列被改(比如 password、created_at、balance),最直接有效的方式是用 BEFORE UPDATE 强制还原旧值:
SET NEW.password = OLD.password;SET NEW.created_at = OLD.created_at;- 如果要报错而非静默覆盖,改用
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Field xxx is immutable';
这种写法不依赖哈希,无性能开销,且在事务内立即生效。MySQL 5.7+ 和 8.0 都支持;PostgreSQL 则需在 PL/pgSQL 函数中 RETURN OLD;(行级 BEFORE 触发器返回 OLD 会跳过本次更新)。
真要存行哈希,得用 GENERATED COLUMN + CHECK 约束组合
如果你的目标是“让某行数据一旦写入就不可被静默篡改”,推荐用 MySQL 5.7+ 的虚拟列 + 检查约束,而非触发器:
- 加一列
row_hash CHAR(64) AS (SHA2(CONCAT(id, name, email, updated_at), 256)) STORED - 再加
CHECK (row_hash = SHA2(CONCAT(id, name, email, updated_at), 256))
这样每次 UPDATE 都会重新计算并校验,失败则整个语句回滚。注意:STORED 列会占用磁盘空间;若字段含 NULL,CONCAT 会返回 NULL,需用 COALESCE 处理。
审计表 + 触发器才是 HASH 的合理归宿
行哈希真正的用武之地是事后比对和审计,不是事前拦截。典型做法是:
- 建审计表
audit_log,含table_name、row_id、old_hash、new_hash、updated_at、updated_by - 在
AFTER UPDATE触发器里,用INSERT INTO audit_log SELECT ..., SHA2(CONCAT(OLD.*), 256), SHA2(CONCAT(NEW.*), 256), ... - 后续通过比对
old_hash和new_hash快速识别哪些行内容实际变了
这个方案不阻断业务,但把“是否篡改”的判断权交给了离线任务或定时巡检脚本——这才是哈希该起的作用:证据链,不是守门员。










