1、SQL 触发器使用教程和命名规范1,触发器简介触发器(Trigger)是数据库对象的一种,编码方式类似存储过程,与某张表(Table)相关联,当有DML 语句对表进行操作时,可以引起触发器的执行,达到对插入记录一致性,正确性和规范性控制的目的。在当年 C/S 时代盛行的时候,由于客户端直接连接数据库,能保证数据库一致性的只有数据库本身,此时主键(Primary Key),外键(Foreign Key),约束(Constraint)和触发器成为必要的控制机制。而触发器的实现比较灵活,可编程性强,自然成为了最流行的控制机制。到了 B/S 时代,发展成 4 层架构,客户端不再能直接访问数据库,只有中
2、间件才可以访问数据库。要控制数据库的一致性,既可以在中间件里控制,也可以在数据库端控制。很多的青睐 Java 的开发者,随之将数据库当成一个黑盒,把大多数的数据控制工作放在了 Servlet 中执行。这样做,不需要了解太多的数据库知识,也减少了数据库编程的复杂性,但同时增加了 Servlet 编程的工作量。从架构设计来看,中间件的功能是检查业务正确性和执行业务逻辑,如果把数据的一致性检查放到中间件去做,需要在所有涉及到数据写入的地方进行数据一致性检查。由于数据库访问相对于中间件来说是远程调用,要编写统一的数据一致性检查代码并非易事,一般采用在多个地方的增加类似的检查步骤。一旦一致性检查过程发生
3、调整,势必导致多个地方的修改,不仅增加工作量,而且无法保证每个检查步骤的正确性。触发器的应用,应该放在关键的,多方发起的,高频访问的数据表上,过多使用触发器,会增加数据库负担,降低数据库性能。而放弃使用触发器,则会导致系统架构设计上的问题,影响系统的稳定性。2,触发器示例触发器代码类似存储过程,以 PL/SQL 脚本编写。下面是一个触发器的示例:新建员工工资表 salarycreate table SALARY(EMPLOYEE_ID NUMBER, -员工 IDMONTH VARCHAR2(6), -工资月份AMOUNT NUMBER -工资金额)创建与 salary 关联的触发器 sala
4、ry_trg_rai1 Create or replace trigger salary_trg_rai2 After insert on salary3 For each row4 declare5 Begin6 Dbms_output.put_line(员工 ID: | :new.employee_id);7 Dbms_output.put_line(工资月份: | :new.month);8 Dbms_output.put_line(工资: | :new.amount);9 Dbms_output.put_line(触发器已被执行 );10 End;打开一个 SQL Window 窗口(
5、使用 PL/SQL Developer 工具),或在 sqlplus 中输入:Insert into salary(employee_id, month, amount) values(1, 200606, 10000);执行后可以在 sqlplus 中,或在 SQL Window 窗口的 Output 中见到员工 ID:1工资月份:200606工资:10000触发器已执行在代码的第一行,定义了数据库对象的类型是 trigger,定义触发器的名称是 salary_trg_rai第二行说明了这是一个 after 触发器,在 DML 操作实施之后执行。紧接着的 insert 说明了这是一个针对in
6、sert 操作的触发器,每个对该表进行的 insert 操作都会执行这个触发器。第三行说明了这是一个针对行级的触发器,当插入的记录有 n 条时,在每一条插入操作时都会执行该触发器,总共执行 n 次。Declare 后面跟的是本地变量定义部分,如果没有本地变量定义,此部分可以为空Begin 和 end 括起来的代码,是触发器的执行部分,一般会对插入记录进行一致性检查,在本例中打印了插入的记录和“触发器已执行”。其中:new 对象表示了插入的记录,可以通过:new.column_name 来引用记录的每个字段值3,触发器语法和功能触发器的语法如下CREATE OR REPLACE TRIGGER
7、trigger_nameON table_nameFOR EACH ROWWHEN (condition)DECLAREBEGIN-触发器代码END;Trigger_name 是触发器的名称。可以选择 before 或者 after 或 instead of。Before 表示在 DML 语句实施前执行触发器,而 after 表示在在 dml 语句实施之后执行触发器,instead of 触发器用在对视图的更新上。可以选择一个或多个 DML 语句,如果选择多个,则用 or 分开,如:insert or update。Table_name 是触发器关联的表名。FOR EACH ROW为可选项,如
8、果注明了 FOR EACH ROW,则说明了该触发器是一个行级的触发器,DML 语句处理每条记录都会执行触发器;否则是一个语句级的触发器,每个 DML 语句触发一次。WHEN 后跟的 condition 是触发器的响应条件,只对行级触发器有效,当操作的记录满足 condition 时,触发器才被执行,否则不执行。Condition 中可以通过 new 对象和 old 对象(注意区别于前面的:new 和:old,在代码中引用需要加上冒号)来引用操作的记录。触发器代码可以包括三种类型:未涉及数据库事务代码,涉及关联表(上文语法中的 table_name)数据库事务代码,涉及除关联表之外数据库事务代
9、码。其中第一种类型代码只对数据进行简单运算和判断,没有 DML 语句,这种类型代码可以在所有的触发器中执行。第二种类型代码涉及到对关联表的数据操作,比如查询关联表的总记录数或者往关联表中插入一条记录,该类型代码只能在语句级触发器中使用,如果在行级触发器中使用,将会报 ORA-04091 错误。第三种类型代码涉及到除关联表之外的数据库事务,这种代码可以在所有触发器中使用。从触发器的功能上来看,可以分成 3 类: 重写列(仅限于 before 触发器) 采取行动(任何触发器) 拒绝事务(任何触发器)“重写列”用于对表字段的校验,当插入值为空或者插入值不符合要求,则触发器用缺省值或另外的值代替,在多
10、数情况下与字段的 default 属性相同。这种功能只能在行级 before 触发器中执行。“采取行动”针对当前事务的特点,对相关表进行操作,比如根据当前表插入的记录更新其他表,银行中的总帐和分户帐间的总分关系就可以通过这种触发器功能来维护。“拒绝事务”用在对数据的合法性检验上,当更新的数据不满足表或系统的一致性要求,则通过抛出异常的方式拒绝事务,在其上层的代码可以捕获这个异常并进行相应操作。下面将通过举例说明,在例子中将触发器主体的语法一一介绍,读者可以在例子中体会触发器的功能。4,例一:行级触发器之一CREATE OR REPLACE TRIGGER salary_raiuAFTER IN
11、SERT OR UPDATE OF amount ON salaryFOR EACH ROWBEGINIF inserting THENdbms_output.put_line(插入);ELSIF updating THENdbms_output.put_line(更新 amount 列);END IF;END;以上是一个 after insert 和 after update 的行级触发器。在第二行中 of amount on salary的意思是只有当 amount 列被更新时,update 触发器才会有效。所以,以下语句将不会执行触发器:Update salary set month =
12、 200601 where month = 200606;在触发器主体的 if 语句表达式中,inserting, updating 和 deleting 可以用来区分当前是在做哪一种 DML 操作,可以作为把多个类似触发器合并在一个触发器中判别触发事件的属性。5,例二:行级触发器之二新建员工表 employmentCREATE TABLE EMPLOYMENT(EMPLOYEE_ID NUMBER, -员工 IDMAXSALARY NUMBER -工资上限)插入两条记录Insert into employment values(1, 1000);Insert into employment
13、values(2, 2000);CREATE OR REPLACE TRIGGER salary_raiuAFTER INSERT OR UPDATE OF amount ON salaryFOR EACH ROWWHEN ( NEW.amount = 1000 AND (old.amount IS NULL OR OLD.amount v_maxsalary THENraise_application_error(-20000, 工资超限);END IF;END;以上的例子引入了一个新的表 employment,表中的 maxsalary 字段代表该员工每月所能分配的最高工资。下面的触发器根
14、据插入或修改记录的 employee_id,在 employment 表中查到该员工的每月最高工资,如果插入或修改后的 amount 超过这个值,则报错误。代码中的 when 子句表明了该触发器只针对修改或插入后的 amount 值超过 1000,而修改前的 amount值小于 500 的记录。New 对象和 old 对象分别表示了操作前和操作后的记录对象。对于 insert 操作,由于当前操作记录无历史对象,所以 old 对象中所有属性是 null;对于 delete 操作,由于当前操作记录没有更新对象,所以 new 对象中所有属性也是 null。但在这两种情况下,并不影响 old 和 ne
15、w 对象的引用和在触发器主体中的使用,和普通的空值作同样的处理。在触发器主体中,先通过:new.employee_id,得到该员工的工资上限,然后在 if 语句中判断更新后的员工工资是否超限,如果超限则错误代码为-20000,错误信息为“工资超限”的自定义错误。其中的raise_application_error 包含两个参数,前一个是自定义错误代码,后一个是自定义错误代码信息。其中自定义错误代码必须小于或等于-20000。执行完该语句后,一个异常被抛出,如果在上一层有exception 子句,该异常将被捕获。如下面代码:DECLAREcode NUMBER;msg VARCHAR2(500)
16、;BEGININSERT INTO salary (employee_id, amount) VALUES (2, 5000);EXCEPTIONWHEN OTHERS THENcode := SQLCODE;msg := substr(SQLERRM, 1, 500);dbms_output.put_line(code);dbms_output.put_line(msg);END;执行后,将在 output 中或者 sqlplus 窗口中见着以下信息:-20000ORA-20000: 工资超出限制ORA-06512: 在“SCOTT.SALARY_RAI“, line 9ORA-04088:
17、 触发器 SCOTT.SALARY_RAI 执行过程中出错这里的 raise_application_error 相当于拒绝了插入或者修改事务,当上层代码接受到这个异常后,判断该异常代码等于-20000,可以作出回滚事务或者继续其他事务的处理。以上两个例子中用到的 inserting, updating, deleting 和 raise_application_error 都是dbms_standard 包中的函数,具体的说明可以参照 Oracle 的帮助文档。create or replace package sys.dbms_standard isprocedure raise_appl
18、ication_error(num binary_integer, msg varchar2,function inserting return boolean;function deleting return boolean;function updating return boolean;function updating (colnam varchar2) return boolean;end;对于 before 和 after 行级触发器,:new 和:old 对象的属性值都是一样的,主要是对于在 Oracle 约束(Constraint)之前或之后的执行触发器的选择。需要注意的是,可
19、以在 before 行触发器中更改:new 对象中的值,但是在 after 行触发器就不行。下面介绍一种 instead of 触发器,该触发器主要使用在对视图的更新上,以下是 instead of 触发器的语法:CREATE OR REPLACE TRIGGER trigger_nameINSTEAD OF ON view_nameFOR EACH ROWWHEN (condition)DECLAREBEGIN-触发器代码END;其他部分语法同前面所述的 before 和 after 语法是一样的,唯一不同的是在第二行用上了 instead of 关键字。对于普通的视图来说,进行 inser
20、t 等操作是被禁止的,因为 Oracle 无法知道操作的字段具体是哪个表中的字段。但我们可以通过建立 instead of 触发器,在触发器主体中告诉 Oracle 应该更新,删除或者修改哪些表的哪部分字段。如:6,例三:instead of 触发器新建视图CREATE VIEW employee_salary(employee_id, maxsalary, MONTH, amount) AS SELECT a.employee_id, a.maxsalary, b.MONTH, b.amountFROM employment a, salary bWHERE a.employee_id =
21、b.employee_id如果执行插入语句INSERT INTO employee_salary(employee_id, maxsalary, MONTH, amount)VALUES(10, 100000, 200606, 10000);系统会报错:ORA-01779:无法修改与非键值保存表对应的列我们可以通过建立以下的 instead of 存储过程,将插入视图的值分别插入到两个表中:create or replace trigger employee_salary_riiinstead of insert on employee_salary for each ROWDECLAREv_
22、cnt NUMBER;BEGIN-检查是否存在该员工信息SELECT COUNT(*)INTO v_cntFROM employmentWHERE employee_id = :NEW.employee_id;IF v_cnt = 0 THENINSERT INTO employment(employee_id, maxsalary)VALUES(:NEW.employee_id, :NEW.maxsalary);END IF;-检查是否存在该员工的工资信息SELECT COUNT(*)INTO v_cntFROM salaryWHERE employee_id = :NEW.employee
23、_idAND MONTH = :NEW.MONTH;IF v_cnt = 0 THENINSERT INTO salary(employee_id, MONTH, amount)VALUES(:NEW.employee_id, :NEW.MONTH, :NEW.amount);END IF;END employee_salary_rii;该触发器被建立后,执行上述 insert 操作,系统就会提示成功插入一条记录。但需要注意的是,这里的“成功插入一条记录”,只是 Oracle 并未发现触发器中有异常抛出,而根据insert 语句中涉及的记录数作出一个判断。若触发器的主体什么都没有,只是一个空语
24、句,Oracle 也会报“成功插入一条记录”。同样道理,即使在触发器主体里往多个表中插入十条记录,Oracle 的返回也是“成功插入一条记录”。行级触发器可以解决大部分的问题,但是如果需要对本表进行扫描检查,比如要检查总的工资是否超限了,用行级触发器是不行的,因为行级触发器主体中不能有涉及到关联表的事务,这时就需要用到语句级触发器。以下是语句级触发器的语法:CREATE OR REPLACE TRIGGER trigger_nameON table_nameDECLAREBEGIN-触发器主体END;从语法定义上来看,行级触发器少了 for each row,也不能使用 when 子句来限定入
25、口条件,其他部分都是一样的,包括 insert, update, delete 和 instead of 都可以使用。7,例四:语句级触发器之一CREATE OR REPLACE TRIGGER salary_saiuAFTER INSERT OR UPDATE OF amount ON salaryDECLAREv_sumsalary NUMBER;BEGINSELECT SUM(amount) INTO v_sumsalary FROM salary;IF v_sumsalary 500000 THENraise_application_error(-20001, 总工资超过 500000
26、);END IF;END;以上代码定义了一个语句级触发器,该触发器检查在 insert 和 update 了 amount 字段后操作后,工资表中所有工资记录累加起来是否超过 500000,如果超过则抛出异常。从这个例子可以看出,语句级触发器可以对关联表表进行扫描,扫描得到的结果可以用来作为判断一致性的标志。需要注意的是,在before 语句触发器主体和 after 语句触发器主体中对关联表进行扫描,结果是不一样的。在 before语句触发器主体中扫描,扫描结果将不包括新插入和更新的记录,也就是说当以上代码换成 before 触发器后,以下语句将不报错:INSERT INTO salary(e
27、mployee_id, month, amount) VALUEs(2, 200601, 600000)这是因为在主体中得到的 v_sumsalary 并不包括新插入的 600000 工资。另外,在语句级触发器中不能使用:new 和:old 对象,这一点和行级触发器是显著不同的。如果需要检查插入或更新后的记录,可以采用临时表技术。临时表是一种 Oracle 数据库对象,其特点是当创建数据的进程结束后,进程所创建的数据也随之清除。进程与进程不可以互相访问同一临时表中对方的数据,而且对临时表进行操作也不产生 undo 日志,减少了数据库的消耗。具体有关临时表的知识,可以参看有关书籍。为了在语句级触
28、发器中访问新插入后修改后的记录,可以增加行级触发器,将更新的记录插入临时表中,然后在语句级触发器中扫描临时表,获得修改后的记录。临时表的表结构一般与关联表的结构一致。8,例五:语句级触发器之二目的:限制每个员工的总工资不能超过 50000,否则停止对该表操作。创建临时表create global temporary table SALARY_TMP(EMPLOYEE_ID NUMBER,MONTH VARCHAR2(6),AMOUNT NUMBER)on commit delete rows;为了把操作记录插入到临时表中,创建行级触发器:CREATE OR REPLACE TRIGGER sa
29、lary_raiuAFTER INSERT OR UPDATE OF amount ON salaryFOR EACH ROWBEGININSERT INTO salary_tmp(employee_id, month, amount)VALUES(:NEW.employee_id, :NEW.MONTH, :NEW.amount);END;该触发器的作用是把更新后的记录信息插入到临时表中,如果更新了多条记录,则每条记录都会保存在临时表中。创建语句级触发器:CREATE OR REPLACE TRIGGER salary_saiAFTER INSERT OR UPDATE OF amount ON salaryDECLAREv_sumsalary NUMBER;