必须用 BEFORE INSERT 或 BEFORE UPDATE 触发器配合 RAISE_APPLICATION_ERROR 中断执行,且必须加 FOR EACH ROW;因为只有 BEFORE 时机可拦截未落盘数据,:new 可读写,而 AFTER 数据已写入无法回滚,缺少 FOR EACH ROW 则无法获取单行 :new 值导致校验失效。

必须用 BEFORE INSERT 或 BEFORE UPDATE 触发器,配合 RAISE_APPLICATION_ERROR 中断执行;AFTER 触发器无法阻止非法数据入库,写进去再报错等于白校验。
为什么只能用 BEFORE,且必须加 FOR EACH ROW
Oracle 的 BEFORE INSERT 和 BEFORE UPDATE 是唯一能真正拦截写入的时机。此时数据尚未落盘,:new 伪记录可读可写,校验失败后调用 RAISE_APPLICATION_ERROR 会直接回滚整个 DML 语句。AFTER 触发器里数据已写入,哪怕抛错也无法撤回;FOR EACH ROW 缺失会导致触发器变成语句级——根本拿不到单行 :new 值,校验逻辑全失效。
- 漏写
FOR EACH ROW:触发器只执行一次,:new指向的是不确定的某一行(甚至为空),批量插入时校验完全不可靠 - 误用
AFTER:即使RAISE_APPLICATION_ERROR成功抛出,表中数据已存,应用层看到错误但脏数据已在库里 - 在
DELETE触发器里访问:new:编译直接报错,DELETE只有:old
校验逻辑里哪些操作必须禁用
PL/SQL 触发器不是存储过程,它运行在 DML 事务上下文中,任何破坏原子性或引发变异(mutating)的操作都会导致 ORA-04091 或死锁。最常踩的坑是“以为能查、其实不能查”。
- 禁止在触发器内
SELECT ... FROM 目标表(比如在orders的触发器里再查orders):直接触发ORA-04091 mutating table - 禁止调用含 DML 的存储过程:只要内部有
INSERT/UPDATE/DELETE,就可能触发同上错误 - 避免复杂函数调用:如自定义聚合、远程查询、文件读写——这些要么不被允许,要么让事务变慢、难回滚
- 跨 schema 查询可以,但必须显式授权且用完整对象名,例如
SELECT status FROM app_user.users@remote_db不支持,Oracle 不允许触发器内跨库
如何安全实现跨表校验(比如用户状态检查)
要查关联表(如订单插入前确认用户未被禁用),必须用只读 SELECT INTO + 索引字段,且不能带 ORDER BY 或 LIMIT。Oracle 触发器不支持子查询直接嵌入条件表达式,所以别写 IF (SELECT is_blocked FROM users WHERE id = :new.user_id) THEN —— 这语法不合法。
- 正确写法:声明变量,
SELECT is_blocked INTO v_blocked FROM users WHERE id = :new.user_id; -
users.id必须有主键或唯一索引,否则可能返回多行,触发ORA-01422 - 加异常处理:用
NO_DATA_FOUND处理用户不存在,TOO_MANY_ROWS捕获索引缺失问题 - 别用
COUNT(*) > 0判断存在性:它强制扫全索引,而你只需要一个布尔结果;EXISTS在 Oracle 里虽可用,但触发器中需包装成子查询赋值,不如直接SELECT ... INTO清晰
容易被忽略的状态迁移与多行场景
单行测试能过,批量导入或并发更新时崩,往往是因为没覆盖 OLD 和 NEW 的组合判断,或没考虑多行触发顺序。
-
UPDATE校验不能只看:new.status:比如允许'draft' → 'published',但必须同时拒绝'shipped' → 'cancelled'(除非已归档发货单) - 多行
INSERT时,每个:new是独立的,但触发器本身无序执行——不能假设第 2 行能看到第 1 行刚插入的临时状态 - 涉及金额、库存等敏感字段变更,别在触发器里做扣减:那是业务动作,不是校验;应只检查
:new.balance >= 0这类守门逻辑 - 所有
RAISE_APPLICATION_ERROR错误码建议用-20001到-20999范围,避免和 Oracle 内部错误冲突
真正难的不是写触发器语法,而是想清楚:这个校验是否必须在数据库层强一致完成?如果业务规则常变、依赖外部系统、或涉及多表写操作,大概率该移出触发器,改用应用层事务或带重试的异步校验。触发器一旦上线,改起来比改应用代码更谨慎、更难测。

















