触发器中禁止使用BEGIN TRANSACTION或COMMIT,因其必须依附父语句事务上下文,强行控制会破坏原子性并报错;应将状态跳转逻辑移至应用层或存储过程,触发器仅作轻量校验与单点状态标记。

触发器里不能用事务控制审核状态跳转
直接在触发器中写 BEGIN TRANSACTION 或 COMMIT 会导致报错,比如 SQL Server 报 The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION,MySQL 则可能抛出 Can't execute statement in a READ ONLY transaction。触发器运行在父语句的事务上下文中,它自己不能开启/结束事务。
实操建议:
- 把状态跳转逻辑从触发器移到应用层或存储过程里,由上层统一控制事务边界
- 如果必须用触发器做轻量级响应(如记录日志、更新时间戳),只做
UPDATE同一表的非关键字段,避免跨表修改 - 审核链中“自动推进”应依赖业务层判断:比如插入一条
approval_record后,由服务调用存储过程检查当前节点是否全员通过,再执行下一步UPDATE order_status
多级审核状态跳转容易忽略“并发冲突”
两个审核人几乎同时提交通过操作,触发器若只查当前状态再更新,大概率出现覆盖写入——后提交者把前者的状态变更冲掉,导致状态卡在中间环节(比如停在 approved_by_dept,没走到 approved_by_finance)。
实操建议:
- 用带条件的
UPDATE语句确保状态跃迁合法,例如:UPDATE orders SET status = 'finance_review' WHERE id = 123 AND status = 'dept_review';
返回影响行数为 0 就说明状态已被改过,需重试或告警 - 审核记录表加唯一约束:
UNIQUE (order_id, approver_role),防止同一角色重复审批 - 避免在触发器里读取多行数据做决策(如统计“已通过人数”),这类逻辑延迟高、易脏读,应交给应用层加锁或用
SELECT ... FOR UPDATE
MySQL 和 PostgreSQL 触发器对 NEW/OLD 的处理差异影响状态判断
MySQL 的 BEFORE UPDATE 触发器中,NEW.status 是即将写入的值,而 PostgreSQL 要求显式写 NEW := NEW 才能修改;更关键的是,PostgreSQL 的 AFTER 触发器里 NEW 不可写,但 MySQL 允许在 AFTER 中再改一次(不推荐)。
实操建议:
- 状态跳转逻辑统一放在
BEFORE UPDATE,且只基于OLD.status和新业务事件(如新插入的审批记录)判断,不依赖NEW.status的初始值 - MySQL 示例(安全写法):
IF OLD.status = 'pending' AND NEW.approval_result = 'pass' THEN SET NEW.status = 'dept_review'; END IF;
- PostgreSQL 必须返回修改后的
NEW:IF OLD.status = 'pending' THEN NEW.status := 'dept_review'; RETURN NEW; END IF;
用触发器实现“驳回后重置整条链”风险极高
用户点“驳回”,期望状态回到 pending 并清空所有已存审批记录。若在触发器里连带 DELETE FROM approval_records WHERE order_id = OLD.id,会触发级联删除、再次激活其他触发器,极易形成循环或锁表。
实操建议:
- 驳回操作必须走显式业务接口,由应用层按顺序执行:
UPDATE orders SET status = 'pending'→DELETE FROM approval_records→INSERT INTO audit_log - 触发器只响应单点事件(如某条审批记录插入),不做跨实体清理。真要自动清理,用异步 job 定期扫描
status = 'rejected'订单并归档 - 加数据库注释标明哪些表的触发器是“只读响应”,哪些是“写入型”,避免后续维护者误以为能靠触发器兜底复杂流程

















