登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

区分 BEFORE 触发器修改 NEW 与 AFTER 触发器只能读取行值的行为

来源:17golang原创

时间:2026-10-13 06:13:36 155浏览 收藏

MySQL 触发器里最容易混淆的不是 BEFORE 和 AFTER 的拼写,而是“当前能读到哪一行、当前能不能改值”。处理自动清洗字段时,应该在 BEFORE 触发器里修改 NEW;处理审计或历史记录时,应该在 AFTER 触发器里读取 OLD 和 NEW。OLD 始终是只读值,AFTER 中即使能引用 NEW,也不能再靠赋值改变已经完成的行操作。

MySQL 8.4 官方资料:https://dev.mysql.com/doc/refman/8.4/en/trigger-syntax.html

先按事件判断 OLD/NEW 是否存在,再按触发时机判断 NEW 是否允许赋值:INSERT 只有 NEW,DELETE 只有 OLD,UPDATE 同时有 OLD 和 NEW;真正需要改待写入数据时,把 SET NEW 放在 BEFORE。
  • INSERT:没有旧行,只能引用 NEW.col_name。
  • DELETE:没有新行,只能引用 OLD.col_name。
  • UPDATE:OLD 是更新前值,NEW 是待更新值或更新后值。
  • OLD 只读;NEW 只有在 BEFORE 中才适合通过 SET 修改。

先把三个触发事件的值域分开

OLD 和 NEW 不是两张可以随意切换的临时表,而是由触发事件决定的行值别名。INSERT 没有可供比较的旧行,所以不存在 OLD;DELETE 没有待写入的新行,所以不存在 NEW;UPDATE 才同时拥有前后两个视角。

下面这张图只表达引用关系和写入权限,是静态说明图,不是数据库客户端截图,也不是运行结果。写触发器前先按这个矩阵排除一半错误,通常比遇到报错后再猜更快。

MySQL INSERT UPDATE DELETE 事件中 OLD 和 NEW 的可读写矩阵说明图
图1:INSERT、UPDATE、DELETE 中 OLD/NEW 的可用范围与 BEFORE 写入边界;这是静态结构说明图,不是截图。
事件可引用的别名别名含义可修改对象
INSERTNEW即将插入的新行BEFORE 中可修改 NEW
UPDATEOLD、NEW更新前值、待更新值BEFORE 中可修改 NEW,OLD 只读
DELETEOLD即将删除的现有行OLD 只读

用 BEFORE 规范化待写入的 NEW

假设订单表允许调用方传入状态,但业务约定空状态统一变成 pending,大写状态统一转成小写。这个动作必须发生在行写入之前,否则数据库已经接受了原始值,后续的 AFTER 阶段只能观察,不能再把行改回规范状态。

-- 创建示例表;状态列的默认值只覆盖未提供值的场景
CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  status VARCHAR(20) NOT NULL,
  updated_at TIMESTAMP NOT NULL
);

-- 在 UPDATE 之前整理待写入的状态值;SET NEW 会影响本次行更新
DELIMITER //
CREATE TRIGGER orders_before_update
BEFORE UPDATE ON orders
FOR EACH ROW
BEGIN
  -- OLD.status 只能读取,不能被赋值;只规范化 NEW.status
  SET NEW.status = LOWER(TRIM(NEW.status));

  -- 空字符串回退到业务默认状态,避免把无意义值写入表中
  IF NEW.status = '' THEN
    SET NEW.status = 'pending';
  END IF;
END//
DELIMITER ;

这里的 NEW.status 表示本次 UPDATE 准备写入的值,触发器可以在数据库真正修改该行前把它整理好。OLD.status 则代表修改前的值,适合拿来比较“状态是否发生变化”,不适合拿来做赋值目标。

如果是 INSERT,同样可以在 BEFORE INSERT 中给 NEW 的字段补默认值;如果是 DELETE,则没有可供赋值的 NEW。这种按事件拆开的写法能让触发器的输入输出边界保持清楚。

用 AFTER 记录已经完成的前后值

审计记录关注的是一次成功完成的行变更,因此更适合放在 AFTER UPDATE。此时可以同时读取 OLD 和 NEW,把更新前状态、更新后状态和业务主键写入审计表。AFTER 中的 NEW 是用于读取的结果视角,不是再次修改主表的入口。

-- 审计表只保存状态变化,不把触发器元数据混进业务订单表
CREATE TABLE order_status_audit (
  audit_id BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_id BIGINT NOT NULL,
  old_status VARCHAR(20) NOT NULL,
  new_status VARCHAR(20) NOT NULL,
  changed_at TIMESTAMP NOT NULL
);

-- AFTER 阶段读取已完成的前后值,记录一次成功的状态变化
DELIMITER //
CREATE TRIGGER orders_after_update
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
  -- 只有真正发生状态变化时才写审计,减少无意义记录
  IF NOT (OLD.status  NEW.status) THEN
    INSERT INTO order_status_audit
      (order_id, old_status, new_status, changed_at)
    VALUES
      (NEW.id, OLD.status, NEW.status, CURRENT_TIMESTAMP);
  END IF;
END//
DELIMITER ;

是 MySQL 的 NULL 安全比较运算符。示例用它判断前后状态是否真正不同,避免普通比较遇到 NULL 时得到未知结果。审计触发器只读取 OLD 和 NEW,把写入目标放在独立的审计表中。

MySQL BEFORE 修改待写入 NEW、表行写入和 AFTER 读取审计值的关系图
图2:BEFORE 修改待写入值、表行写入和 AFTER 读取审计值的关系;这是静态结构说明图,不是运行证据。

为什么 AFTER 中的 SET NEW 不会改变主表

触发器的动作时机决定了它能做什么。BEFORE 在行操作完成前触发,修改 NEW 等于修改即将提交给表的值;AFTER 只有在前置触发器和行操作成功后才执行,主表这一行已经完成本次变化,因此再写 SET NEW.column = ... 没有可改变的待写入阶段。

-- 下面是错误的放置方式:AFTER 中不能用 SET NEW 改回主表字段
DELIMITER //
CREATE TRIGGER orders_after_update_invalid
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
  -- 这里试图修改已经完成的行操作,不能作为 AFTER 的修正手段
  SET NEW.status = 'pending';
END//
DELIMITER ;

需要改变主表最终值时,应把修正规则移动到对应的 BEFORE INSERT 或 BEFORE UPDATE。需要记录变更时,则在 AFTER 中读取值并写入审计表。不要把“修改主表”和“记录主表变化”塞进同一个错误时机。

按 INSERT、UPDATE、DELETE 选择正确引用

除了时机,还要核对事件。把 UPDATE 触发器中的写法原样复制到 INSERT 或 DELETE,常见结果就是引用了不存在的别名。可以把实际需求先翻译成“我要读旧值、读新值,还是要改待写入值”,再选择事件和时机。

-- INSERT 只能读取 NEW;适合给新行补齐可计算字段
CREATE TRIGGER orders_before_insert
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
  -- 新行没有 OLD;把空状态整理为统一的业务值
  IF NEW.status IS NULL OR TRIM(NEW.status) = '' THEN
    SET NEW.status = 'pending';
  END IF;
END;

-- DELETE 只能读取 OLD;适合把被删除行的关键字段写入归档表
CREATE TRIGGER orders_after_delete
AFTER DELETE ON orders
FOR EACH ROW
BEGIN
  -- 删除后没有 NEW;审计内容必须来自 OLD
  INSERT INTO order_status_audit
    (order_id, old_status, new_status, changed_at)
  VALUES
    (OLD.id, OLD.status, 'deleted', CURRENT_TIMESTAMP);
END;

上面的两个例子分别体现“新行进入前补值”和“旧行离开后记账”。DELETE 的 AFTER 仍然可以读 OLD,但不能凭空构造一个代表新行的 NEW;INSERT 的 BEFORE 也不能拿 OLD 做前后比较。

发布前检查权限、定义和失败边界

触发器代码看起来正确,还要检查执行账号的权限和失败行为。MySQL 文档说明,读取相关列需要相应的 SELECT 权限,在 BEFORE 中修改 NEW 还需要目标列的 UPDATE 权限;触发器本身的创建也需要关联表的 TRIGGER 权限。生产发布不要只把 DDL 复制到共享账号下执行。

-- 查看触发器最终定义,确认事件和 BEFORE/AFTER 没有写反
SHOW CREATE TRIGGER orders_before_update;
SHOW CREATE TRIGGER orders_after_update;

-- 查看表级触发器清单,避免只改了一个同名意图的触发器
SELECT TRIGGER_NAME, EVENT_MANIPULATION, ACTION_TIMING, ACTION_STATEMENT
FROM INFORMATION_SCHEMA.TRIGGERS
WHERE TRIGGER_SCHEMA = DATABASE()
  AND EVENT_OBJECT_TABLE = 'orders'
ORDER BY ACTION_TIMING, EVENT_MANIPULATION, TRIGGER_NAME;

-- 查看当前会话权限;发布前按最小权限核对,而不是默认放大账号权限
SHOW GRANTS;

还要把失败边界写进发布说明:BEFORE 或 AFTER 触发器报错都会让触发它的语句失败;对事务表,语句失败通常会回滚该语句带来的变化。因而审计表字段约束、触发器账号权限和业务表写入要一起设计,不能只关注 SET NEW 是否能通过语法解析。

常见问题

UPDATE 触发器里的 OLD 和 NEW 分别是什么?

OLD 是更新前的列值,NEW 是准备更新或更新后的列值。需要比较变化时读取两者,需要改变最终写入值时只在 BEFORE 中赋值给 NEW。

AFTER UPDATE 还能读取 NEW 吗?

可以。AFTER 适合读取本次成功行操作对应的 NEW,例如写审计记录;但它不再是修改主表待写入值的阶段。

DELETE 触发器为什么不能使用 NEW?

DELETE 没有新行,触发器只能使用 OLD 读取即将删除的现有行。归档和审计字段应从 OLD 取值。

BEFORE 中修改 NEW 需要注意什么?

确认触发器事件确实是 INSERT 或 UPDATE,执行账号拥有目标列的更新权限,并且赋值后的值仍满足列类型、非空和业务约束。不要把 AFTER 中的修正逻辑当作替代方案。

小结

判断 MySQL 触发器中的 OLD 和 NEW,可以固定按两步走:先按 INSERT、UPDATE、DELETE 确认别名是否存在,再按 BEFORE、AFTER 确认是否处于可修改待写入值的阶段。清洗和补值放在 BEFORE,审计和记录放在 AFTER;OLD 始终只读,AFTER 中的 NEW 主要用于读取,不要再尝试通过赋值修改已经完成的主表行。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>