PLSQL——09、触发器
触发器 trigger
触发器是一种特殊的过程,它的执行是由一系列事件触发的。这些事件有用户登录注销事件,DML语句执行事件等等。我们常用的就是DML触发器,事件的精确粒度可以是表级或行级。
trigger的实际应用非常广泛,例如A表有数据DML操作,就可以在A表上做个触发器,将数据协同更新到B表。我们也经常用触发器来做审计。
触发器可分为:
- database 触发器:审计,例如记录用户登录和退出的信息等
- schema 用户触发器:可以用于:禁用DDL、禁止某个IP登录
- DML触发器(表级 和 行级):表级 DML前(before) DML后(after) ;行级 更新每一行触发一次 (before|after),行级带有for each row关键字
创建触发器语法
CREATE OR REPLACE TRIGGER trigger_name {BEFORE|AFTER|INSTEAD OF} EVENTs ON TABLE_NAME [for each row] declare ... begin ... end;
- {BEFORE|AFTER|INSTEAD OF} 触发器在事件执行之前或之后触发,还有一种替代触发器
- EVENTs 不同类型的触发器有不同的事件,例如DML触发器 update or delete or insert
- ON TABLE_NAME 触发器依附的表
- [for each row] 如果有这个关键字,就代表是行级触发器,没有就是表级触发器
检测的谓词可以直接用于 if 条件
- updating 如果是update操作 此值为true 否则false
- inserting 如果是insert操作 此值为true 否则false
- deleting 如果是delete操作 此值为true 否则false
查看触发器的数据字典
- user_source
- user_triggers
- user_trigger_cols
演示1:禁止DML操作T1表 触发器直接抛出错误中断DML操作
SQL> create table t1 as select * from emp; Table created. SQL> create or replace trigger tr_no_dmlbefore update or delete or inserton t1 begin if updating then raise_application_error(-20001,‘不允许做UPDATE操作!‘); elsif inserting then raise_application_error(-20002,‘不允许做insert操作!‘); else raise_application_error(-20003,‘不允许做delete操作!‘); end if; end; Trigger created. SQL> update t1 set sal=sal+1 where empno=7788; update t1 set sal=sal+1 where empno=7788 * ERROR at line 1: ORA-20001: 不允许做UPDATE操作! ORA-06512: at "SCOTT.TR_NO_DML", line 4 ORA-04088: error during execution of trigger ‘SCOTT.TR_NO_DML‘ SQL> delete t1 where empno=7788; delete t1 where empno=7788 * ERROR at line 1: ORA-20003: 不允许做delete操作! ORA-06512: at "SCOTT.TR_NO_DML", line 9 ORA-04088: error during execution of trigger ‘SCOTT.TR_NO_DML‘ SQL> insert into t1 select * from emp where empno=7788; insert into t1 select * from emp where empno=7788 * ERROR at line 1: ORA-20002: 不允许做insert操作! ORA-06512: at "SCOTT.TR_NO_DML", line 7 ORA-04088: error during execution of trigger ‘SCOTT.TR_NO_DML‘
演示2:更新t2表,将更新记录在aud_tmp表
SQL> create table t2 as select * from emp; Table created. SQL> SQL> create table aud_tmp(exec_time date,exec_type varchar2(10),exec_user varchar2(20)); Table created. ----获取用户和时间的方法: SQL> select sys_context(‘USERENV‘,‘CURRENT_USER‘),sysdate from dual; SYS_CONTEXT(‘USERENV‘,‘CURRENT_USER‘) SYSDATE ------------------------------------------- ------------------- SCOTT 2011-03-11 05:37:06 SQL> SQL> create or replace trigger tri_t2_after_row after update or insert or delete on t2 for each row begin if updating then insert into aud_tmp values(sysdate,‘update‘,SYS_context(‘USERENV‘,‘CURRENT_USER‘)); elsif inserting then insert into aud_tmp values(sysdate,‘insert‘,SYS_context(‘USERENV‘,‘CURRENT_USER‘)); else insert into aud_tmp values(sysdate,‘delete‘,SYS_context(‘USERENV‘,‘CURRENT_USER‘)); end if; end; / Trigger created. SQL> select * from aud_tmp; no rows selected SQL> update t2 set sal=sal+1 where empno=7788; 1 row updated. SQL> select * from aud_tmp; EXEC_TIME EXEC_TYPE EXEC_USER ------------------- ---------- -------------------- 2011-03-11 05:39:32 SCOTT update SQL>
表级触发器和行级触发器的区别
- 行级建立的时带有 for each row 关键字
- 表级只触发一次
- 行级每一行都会触发一次
演示如下:
SQL> create table t3 as select * from emp; Table created. SQL>create or replace trigger tr_tab_t3 before update on t3 begin dbms_output.put_line(‘run tab tr‘); end; SQL>create or replace trigger tr_row_t3 before update on t3 for each row begin dbms_output.put_line(‘run row tr‘); end; / SQL> update t3 set sal=sal+1 where empno=7788; run tab tr run row tr /*单行操作 表级触发一次 行级触发一次*/ 1 row updated. SQL> SQL> update t3 set sal=sal+1 where deptno=10; run tab tr run row tr run row tr run row tr /*多行操作 表级触发一次 行级触发次数由操作行数来决定*/ 3 rows updated.
结论:
- 单行操作:表级触发一次,行级触发一次
- 多行操作:表级触发一次,行级触发次数由操作行数来决定
触发器触发顺序
before statement trigger(on table)
->before row trigger (on table for each row)
->after row trigger (on table for each row)
->after statement trigger (on table)
表前==>行前==>行后==>表后
演示:
create or replace trigger e_update1 before update on e begin if updating then dbms_output.put_line(‘table before‘); end if; end; / create or replace trigger e_update2 before update on e for each row begin if updating then dbms_output.put_line(‘row before‘); end if; end; / create or replace trigger e_update3 after update on e for each row begin if updating then dbms_output.put_line(‘row after‘); end if; end; / create or replace trigger e_update4 after update on e begin if updating then dbms_output.put_line(‘table after‘); end if; end; / update e set sal=sal+1 where empno=7369;
触发器的禁用和删除
--验证触发器的状态 select trigger_name,status from user_triggers; --禁用某个触发器 ALTER TRIGGER e_update3 disable; --禁用某个表上的所有触发器 alter table e disable all triggers; --删除触发器 DROP TRIGGER e_update3;
行级触发器的协同更新
行级触发器增加了更新动作的旧值和新值的存储,表级不可以,行级触发器才有 :old.COL 和 :new.COL 的定义。
演示:
SQL> create or replace trigger tri_t4 after update or delete or insert on t4 for each row begin if updating then dbms_output.put_line(‘old_value: ‘||:old.sal||‘ ‘||‘new_value: ‘||:new.sal); elsif inserting then dbms_output.put_line(‘old_value: ‘||:old.sal||‘ ‘||‘new_value: ‘||:new.sal); else dbms_output.put_line(‘old_value: ‘||:old.sal||‘ ‘||‘new_value: ‘||:new.sal); end if; end; SQL> / Trigger created. SQL> update t4 set sal=sal+1 where ename=‘SCOTT‘; old_value: 3000 new_value: 3001 1 row updated. SQL> delete t4 where ename=‘SCOTT‘; old_value: 3001 new_value: 1 row deleted. SQL> insert into t4 select * from emp where ename=‘SCOTT‘; old_value: new_value: 3000 1 row created. SQL>
自制事务
DML(delete/update/insert)触发器中不能使用DDL(CREATE,DROP,ALTER)语句,也不能使用事务控制语句(ROLLBACK, COMMIT,SAVEPOINT)。特别注意的是,在触发器的主体中引用的函数(function)/过程(procedure)中也不能有事物控制语句,否则会报错误:ORA-04092: cannot COMMIT in a trigger
trigger不能含有事物处理的语句,需要使用自制事务。自制事务可以将触发器内的DML独立成事务。
自制事务在declare中声明PRAGMA autonomous_transaction; pragma为编译指示。
切记:如果触发器中引用的过程也带有DDL DCL也要定义成自制事务。
不使用自制事务,拿上面做过的T2表触发器做演示
create or replace trigger tr_aud_t2 after update on t2 begin insert into aud_tmp values(sysdate,sys_context(‘USERENV‘,‘CURRENT_USER‘),‘update‘); commit; end; SQL> / Trigger created. SQL> select ename,sal from t2 where empno=7788; ENAME SAL ---------- ---------- SCOTT 102 SQL> update t2 set sal=sal+1 where empno=7788; update t2 set sal=sal+1 where empno=7788 * ERROR at line 1: ORA-04092: cannot COMMIT in a trigger ORA-06512: at "SCOTT.TR_AUD_T2", line 3 ORA-04088: error during execution of trigger ‘SCOTT.TR_AUD_T2‘
对T2执行更新时,会报错触发器不能使用commit语句。
改成自制事务
create or replace trigger tr_aud_t2 after update on t2 declare PRAGMA autonomous_transaction; begin insert into aud_tmp values(sysdate,sys_context(‘USERENV‘,‘CURRENT_USER‘),‘update‘); commit; end; SQL> / SQL> select ename,sal from t2 where empno=7788; ENAME SAL ---------- ---------- SCOTT 102 SQL> select * from aud_tmp; EXEC_TIME EXEC_TYPE EXEC_USER ------------------- ---------- -------------------- 2011-03-11 05:39:32 SCOTT update SQL> update t2 set sal=sal+1 where empno=7788; 1 row updated. SQL> select ename,sal from t2 where empno=7788; ENAME SAL ---------- ---------- SCOTT 103 SQL> select * from aud_tmp; EXEC_TIME EXEC_TYPE EXEC_USER ------------------- ---------- -------------------- 2011-03-11 05:39:32 SCOTT update 2011-03-11 06:02:04 SCOTT update SQL> roll Rollback complete. SQL> select ename,sal from t2 where empno=7788; ENAME SAL ---------- ---------- SCOTT 102 SQL> select * from aud_tmp; EXEC_TIME EXEC_TYPE EXEC_USER ------------------- ---------- -------------------- 2011-03-11 05:39:32 SCOTT update 2011-03-11 06:02:04 SCOTT update SQL>
这样,触发器提交的只有触发器内的事务(AUD_tmp表的插入),而T2表的更新被回滚也可以记录下来。
替代触发器 instead of
替代触发器作用在视图上,把单条的DML分解成多条DML到多个表,就可以实现插入了,但带有聚集函数的还是不能操作。
局限性:只能更新两张表的简单视图。
演示如下:
----关闭d、e表之前我们做的所有触发器 以免影响此次测试 SQL> alter table d disable all triggers; Table altered. SQL> alter table e disable all triggers; Table altered. ----创建个视图 CREATE OR REPLACE VIEW V1 AS SELECT E.EMPNO,E.ENAME,D.DEPTNO,D.DNAME,D.LOC FROM EMP E,DEPT D WHERE E.DEPTNO=D.DEPTNO; View created. SQL> select * from v1; EMPNO ENAME DEPTNO DNAME LOC ---------- ---------- ---------- -------------- ------------- 7369 SMITH 20 RESEARCH DALLAS 7499 ALLEN 30 SALES CHICAGO 7521 WARD 30 SALES CHICAGO 7566 JONES 20 RESEARCH DALLAS 7654 MARTIN 30 SALES CHICAGO 7698 BLAKE 30 SALES CHICAGO 7782 CLARK 10 ACCOUNTING NEW YORK 7788 SCOTT 20 RESEARCH DALLAS 7839 KING 10 ACCOUNTING NEW YORK 7844 TURNER 30 SALES CHICAGO 7876 ADAMS 20 RESEARCH DALLAS 7900 JAMES 30 SALES CHICAGO 7902 FORD 20 RESEARCH DALLAS 7934 MILLER 10 ACCOUNTING NEW YORK 14 rows selected. SQL> ----无法插入 因为这个视图涉及多个表连接 是复杂视图 SQL> insert into v1 values(8000,‘SEKER‘,50,‘JS‘,‘BJ‘); insert into v1 values(8000,‘SEKER‘,50,‘JS‘,‘BJ‘) * ERROR at line 1: ORA-01776: cannot modify more than one base table through a join view
利用替代触发器分解插入语句实现插入
insert into v1 values(8000,‘SEKER‘,50,‘JS‘,‘BJ‘);
分解这个语句到每个表
- insert into d(DEPTNO,DNAME,LOC) values(50,‘JS‘,‘BJ‘);
- insert into e(EMPNO,ENAME) values(8000,‘SEKER‘);
但在程序中我们应该使用 :new.XXX的值。
create or replace trigger tr_DML_v1 instead of insert on v1 begin insert into d(DEPTNO,DNAME,LOC) values(:new.DEPTNO,:new.DNAME,:new.LOC); insert into e(EMPNO,ENAME) values(:new.EMPNO,:new.ENAME); end; / Trigger created. SQL> insert into v1 values(8000,‘SEKER‘,50,‘JS‘,‘BJ‘); 1 row created. SQL> select * from d where deptno=50; DEPTNO DNAME LOC ---------- -------------- ------------- 50 JS BJ SQL> select * from e where empno=8000; EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO ---------- ---------- --------- ---------- ------------------- ---------- ---------- ---------- 8000 SEKER SQL>
练习
练习 1:DML触发器(作用于某个列上) --------------------------------------------------------------------------------- create or replace trigger e_update before update of sal on e for each row begin if updating then raise_application_error(-20001,‘salary do not modify!‘); end if; end; / 练习 2:DML触发器限制数据修改 --------------------------------------------------------------------------------- CREATE OR REPLACE TRIGGER secure_emp before INSERT ON e BEGIN IF TO_CHAR (SYSDATE, ‘HH24‘) NOT BETWEEN ‘08‘ AND ‘09‘ OR TO_CHAR (SYSDATE, ‘DY‘) IN (‘SAT‘, ‘SUN‘) THEN RAISE_APPLICATION_ERROR (-20205, ‘insert into table E!‘); END IF; END; / 练习 3:更新d表deptno时自动更新e表deptno --------------------------------------------------------------------------------- CREATE or replace TRIGGER vdate AFTER UPDATE OF deptno ON d FOR EACH ROW BEGIN IF (UPDATING AND :old.deptno != :new.deptno) THEN --关联标识(保留结构) UPDATE e SET deptno = :new.deptno WHERE deptno = :old.deptno; END IF; END; / 练习 4:删除d表数据时自动删除e表数据 --------------------------------------------------------------------------------- CREATE or replace TRIGGER vdate AFTER DELETE ON d FOR EACH ROW BEGIN IF DELETING THEN delete e WHERE deptno = :old.deptno; END IF; END; / 练习 5:删除d表数据时将e表deptno置空 --------------------------------------------------------------------------------- CREATE or replace TRIGGER vdate AFTER DELETE ON d FOR EACH ROW BEGIN IF DELETING THEN UPDATE e SET deptno = :new.deptno WHERE deptno = :old.deptno; END IF; END; / 练习 6:删除d表数据时将e表deptno置空,更新d表deptno时自动更新e表deptno --------------------------------------------------------------------------------- CREATE or replace TRIGGER vdate AFTER delete or UPDATE OF deptno ON d FOR EACH ROW BEGIN IF deleting or (UPDATING AND :old.deptno != :new.deptno) THEN UPDATE e SET deptno = :new.deptno WHERE deptno = :old.deptno; END IF; END; / 练习 7:删除d表数据时自动删除e表数据,更新d表deptno时自动更新e表deptno --------------------------------------------------------------------------------- CREATE or replace TRIGGER vdate AFTER delete or UPDATE OF deptno ON d FOR EACH ROW BEGIN IF (UPDATING AND :old.deptno != :new.deptno) THEN UPDATE e SET deptno = :new.deptno WHERE deptno = :old.deptno; elsif deleting then delete e WHERE deptno = :old.deptno; END IF; END; / 练习 8:trigger不能含有事物处理的语句!需要使用自制事务! --------------------------------------------------------------------------------- create or replace procedure Autonomous (v_sal e.sal%type,v_empno e.empno%type) is PRAGMA Autonomous_transaction; --声明为自制事物 begin update e set sal=v_sal where empno=v_empno; commit; end Autonomous; / --如果emp表的sal被更新那么自动更新e表的sal create or replace trigger cascade_update_e before update of sal on emp for each row declare v_sal e.sal%type; v_empno e.empno%type; begin v_sal := :new.sal; v_empno := :new.empno; Autonomous(v_sal,v_empno);--调用过程,过程中声明了自制事务 end; / 练习 9:系统触发器 --------------------------------------------------------------------------------- conn scott/tiger drop table log_table; create table log_table(sid number, serial# number, username varchar2(30), action varchar2(8), log_time varchar2(19)); grant select on scott.log_table to public; grant insert on scott.log_table to public; --用户级别: create or replace trigger scott_logon after logon on schema declare v_sid v$mystat.sid%type; v_serial# v$session.serial#%type; v_username v$session.username%type; begin select sid into v_sid from v$mystat where rownum=1; select serial#,username into v_serial#,v_username from v$session where sid=v_sid; insert into scott.log_table values (v_sid,v_serial#,v_username,‘logon‘,to_char(sysdate,‘yyyy-mm-dd hh24:mi:ss‘)); end scott_logon; / create or replace trigger scott_logof before logoff on schema declare v_sid v$mystat.sid%type; v_serial# v$session.serial#%type; v_username v$session.username%type; v_status v$session.status%type; begin select sid into v_sid from v$mystat where rownum=1; select serial#,username into v_serial#,v_username from v$session where sid=v_sid; insert into scott.log_table values (v_sid,v_serial#,v_username,‘logoff‘,to_char(sysdate,‘yyyy-mm-dd hh24:mi:ss‘)); end scott_logof; / --通过用户级别触发器修改排序方法:(也可以修改语言选项或日期格式) CONN SCOTT/TIGER CREATE OR REPLACE TRIGGER test_tri AFTER LOGON ON schema DECLARE sqlstr VARCHAR2(200) := ‘alter session set nls_sort = SCHINESE_STROKE_M‘; BEGIN execute immediate sqlstr; END test_tri; / --通过数据库级别触发器修改排序方法: CONN / AS SYSDBA CREATE OR REPLACE TRIGGER test_tri AFTER LOGON ON DATABASE DECLARE sqlstr VARCHAR2(200) := ‘alter session set nls_sort = SCHINESE_STROKE_M‘; BEGIN IF (USER = ‘SCOTT‘) THEN execute immediate sqlstr; END IF; END test_tri; / --数据库级别:得具有访问v$的权限! conn sys/oracle as sysdba create or replace trigger LogSchemaConnects after logon on database declare v_sid v$mystat.sid%type; v_serial# v$session.serial#%type; v_username v$session.username%type; begin select sid into v_sid from v$mystat where rownum=1; select serial#,username into v_serial#,v_username from v$session where sid=v_sid; insert into scott.log_table values (v_sid,v_serial#,v_username,‘logon‘,to_char(sysdate,‘yyyy-mm-dd hh24:mi:ss‘)); end LogSchemaConnects; / create or replace trigger dbshutdown before shutdown on database declare v_sid v$mystat.sid%type; v_serial# v$session.serial#%type; v_username v$session.username%type; begin select sid into v_sid from v$mystat where rownum=1; select serial#,username into v_serial#,v_username from v$session where sid=v_sid; insert into scott.log_table values (v_sid,v_serial#,v_username,‘ShutDown‘,to_char(sysdate,‘yyyy-mm-dd hh24:mi:ss‘)); end dbshutdown; / --限制指定用户从指定IP登录: CREATE OR REPLACE TRIGGER TRIGGER_RESTRICT_LOGON AFTER LOGON ON DATABASE DECLARE RESTRICTED_USER VARCHAR2(32) := ‘SCOTT‘; ALLOWED_IP VARCHAR2(16) := ‘10.1.1.157‘; LOGON_USER VARCHAR2(32); CLIENT_IP VARCHAR2(16); BEGIN LOGON_USER := SYS_CONTEXT(‘USERENV‘,‘SESSION_USER‘); CLIENT_IP := NVL(SYS_CONTEXT(‘USERENV‘,‘IP_ADDRESS‘), ‘NULL‘); IF LOGON_USER = RESTRICTED_USER AND CLIENT_IP <> ALLOWED_IP THEN RAISE_APPLICATION_ERROR(-20001, RESTRICTED_USER || ‘ is not allowed to connect from ‘ || CLIENT_IP); END IF; END; / 练习 10:替代触发器(由视图的dml操作所触发) ------------------------------------------------------------------------------------------- CREATE OR REPLACE VIEW VVV AS SELECT E.EMPNO,E.ENAME,D.DEPTNO,D.DNAME,D.LOC FROM EMP E,DEPT D WHERE E.DEPTNO=D.DEPTNO; INSERT INTO VVV VALUES (7777,‘zjz‘,50,‘SALES‘,‘Beijing‘); CREATE OR REPLACE TRIGGER insert_EMP_AND_DEPT INSTEAD OF INSERT ON VVV DECLARE duplicate_info EXCEPTION; PRAGMA EXCEPTION_INIT (duplicate_info, -00001); BEGIN IF INSERTING THEN INSERT INTO DEPT (DEPTNO, DNAME, LOC) VALUES ( :new.DEPTNO, :new.DNAME, :new.LOC); INSERT INTO EMP (EMPNO, ENAME, DEPTNO) VALUES ( :new.EMPNO, :new.ENAME, :new.DEPTNO); ELSIF DELETING THEN NULL; ELSE NULL; END IF; EXCEPTION WHEN duplicate_info THEN RAISE_APPLICATION_ERROR ( num=> -20107, msg=> ‘Duplicate EMP or DEPT ID‘); END insert_EMP_AND_DEPT; /