简介:这份PDF资料面向Oracle数据库开发者与PL/SQL初学者,系统讲解触发器的编程方法与应用场景,帮助读者掌握用触发器弥补完整性约束不足、实现复杂业务规则与审计跟踪的核心技能。资源包共1个PDF文件,约39KB,内容紧凑,适合作为随查随用的技术手册。资料从基本概念切入,梳理DML触发器、INSTEAD OF触发器与系统触发器的分类,并逐一说明触发事件、WHEN触发条件、触发对象、触发时机及行级与语句级子类型,同时讲解NEW与OLD表的使用。随后结合CREATE TRIGGER语句,给出教师表插入更新校验、操作类型记录等完整示例,并演示DROP TRIGGER删除触发器的写法,覆盖创建、执行到删除的全流程。目前已有262人学习,适合需要快速理解Oracle触发器机制、对照示例动手实践的读者参考。
1. 触发器不是“自动执行的存储过程”:先厘清它到底替你扛了什么
很多人第一次接触 ORACLE PL/SQL 触发器,是在一张核心业务表上被要求“加个审计”——谁改了、什么时候改的、改前改后是什么,全都要留痕。这时候触发器就登场了。它和存储过程最大的区别在于:存储过程要人主动调用,触发器由数据库事件自动唤起,你拦不住也绕不开。ORACLE PL/SQL 触发器能完成数据库完整性约束难以覆盖的复杂业务规则,也能监视数据库操作、实现审计功能。它适合两类人:一类是被“约束管不住、应用层又不可信”折磨的后端和 DBA,另一类是要在视图上做可写映射、在 DDL 或登录事件上做管控的运维。但触发器是把双刃剑,写得好是隐形守卫,写不好就是性能黑洞和递归地狱。这篇就按“是什么、怎么建、怎么执行、坑在哪、怎么进阶”拆一遍,代码都能直接抄。
2. 触发器类型与触发要素:选错类型,后面全白搭
2.1 DML、INSTEAD OF、系统触发器怎么选
ORACLE 里触发器按触发对象和事件分成三大类,选型错了,轻则逻辑不生效,重则报错编译不过。
DML 触发器定义在表或视图上,对 INSERT、UPDATE、DELETE 操作触发,这是日常用得最多的一类。INSTEAD OF 触发器只定义在视图上,用来替代实际的 DML 语句——因为普通视图往往不可直接更新,用它把视图上的操作翻译成对基表的操作。系统触发器则对数据库系统级操作触发,比如 DDL 语句、数据库启动或关闭、用户登录登出等,常见于审计和权限管控。
选型判断很简单:操作的是表数据,用 DML;操作的是视图且要可写,用 INSTEAD OF;要监控的是建表、改结构、登录这类系统行为,用系统触发器。三者不能混用,视图上建普通 DML 触发器在多数场景下是无效的。
2.2 触发时机、条件谓词与 NEW/OLD 伪记录
触发时机分 BEFORE 和 AFTER,表示触发器相对触发语句执行的先后。BEFORE 常用于校验和赋值,AFTER 常用于审计和级联。触发子类型分语句级和行级:语句级整个操作只触发一次,行级对每一行都触发,用FOR EACH ROW声明。
行级触发器里能访问:NEW和:OLD两个伪记录。:NEW是插入或更新后的新值,:OLD是更新或删除前的旧值。INSERT 时只有:NEW,DELETE 时只有:OLD,UPDATE 时两者都有。触发条件用 WHEN 子句限定,注意 WHEN 里引用字段不加冒号,写new.TNAME而不是:new.TNAME,这是新手最容易翻车的地方。
条件谓词INSERTING、UPDATING、DELETING在触发体内判断当前是哪种操作,返回布尔值。一个触发器可以同时挂 INSERT OR UPDATE OR DELETE,靠条件谓词分流,省得建三个。
2.3 一个能跑的审计触发器长什么样
下面这个触发器挂在 TEACHERS 表上,对 INSERT、UPDATE、DELETE 都记录操作类型到 SQL_INFO 表,是审计场景的最小可用模板。
CREATE OR REPLACE TRIGGER my_trigger1 AFTER INSERT OR UPDATE OR DELETE ON TEACHERS FOR EACH ROW DECLARE info CHAR(10); BEGIN IF inserting THEN info := 'INSERT'; ELSIF updating THEN info := 'Update'; ELSE info := 'Delete'; END IF; INSERT INTO SQL_INFO VALUES(info); END my_trigger1; /逻辑说明:AFTER保证业务操作已经落库再记审计,避免主操作回滚后审计却留下脏记录。FOR EACH ROW让每一行变更都留一条。条件谓词按优先级判断,ELSE兜住 DELETE。参数上,info用 CHAR(10) 够放三种操作名;SQL_INFO 表要提前建好,字段类型和这里对齐。
注意:行级触发器里对同一张表做查询或写入要格外小心,容易触发变异表(mutating table)错误,后面避坑章节细说。
3. 创建与执行触发器:从语法骨架到可复现示例
3.1 CREATE TRIGGER 的完整语法骨架
创建触发器的基本结构是:CREATE OR REPLACE TRIGGER 触发器名加触发时机、触发事件、触发对象,再跟触发体。触发体可以是完整的 PL/SQL 块,含 DECLARE、BEGIN、EXCEPTION。
CREATE OR REPLACE TRIGGER my_trigger BEFORE INSERT OR UPDATE OF TID, TNAME ON TEACHERS FOR EACH ROW WHEN (new.TNAME = 'David') DECLARE teacher_id TEACHERS.TID%TYPE; INSERT_EXIST_TEACHER EXCEPTION; BEGIN SELECT TID INTO teacher_id FROM TEACHERS WHERE TNAME = new.TNAME; RAISE INSERT_EXIST_TEACHER; EXCEPTION WHEN INSERT_EXIST_TEACHER THEN INSERT INTO ERROR(TID, ERR) VALUES(teacher_id, 'the teacher already exists!'); END my_trigger; /逻辑说明:BEFORE INSERT OR UPDATE OF TID, TNAME表示只在插入或更新 TID、TNAME 这两列时触发,更新其他列不触发,这是OF子句的精准控制。WHEN (new.TNAME = 'David')是触发条件,只有新值等于 David 才进触发体。触发体里先查 TID,再主动抛自定义异常,异常处理里把冲突记录写进 ERROR 表。
参数说明:TEACHERS.TID%TYPE是锚定类型,跟着基表字段类型走,基表改了这里不用改。自定义异常INSERT_EXIST_TEACHER用RAISE抛出,EXCEPTION WHEN捕获。这里有个隐患——在行级触发器里SELECT ... FROM TEACHERS查自己这张表,正是变异表错误的经典触发场景,实际生产要改写,避坑章节展开。
3.2 触发器的自动执行与验证方法
触发器建好后不需要显式调用,用户对 TEACHERS 做 DML 时自动执行。验证是否生效,最直接的办法是执行一条 DML 再查审计表。
-- 触发审计触发器 INSERT INTO TEACHERS(TID, TNAME) VALUES(1001, 'Tom'); COMMIT; -- 查看审计结果 SELECT * FROM SQL_INFO;逻辑说明:插入一条教师记录后,my_trigger1 自动往 SQL_INFO 写一条 'INSERT'。如果查不到记录,先确认触发器状态是否为 ENABLED,再确认 DML 是否真的提交。
-- 查看触发器状态 SELECT trigger_name, status FROM user_triggers WHERE trigger_name = 'MY_TRIGGER1';参数说明:user_triggers是当前用户下的触发器视图,status为 ENABLED 才生效,DISABLED 需要用ALTER TRIGGER my_trigger1 ENABLE启用。编译报错的触发器状态是 INVALID,得先修语法。
3.3 删除与禁用:别让触发器变成甩不掉的包袱
删除触发器用DROP TRIGGER,禁用用ALTER TRIGGER ... DISABLE。批量维护或数据迁移时,禁用比删除更稳妥,迁移完再启用。
-- 删除触发器 DROP TRIGGER my_trigger; -- 禁用与启用 ALTER TRIGGER my_trigger1 DISABLE; ALTER TRIGGER my_trigger1 ENABLE;逻辑说明:DROP 是永久删除,定义没了要重建;DISABLE 只是停用,定义还在,适合临时关闭。数据批量导入前禁用审计触发器能大幅提速,导入后记得启用,否则审计断档。
注意:删除或禁用触发器前,先确认没有其他对象依赖它,尤其是系统触发器和登录触发器,贸然禁用可能影响连接和权限校验。
4. 避坑与排查:触发器翻车的五个真实场景
4.1 变异表错误 ORA-04091
现象:行级触发器里查询或修改自己所在的表,报 ORA-04091 table is mutating。
原因:行级触发器执行时表正处于变更中,ORACLE 不允许在触发器里读同一张表的一致性快照。
解决:把逻辑拆到语句级触发器加包变量,或用复合触发器(COMPOUND TRIGGER)在 AFTER STATEMENT 阶段处理。简单场景也可以改用约束或应用层校验。
4.2 WHEN 子句里加了冒号
现象:WHEN (:new.TNAME = 'David')编译报错。
原因:WHEN 子句是 SQL 层面解析,伪记录不加冒号;触发体内才是 PL/SQL,要加冒号。
解决:WHEN 里写new.TNAME,触发体里写:new.TNAME,记住这个分界。
4.3 触发器递归触发自己
现象:触发器里对同一张表做 DML,导致无限递归或超深调用栈。
原因:触发器内的 DML 又触发了同一个触发器。
解决:用PRAGMA AUTONOMOUS_TRANSACTION谨慎隔离,或改用包变量加语句级触发器,从设计上避免自触发。递归深度受open_links等参数间接影响,但根子在逻辑。
4.4 审计触发器拖慢批量操作
现象:批量导入几万行,速度慢到无法接受。
原因:行级触发器每行都执行一次,还带额外 INSERT,开销成倍放大。
解决:批量场景先ALTER TRIGGER ... DISABLE,导入完再 ENABLE 并补审计。或者把审计改成异步写入,减少主事务阻塞。
4.5 触发器编译通过但状态 INVALID
现象:CREATE 没报错,但查 user_triggers 状态是 INVALID。
原因:触发器引用的表、字段或包在编译时不存在或权限不足。
解决:查user_errors看具体错误行,补权限或先建依赖对象,再ALTER TRIGGER ... COMPILE重编译。
5. 进阶:用复合触发器把行级与语句级捏在一起
普通触发器要么行级要么语句级,遇到“每行收集数据、整条语句结束后统一处理”的需求就很别扭。ORACLE 11g 引入的复合触发器(COMPOUND TRIGGER)正好解决这个痛点,它在一个触发器里同时定义 BEFORE STATEMENT、BEFORE EACH ROW、AFTER EACH ROW、AFTER STATEMENT 四个时间点,共享包级变量。
下面这个例子在每行把变更的 TID 收集到数组,语句结束后统一写审计,既避免变异表,又减少逐行写库开销。
CREATE OR REPLACE TRIGGER trg_teachers_audit FOR INSERT OR UPDATE OR DELETE ON TEACHERS COMPOUND TRIGGER TYPE t_ids IS TABLE OF TEACHERS.TID%TYPE INDEX BY PLS_INTEGER; v_ids t_ids; v_cnt PLS_INTEGER := 0; BEFORE EACH ROW IS BEGIN v_cnt := v_cnt + 1; IF INSERTING OR UPDATING THEN v_ids(v_cnt) := :new.TID; ELSE v_ids(v_cnt) := :old.TID; END IF; END BEFORE EACH ROW; AFTER STATEMENT IS BEGIN FOR i IN 1 .. v_cnt LOOP INSERT INTO SQL_INFO VALUES('TID=' || v_ids(i)); END LOOP; END AFTER STATEMENT; END trg_teachers_audit; /逻辑说明:COMPOUND TRIGGER下用BEFORE EACH ROW收集数据到关联数组,AFTER STATEMENT里统一写审计。这样既拿到了行级的新旧值,又避开了在行级阶段操作表的限制。参数上,t_ids是索引表类型,v_cnt计数,循环按实际行数写。
验证方法和普通触发器一样,执行 DML 后查 SQL_INFO。区别在于审计写入发生在语句结束后,事务内可见。
我自己的习惯是:任何行级触发器上线前,先用复合触发器结构评估一遍,能挪到语句级的绝不留在行级。从那以后我每次建触发器都强制走一遍“类型选对没、WHEN 冒号加没加、会不会自触发、批量场景禁没禁”的检查,省下不少半夜排障的时间。希望帮到你。
本文还有配套的精品资源,点击获取