在oracle 19c中,应使用before insert or update触发器配合raise_application_error实现数据双写校验,因其能在数据变更前拦截非法值;after触发器无法真正阻止写入且易引发ora-04091等异常。

直接说结论:在 Oracle 19c 中用 BEFORE INSERT OR UPDATE 触发器 + RAISE_APPLICATION_ERROR 是实现数据双写校验最稳妥的方式,不建议在 AFTER 触发器里做校验,也别依赖应用层补救。
为什么必须用 BEFORE 触发器而不是 AFTER?
校验的目的是阻止非法数据写入,不是事后打补丁。AFTER 触发器执行时,DML 已提交(或至少已修改 buffer),此时再抛异常会导致事务回滚不干净,还可能干扰自治事务或触发链。BEFORE 触发器在数据变更前介入,能真正拦住脏数据。
常见错误现象:ORA-04091: table is mutating 常出现在 AFTER 触发器里查本表,而 BEFORE 触发器中可安全使用 :NEW 和 :OLD 伪记录,无需额外游标或临时表。
- INSERT 场景只读
:NEW,UPDATE 场景两者都可用,DELETE 只读:OLD - 所有校验逻辑必须放在触发器体内部,不能调用含 DML 的函数(除非声明为
PRAGMA RESTRICT_REFERENCES) - 避免在触发器里调用
COMMIT或ROLLBACK—— 这会报ORA-04092
双写校验的典型场景和参数差异
“双写”通常指同一业务字段需满足两个独立约束,比如:用户注册时,email 既要格式合法(正则),又要全局唯一(查表)。这两者不能合并成一条 SQL 判断,因为唯一性需查表,而格式校验是纯计算。
示例:对 employees 表的 salary 字段做范围 + 同部门一致性校验:
BEGIN
IF :NEW.salary 50000 THEN
RAISE_APPLICATION_ERROR(-20001, 'Salary must be between 3000 and 50000');
END IF;
-- 同部门最高薪限制(需查表)
DECLARE
v_max_dept_sal NUMBER;
BEGIN
SELECT MAX(salary) INTO v_max_dept_sal
FROM employees
WHERE department_id = :NEW.department_id;
IF v_max_dept_sal IS NOT NULL AND :NEW.salary > v_max_dept_sal * 1.2 THEN
RAISE_APPLICATION_ERROR(-20002, 'Salary exceeds 120% of department max');
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN NULL;
END;
END;
- 范围校验用纯表达式,快且无锁;查表校验需显式
SELECT INTO,注意加EXCEPTION捕获NO_DATA_FOUND - 不要用
EXISTS替代SELECT INTO—— 触发器里无法返回布尔值给条件判断 - 如果校验逻辑复杂、复用率高,应封装成带
DETERMINISTIC的函数,但函数内仍不可含 DML
容易踩的坑:自治事务、性能与调试
有人想把校验日志写进另一张表,于是加 PRAGMA AUTONOMOUS_TRANSACTION。这看似合理,实则危险:日志写成功但主事务失败,会造成审计断点;更糟的是,自治事务会绕过主事务的隔离级别,导致幻读或重复日志。
真正该做的,是用 DBMS_OUTPUT.PUT_LINE 打印调试信息(仅开发环境),上线后全删掉 —— 触发器里任何 I/O 都会显著拖慢 DML 性能。
- 触发器执行时间应控制在毫秒级,查表最多一次,避免嵌套循环或多次
SELECT - 确保校验字段上有索引(如
department_id),否则SELECT MAX()会全表扫 - 测试时务必覆盖
INSERT、UPDATE、UPDATE OF salary等不同触发路径,UPDATE OF子句能减少不必要的触发
最易被忽略的一点:触发器本身没有版本管理,CREATE OR REPLACE TRIGGER 会直接覆盖旧逻辑,且不会保留历史。线上系统若需灰度验证,得先重命名旧触发器,再建新触发器,靠应用开关控制是否启用 —— 别指望靠注释或条件编译来“临时关闭”。











