书城计算机数据库原理及Oracle应用
31848700000016

第16章 PL/SQL高级编程

7.1 存储过程函数的概念

第6章介绍了PL/SQL的无名块。无名块不存储在数据库中,并且不能从其他的PL/SQL块中进行调用。本章将介绍存储过程、函数、包是以编译形式存储在数据库数据字典中的有名块,可以在客户端与服务器端的任何工具和任何应用中运行。

存储程序主要包括四类:存储过程、存储函数、包和触发器。过程执行某一动作,函数计算一个值,包将相关的过程函数逻辑上捆绑在一起,触发器隐式执行某一动作。

7.1.1 存储过程与应用程序的区别

过程和函数是有名的PL/SQL块,可以在各种开发环境中创建,这些过程和函数称为子程序,包括存储在服务器端的存储子程序和存储在应用程序中的应用子程序。

7.1.2 开发存储过程和函数

7.1.2.1 开发一个存储过程和函数的步骤

用文本编辑器编写一个含有Create Procedure或Create Function语句的PL/SQL脚本文件,(如:c:\procedure1.sql),在写脚本时,充分考虑出错处理。

在SQL*Plus中用命令(如:SQL>start c:\procedure1.sql)编译脚本文件,调试编译错误。不论编译是否成功,只要进行编译,系统就将创建过程或函数的源代码存入数据字典中。只有编译通过后才产生编译代码pcode,并把编译代码存储到数据库数据字典中。

在编译成功并将编译代码存入数据字典后,在Oracle任何应用环境中就可以调用存储函数或过程。不同的环境调用同一个过程函数的语法不同,调用过程与调用函数的语法和命令不同(参见7.1.4.2节)。

在运行期间调试存储过程、函数的逻辑错误。

7.1.2.2 创建存储过程

创建存储过程的语法如下:

CREATE[OR REPLACE]PROCEDURE 过程名

[(参数名[IN|OUT|IN OUT]数据类型……)]

{IS|AS}

[说明部分]

BEGIN

语句序列

[EXCEPTION 出错处理]

END[过程名];

从上述语法可知:OR REPLACE 是一个可选的关键字,建议用户使用此关键字,以便替代原有的过程。关键字AS 和IS 均可,它们本身没有区别。IS 后面是一个完整的PL/SQL块的三部分,可以定义局部变量、游标等,但不能以DECLARE开始。

形式参数可以有三种模式:IN,OUT和IN OUT。如果没有为形式参数指定模式,那么默认是IN模式。如果形式参数的数据类型是字符型的,则不要指定长度,而由调用环境决定。

用来从调用环境中向存储过程传递值,过程体内不能给IN模式参数赋值,即IN模式参数不能出现在赋值语句的左边。给此参数传递的值可以是常量、有值的变量、表达式等OUT参数(输出参数)

用来从存储过程中返回值给调用者。在过程体内,必须给OUT模式参数赋值,OUT模式参数可以出现在赋值语句的左边。没有值的OUT模式参数不能出现在赋值语句的右边IN OUT参数(输入输出参数)

7.1.2.4 存储过程与函数的区别

存储过程和函数的主要差别有两个:一是返回值的方法不同,二是调用方法不同。

1.返回值的方法不同

存储函数有零个或多个参数,函数可以有IN参数,但不能有OUT参数。函数只返回一个值,函数值的返回是靠RETURN子句返回的。

存储过程有零个或多个参数,过程可以有IN参数,在调用过程时通过参数列表接受IN参数的输入。过程不返回值,其返回值是靠OUT参数带出来的,过程可以有零个或多个OUT参数返回结果。

2.调用方法不同

调用过程的语句可以作为单独的可执行语句在PL/SQL块中单独出现。如:

过程名(实际参数1,实际参数2……);

函数可以在任何表达式能够出现的地方被调用,调用函数的语句不能作为可执行语句单独出现在PL/SQL块中。如:

变量名:函数名(实际参数1,实际参数2……)

7.1.2.5 存储过程和函数中的出错处理

存储过程和函数就是一个PL/SQL块,在第6章中已介绍一个完整的PL/SQL块应该考虑出错处理,所以在过程函数体内应该考虑出错处理。这种出错情况可能是用户自定义的错误或者是系统预定义的错误(参见第6章的出错处理部分)。不管是哪种类型的出错情况,只要在过程函数体内考虑了出错处理的方法,即使在运行过程中出现了错误,过程函数都能成功地执行,程序不会被中断。如果在过程和函数体内忽略了出错处理,过程和函数执行时以交互方式通知用户出错,让用户自行处理,程序被中断。

例7-7 根据给定的员工号,删除该员工记录。按照用户自定义的出错处理步骤进行出错处理。首先定义一个错误,当给定的员工号在emp表中不存在时触发错误,最后通过ROLLBACK和往exceptiontable表中插入一行来处理错误。

CREATE OR REPLACE PROCEDURE fireemp(pempno IN emp.empno%TYPE)

IS

invalidemployee EXCEPTION;(定义错误)

BEGIN

DELETE FROM emp WHERE empnopempno

IF SQL%NOTFOUND THEN

RAISE invalidemployee;(触发错误)

END IF;

COMMIT WORK;

EXCEPTION

WHEN invalidemployee THEN(处理错误)

ROLLBACK WORK;

INSERT INTO exceptiontable(linenr,line)

VALUES(1,′Employee does not exist.′);

WHEN others THEN

Dbmsoutput.putline(sqlerrm)

END fireemp;

/

当程序编译通过后存入数据字典,然后就可以在其他应用环境中调用该过程。

例7-8 用下列命令在SQL*PLUS中调用该过程,删除员工号9999的员工。

SQL>EXECUTE fireemp(9999);

执行后显示PL/SQL procedure successfully completed,即调用过程成功。虽然在emp表中根本没有员工号为9999的员工,但过程成功执行,没有被中断。

7.1.3 存储过程和函数的管理

首先编写创建存储过程或函数的SQL*Plus脚本文件,文件的最后以斜杠“/”作为结束标志;编译该文件并将其存入数据库数据字典中。可以使用一些SQL命令完成对已存入数据库中的过程和函数进行管理。

7.1.3.1 过程和函数的管理命令

过程、函数的源代码可通过查询数据字典视图USER SOURCE获得。只要查询该视图中的TEXT列即可得到过程、函数的源代码。

过程、函数的编译出错信息可通过查询数据字典视图USERERRORS获得。只要查询该视图的LINE,POSITION,TEXT列即可知道出错的行号、列号及错误信息。也可以在SQL*Plus中用SHOW ERRORS命令显示刚编译的过程函数的出错信息,如:

SQL>SHOW ERRORS

7.1.4 存储过程和函数的调用和测试

过程和函数一旦被存入数据库数据字典,就可从各种Oracle环境中(既可以通过命令行方式,也可以从一个具体应用的代码中)调用它,调用时必须传递相应的参数。

7.1.4.1 参数传值

调用含有形式参数的过程和函数时,必须有对应的实际参数。一般采用位置对应法向形式参数传值,要求实际参数与形式参数保持次序、类型及个数的一致。

例7-10 从SQL*Plus命令中通过位置对应法调用HIREEMP过程。因为该过程有OUT 模式的形式参数,必须定义一个接受OUT参数返回值的变量,作为OUT模式参数的实际参数。

SQL>variable vename varchar2(12);

SQL>EXECUTE hireemp(9999,vename);

注意:

如果形式参数是IN(输入)模式的参数,实际参数可以是一个具体的值,或一个有值的变量。

如果形式参数是OUT(输出)模式的参数,实际参数必须是一个变量。当调用过程后,此变量就被赋值了。可以调用系统内置的DBMSOUTPUT包中的过程输出此变量的值,测试过程执行的结果。

如果形式参数是IN OUT(输入输出)模式的参数,则实际参数必须是一个预先已经赋值的变量。执行完过程后,该变量被重新赋值。可以调用系统内置的DBMSOUTPUT包中的过程输出此变量的值,测试过程执行的结果。

7.1.4.2 存储过程和函数的调用方法

过程和函数一旦被存入数据库数据字典,就可从各种Oracle环境中调用它。函数与过程的调用方法类似,需要注意的是,函数需要返回值。所以函数不能单独作为一条语句出现,只能作为语句中的表达式出现;过程可以单独作为一条语句出现。

2.在SQL*Plus中调用

(1)过程的调用

例7-13 从SQL*Plus中调用过程QUERYEMP,该过程有一个IN参数,三个OUT 参数。必须在SQL*Plus中用VARIABLE定义三个变量,执行过程时给这三变量赋值。可以用下列SQL*Plus的脚本文件实现。

SET SERVEROUTPUT ON/*激活DBMSOUTPUT 系统包*/

ACCEPT pempno PROMPT′please enter the employee number:′/*接受员工号*/

VARIABLE vempname varchar2(14);/*定义存放OUT参数输出结果的变量*/

VARIABLE vempsal number;

VARIABLE vempcomm number;

EXECUTE queryemp(&pempno,:vempname,:vempsal,:vempcomm)

EXECUTE dbmsoutput.putline(′Information for employee:′||tochar(&pempno));

EXECUTE dbmsoutput.putline(′The name is:′||:vempname);

EXECUTE dbmsoutput.putline(′the salary is:′||tochar(:vempsal));

EXECUTE DBMSoutput.putline(′The commission is:′||tochar(:vempcomm));

注意:在SQL*Plus中,用VARIABLE定义的变量在引用时,必须前面加冒号(:)。用ACCEPT接受的变量在引用时,前面加&符号。

(2)函数的调用

函数不能作为一条语句单独出现,只能出现在别的过程中作为别的过程的参数。

例7-14 函数的调用。

SQL>EXECUTE dbmsoutput.putline(′员工号7654的工资是:′||getsal(7654))

或者用SELECT语句查询函数的结果,如:

SQL>SELECT getsal(7654)FROM dual;

调用函数averagesal,计算3人的平均工资。运行过程中显示每一步的测试信息。

SQL>SELECT averagesal(3)FROM dual;

3.在PRO*C中调用过程

例7-15 从一个PRO*C程序中调用过程FIREEMP。

void runfireemp()

{

EXEC SQL BEGIN DECLARE SECTION;

int empno;

EXEC SQL END DECLARE SECTION;

printf(′\nPlease enter the employee number:′);

scanf(′%d′,&empno);

EXEC SQL fireemp(:empno);/*在过程名前面加EXEC SQL关键字*/

RETURN;

}

7.1.5 存储过程和函数的安全性

存储过程、函数和包是存储在数据字典中的对象,它们是属于某一数据库用户的。该用户对其所拥有的对象可以进行任何操作。其他用户在被授予了合适的权限以后也可以访问这些对象。

通过区分过程或函数的拥有者和使用者来进行安全性管理,这与它们存取数据的安全性管理一样。过程或函数的拥有者比其使用者有着较高的权限。过程的拥有者需要拥有CREATE PROCEDURE或CREATE ANY PROCEDURE系统权限,才能建立过程,需要拥有DROP ANY PROCEDURE系统权限才能删除过程,拥有对过程的执行权限。过程的使用者需要拥有EXECUTE ANY PROCEDURE系统权限才能执行别人所拥有的过程。

例7-16 过程的安全性控制机制。Scott用户拥有对emp表操作所需的相应权限,以及建立过程的权限后,就可以建立过程FIREEMP。然后Scott用户用下列命令授予GREEN用户对FIREEMP过程的EXECUTE权限。

SQL>GRANT EXECUTE

ON fireemp

TO green;

GREEN用户就可以调用scott用户所建的过程,例如:

SQL>EXECUTE scott.fireemp(7788);

7.1.6 存储过程和函数的优点

除了可以使应用规范和清晰以外,存储过程和函数还具有以下优点。

(1)提高数据的安全性与完整性

利用安全性的权限来控制那些没有足够权限的用户对数据库的间接访问;通过把相关联的表的操作集中到一起,来保证对这些相关表执行一致的操作。

(2)改善操作性能

多个用户使用同一个SQL语句时,只做一次语法分析;只在编译时进行语法分析,运行时不做语法分析,直接调用编译代码;把若干命令组合在一起,可以减少访问数据库的次数,降低网络传输量。

(3)节省存储量

对不同应用,只在数据库中存储代码的一个备份,而不是每个应用存储一份;通过共享SQL来避免为不同用户申请不同游标,改善管理;提高可维护性;可以在不打扰其他用户的情况下修改运行的过程;对某个过程的修改可以同时作用于多个应用;过程利用了共享内存资源。

(4)模块化

过程是模块化的,这意味着用户可以将程序分割成定义清晰、易于管理的单元。

7.2 包

包是一个可以将相关对象存储在一起的PL/SQL结构,它包含了两个分离的组成部分:包说明(specification)和包主体(body)。这两部分被单独存储在数据字典中。包说明是一个操作接口,对应用来说是可见;包主体是黑盒,对应用来说隐藏了实现细节。除了可以将相关对象作为一组存储在一起以外,包在依赖性方面的限制比较小,并且有许多性能上的优点。

7.2.1 包的组成

可以将相关的若干程序单元组织到一块,用一个包来标识这个集合。

带有参数的程序函数(Function)

带有参数的程序,该程序只返回一个值变量(Variable)

用于存储变化值的存储单元游标(Cursor)

指向一个SQL语句的指针类型(TYPE)

定义一个结构类型(如RECORD、VARRAY等)常量(Constant)

指向常数的指针出错情况(Exception)

标志一个出错情况

7.2.2 开发包

7.2.2.1 开发包的步骤

开发包的步骤与开发存储过程的步骤很相似。可按下述步骤开发一个包。

在一个文本文件中写入CREATE PACKAGE 语句创建一个包的说明。

在另一个文本文件中用CREATE PACKAGE BODY语句创建包主体。

在SQL*Plus中编译这两个文本文件,以将包的源代码编译成编译代码(P-Code),并将这两种代码存入数据库数据字典中。必须先编译创建包说明的文本文件,再编译创建包主体的文本文件。

从Oracle环境,或者从一个独立的函数或过程中调用包内的公共元素。

7.2.2.3 创建包说明

例7-17 制作一个包的说明。生成一个管理雇员薪水的包salpackage,其中包括一个为雇员加薪的过程和降薪的过程,并且在包中还有两个记录所有雇员薪水增加和减少的全局变量。

CREATE PACKAGE salpackage IS

PROCEDURE raisesal(vempno emp.empno%TYPE,

vsalincrement emp.sal%TYPE);

PROCEDURE reducesal(vempno emp.empno%TYPE,

vsalreduce emp.sal%TYPE);

vraisesal emp.sal%TYPE:0;

vreducesal emp.sal%TYPE:0;

END;

/

7.2.2.3 创建包主体

创建包主体的语法如下:

CREATE[OR REPLACE]PACKAGE BODY 包名

{IS|AS}

私有变量的定义

私有类型的定义

私有出错处理的定义

私有游标的定义

函数定义

过程定义

END;

/

例7-18 制作salpackage包的包主体。

CREATE OR REPLACE PACKAGE BODY salpackage IS

PROCEDURE raisesal(vempno emp.empno%TYPE,

vsalincrement emp.sal%TYPE)

IS

BEGIN

UPDATE emp SET salsal+vsalincrement WHERE empnovempno;

COMMIT WORK;

vraisesal:vraisesal+vsalincrement;

END;

PROCEDURE reducesal(vempno emp.empno%TYPE,

vsalreduce emp.sal%TYPE)

IS

BEGIN

UPDATE emp SET salsal-vsalreduce WHERE empnovempno;

COMMIT WORK;

vreducesal:vreducesal+vsalreduce;

END;

END;

/

注释:包中NUMBER型变量默认初始化值为0,VARCHAR2型变量默认初始化值为空。

7.2.3 包的管理

7.2.3.1 包的管理命令

包的管理和一个单独的存储过程的管理一样。

注意:当包的说明被删除时,要求把包主体也删除;当删除包主体时,可以不删除包的说明。

7.2.3.2 查看包的文档信息

可以从相应数据字典视图中获得包的文档信息和出错信息。

通过查询数据字典视图USER SOURCE中的TEXT列即可得到包的源代码。

通过查询数据字典视图USERERRORS中的LINE,POSITION,TEXT列即可知道出错的行号、列号及错误信息。也可以在SQL*Plus中用SHOW ERRORS命令显示刚编译的包的出错信息。

7.2.4 包的调用

在包的说明部分中声明的所有对象在包外可以被调用,只要指定包的名字即可访问该对象。包中过程函数的调用和单独存储过程函数的调用方法相同,唯一的区别在于在过程名前面必须指明其所在的包的名字。与调用单独存储过程的方式类似,在不同的环境中调用包中过程的语法有差别,见表7-6。

例7-19 从一个单独存储过程中调用包中的过程(作为单独的可执行语句)。

salpackage.raisesal(7788,1000)/*给7788员工涨1000元*/

例7-20 从SQL*Plus工具中调用包中的过程。

SQL>EXECUTE salpackage.raisesal(7788,1000);

注意:在不同包内的两个不同过程可以有相同的名字。

7.2.5 系统包

Oracle事先定义的包称为系统包,这些包可以供用户使用。

7.3 触发器

7.3.1 触发器的基本概念和组成

7.3.1.1 触发器的基本概念

触发器类似于存储过程和函数,都是由说明部分、语句执行部分和出错处理部分三部分组成的PL/SQL有名块,触发器存储在数据库数据字典中。但是,对于存储过程而言,可以在另一个程序中调用过程,显式地执行一个过程,同时在调用时可以向存储过程传递参数。对于触发器而言,当触发事件发生时隐式地(自动地)执行该触发器,不能在程序中调用触发器,并且触发器不接受参数。

可以触发触发器的事件包括:在数据库表上执行的INSERT,UPDATE,DELETE操作。

使用触发器可以完成以下任务:

维护不可能在表创建时通过说明性约束进行的复杂的完整性约束限制。

通过记录所进行的修改及谁做了修改等信息对表进行审计。

当表被修改的时候,自动给需要执行操作的程序发信号。

7.3.2 创建触发器

7.3.2.1 开发触发器的步骤

用文本编辑器编写一个含有CREATE TRIGGER语句的创建触发器的脚本文件(如:c:\createtrigger.sql)。在写脚本文件时,充分考虑运行时的出错处理。必须在脚本文件的最后以“/”作为结束符。

在SQL*Plus中用下列命令编译脚本文件:

SQL>start c:\createtrigger.sql

不管编译是否成功,系统将源代码存入数据库数据字典中,如果编译成功,则产生编译代码pcode,并把编译代码pcode也存储到数据库数据字典中。

如果编译有错,则源代码存入数据库数据字典,没有产生编译代码。此时调试编译错误,重新编译。直到编译成功为止。

在SQL*Plus中测试触发器,查看触发器是否有逻辑错误。在SQL*Plus中用DML语句(INSERT、UPDATE、DELETE语句)对表进行操作,查看触发器是否按要求执行。如果定义触发器时的触发事件是INSERT,则对表执行INSERT操作。如果定义触发器时的触发事件是DELETE,则对表执行DELETE操作。如果定义触发器时的触发事件是UPDATE,则对表执行UPDATE操作。

7.3.2.2 语句级触发器

1.创建语句级触发器的语法

用CREATE TRIGGER语句创建一个语句级触发器,该触发器在一个数据操作语句发生时只触发一次。创建一个语句级触发器的语法如下:

CREATE[OR REPLACE]TRIGGER triggername

{BEFORE|AFTER}event1[OR event2……]ON tablename

PL/SQL block

其中:trigger name指触发器名。timing指明触发事件触发的时间。有两种可能的值:BEFORE和AFTER。event指明触发事件的数据操纵语句。有三种可能的值:INSERT,UPDATE 或UPDATE OF 列名1[,列名2……],DELETE。table name 标明与该触发器相关联的表名。PL/SQL block指触发器体,指明该触发器将执行的操作。

注意:

当触发器已经存在时,用REPLACE选项,表示替代已有的触发器。

用UPDATE OF 列名……表明UPDATE事件只有在修改特定的列时才触发。否则,修改任何一列都触发。

例7-24 创建一个BEFORE型语句级触发器。限制一周内向emp表插入数据的时间,如果是周六、周日,或晚上6点到第二天早上8点之间插入,则中断操作,并提示用户不允许在此时间向emp表插入。

CREATE OR REPLACE TRIGGER secureemp

BEFORE INSERT ON emp

BEGIN

IF(TOCHAR(sysdate,′DY′)IN(′SAT′,′SUN′))

OR(TOCHAR(sysdate,′HH24′)NOT BETWEEN′8′AND′18′)THEN

RAISEAPPLICATIONERROR(-20500,

′you may only insert into emp during normal hours.′)

END IF;

END;

/

2.使用触发器谓词(INSERTING、UPDATING、DELETING)

DML触发器是一个INSERT、UPDATE、DELETE触发器。可以创建一个包含多个触发事件的触发器,在触发器体中使用谓词判断是哪个触发事件触发了触发器,从而把多种触发事件组成一个触发器。这些谓词包括INSERTING,UPDATING和DELETING,这些谓词的值是布尔型的,由系统根据触发事件来决定其值是TRUE还是FALSE。

7.3.2.3 行级触发器

1.创建行级触发器的语法

通过在CREATE TRIGGER语句中指定FOR EACH ROW子句创建一个行级触发器,使其在受到触发事件影响的每一行上都被触发。创建一个行级触发器的语法如下:

CREATE[OR REPLACE]TRIGGER triggername

{BEFORE|AFTER}event1[OR event2……]ON tablename

FOR EACH ROW[WHEN restrictingcondition]

PL/SQL block

其中:restricting condition为指定的限制条件,以确定触发器体是否被执行。在触发事件发生并满足此限制条件时,触发器体被执行。否则,触发器体不被执行。

例7-26 创建一个行级触发器。将每个用户对数据库emp表进行数据操纵(插入、更新、删除)的次数记录到audittable表中。

CREATE OR REPLACE TRIGGER auditemp

AFTER DELETE OR INSERT OR UPDATE ON emp

FOR EACH ROW

BEGIN

IF DELETING THEN

UPDATE audittable SET del=del+1

WHERE usernameuser AND tablename=′emp′AND colunname IS NULL;

ELSIF INSERTING THEN

UPDATE audittable SET insins+1

WHERE usernameuser AND tablename′emp′AND columnname IS NULL;

EISE

UPDATE audittable SET updupd+1

WHERE username user AND tablename=′emp′AND columnname IS NULL;

END IF;

END;

/

2.使用行级触发器的标识符(:OLD和:NEW)

当编写触发器时,如果需要引用被插入和被删除记录的值,或者被更新记录的更新前和更新后的值,标识符:OLD和:NEW就是为这种用途提供的。其功能与一个PL/SQL记录很类似,所以也称为伪记录。

在BEFORE型行级触发器和AFTER型行级触发器中使用这些标识符。

在语句级触发器中不要使用这些标识符。

在触发器体的SQL语句或PL/SQL语句中使用这些标识符时,前加冒号(:)。

在行级触发器的WHEN限制条件中使用这些标识符时,前面不要加冒号(:)。

例7-28 在行级触发器中获取某列的新值和旧值,为emp表中的所有数据保留一个历史档案。

CREATE OR REPLACE TRIGGER auditempvalues

AFTER DELETE OR INSERT OR UPDATE ON emp

FOR EACH ROW

BEGIN

INSERT INTO auditemp(username,timestamp,empno,oldename,newename,

oldjob,newjob,oldsal,newsal)

VALUES(USER,SYSDATE,old.empno,old.ename,new.ename,

:old.job,new.job,old.sal,new.sal);

END;

/

当在行级触发器中使用WHEN子句,保证当行级触发器被触发时只有在当前行满足一定限制条件时,才执行触发器体的PL/SQL语句。WHEN后面是一个布尔表达式,它将对每一行进行求值。当布尔表达式的值为TRUE时,执行触发器体。如果为FALSE,不执行触发器体。

例7-29 在行级触发器加WHEN限制条件。根据销售员工资的改变自动计算销售员的奖金。

CREATE OR REPLACE TRIGGER derivecomm

BEFORE UPDATE OF sal ON emp

FOR EACH ROW

WHEN(new.job=′SALESMAN′)

BEGIN

:new.comm:old.comm*(:new.sal/:old.sal);

END;

/

7.3.3 触发器的管理

7.3.3.1 触发器的管理命令

管理触发器的命令与管理存储过程的命令相类似。

与存储过程不同的是,当一个触发器不再适宜被触发时,可以使其处于无效状态,即关闭触发器。关闭触发器与删除触发器是有区别的,删除触发器是从数据字典中永久删除,而关闭触发器只是让触发器失效,临时不能被触发,没有从数据字典中删除,在需要时可以再让其生效(即打开)。使用ALTER TRIGGER命令可以打开和关闭触发器,语法如下:

ALTER TRIGGER 触发器名{DISABLE|ENABLE}

其中:DISABLE表示关闭触发器,ENABLE表示打开触发器。

注意:

当触发器刚创建时,它被自动设置为有效。可查看数据字典视图USERTRRIGERS的STATUS列了解触发器的状态,看其是否有效。

为了改善性能,以及在大量装载数据时(例如使用SQL*Loader)避免进行数据的完整性校验,可以使数据库触发器暂时失效。

当发生网络连接故障、磁盘损坏、数据文件脱机或表空间脱机从而导致触发器中涉及的表不能再被访问时,应使触发器失效。

7.3.4 测试触发器

可以使用多种方法测试触发器:用多种触发事件(如:INSERT、UPDATE、DELETE等)分别对触发器进行测试,以确保触发器的正确运行;用满足WHEN条件的触发事件进行测试;在触发器内利用DBMSOUTPUT包输出运行结果进行测试。

例7-31 用下列命令测试DERIVECOMM触发器。

因为该触发器有一个WHEN限制条件,只有更新销售员的工资时,触发器才更新其奖金。所以先用SELECT语句查看销售员的员工号。假如查询结果有员工号7788,工资2000,奖金800,则用UPDATE语句将7788的工资乘2。

SQL>SELECT empno,sal,comm FROM emp WHERE job′SALESMAN′

SQL>UPDATE emp SET sal=sal*2 WHERE empno7788

再用SELECT语句查看奖金是否也乘2。如果发现奖金变成1600,则表示触发器已触发。

SQL>SELECT empno,sal,comm FROM emp WHERE job′SALESMAN′

例7-32 对auditempvalues触发器,可以用下列命令进行测试。

首先用CREATE TABLE语句建立保留emp历史档案信息的auditemp表,建立后该表是空的。然后对emp表执行INSERT、UPDATE、DELETE操作,查看auditemp表是否已增加记录及增加记录的值的变化。

SQL>CREATE TABLE auditemp(

username varchar2(30),

timestamp date,

empno number(4),

oldename varchar2(12),

newename varchar2(12),

oldjob varchar2(12),

newjob varchar2(12),

oldsal number,

newsal number)

SQL>INSERT INTO emp(empno,ename,job,sal,deptno)

VALUES(1111,′张三′,′SALESMAN′,3000,20)

SQL>UPDATE emp SET ename′李四′,sal5000 WHERE empno1111

SQL>DELETE FROM emp WHERE empno1111

查看emp表的记录没有变化,因为刚才插入的记录又被删除了。但查看auditemp表就会发现该表中增加了三条记录,分别记载了刚才插入、更新、删除的数据。

SQL>SELECT*FROM auditemp

7.3.5 触发器的应用

创建数据库触发器可以大大增强Oracle系统的性能,完成一些Oracle系统本身提供的服务所不能完成的功能。触发器的应用主要在安全性、审计、数据完整性、参考完整性和数据复制几个方面。

7.3.5.1 安全性

在Oracle中通过给用户授权完成对表中数据进行操作的安全性控制。

例7-33 使用权限控制安全性。用下列语句限制用户CLERK对emp表的访问权。

SQL>GRANT SELECT,INSERT,UPDATE,DELETE ON emp TO CLERK;

授权后,用户CLERK随时都可以对emp表进行访问。Oracle系统无法对其进行限制。

但如果希望限制CLERK用户只能在特定时间内访问emp表,非规定时间内不允许访问,则可以使用数据库触发器,实现进一步的安全性控制。

例7-34 在触发器中控制数据的安全性。限制只允许在某周的特定时间内进行数据操作。

CREATE OR REPLACE TRIGGER secureemp

BEFORE INSERT OR UPDATE OR DELETE ON emp

BEGIN

IF(TOCHAR(sysdate,′DY′)IN(′SAT′,′SUN′))

OR(TONUMBER(sysdate,′HH24′)NOT BETWEEN 8 AND 18)THEN

RAISEAPPLICATIONERROR(-20506,′You may only change data during normal hours.′)

END IF;

END

/

7.3.5.2 审计

Oracle系统本身提供了审计功能,可以审计用户插入、更新、删除、查询语句、DDL语句的执行次数,也可以审计操作的成功和失败,并把审计情况写入审计表中(数据字典表中),但不能审计数据操纵时值的变化。

例7-35 利用系统本身提供的审计,审计emp表上所有成功的数据操作的次数。

SQL>AUDIT INSERT,UPDATE,DELETE ON emp BY ACCESS WHENEVER SUCCESSFUL;

利用触发器也可以进行审计,但只审计数据操纵语句(INSERT,UPDATE和DELETE),不审计查询语句和数据定义语句。利用触发器可以审计操纵语句所操作的值的变化,可以将值的变化情况写入用户定义的档案表。

例7-36 用触发器进行审计。将emp表上值的变化记录到auditemp表中。

CREATE OR REPLACE TRIGGER auditempvalues

AFTER DELETE OR INSERT OR UPDATE ON emp

FOR EACH ROW

BEGIN

INSERT INTO auditemp(username,timestamp,empno,oldename,newename,

oldjob,newjob,oldsal,newsal)

VALUES(USER,SYSDATE,old.empno,old.ename,new.ename,

:old.job,new.job,old.sal,new.sal);

END;

/

7.3.5.3 数据完整性

在建表时,使用Oracle系统提供的完整性约束条件可以保证标准的数据完整性。标准的数据完整性包括五种约束条件:非空(NOT NUIL)约束、主键约束、外键约束、CHECK约束和唯一性约束(参见第5章)。标准完整性只能保证静态约束,比如:保证薪水不低于$500,500是一个静态值,对所有员工都一样。

例7-37 利用系统提供的约束条件定义主外键,并保证雇员的薪水不低于$500。

SQL>CREATE TABLE emp(

Empno number(4)constraint pk1 primary key,/*主键*/

Ename varchar2(12)not null,

Job varchar2(12),

Hiredate date,

Mgr number(4),

Sal number constraint ck1 check(sal>500),/*check约束*/

Comm number,

Deptno numbe r(4)constraint fk1 foreign key(deptno)/*外键*/

references dept(deptno))on delete cascade

可以利用触发器加强非标准的完整性检查,提供动态约束。比如:保证薪水不能降低,但同时增长幅度不超过10%。每个人的工资是不一样的,即要检查的完整性是动态的。

例7-38 用触发器实现非标准的数据完整性。确保薪水不能降低,但同时增长幅度不能超过10%。

CREATE OR REPLACE TRIGGER checksal

BEFORE UPDATE OF sal ON emp

FOR EACH ROW

WHEN(new.sal<old.sal)OR(new.sal>old.sal*1.1)

BEGIN

RAISEAPPIICATIONERROR(-20508,

′do not decrease salary nor increase by more than 10%′);

END;

/

7.3.5.4 参考完整性(关联完整性)

利用Oracle系统提供的reference参考完整性约束,限制关联表中UPDATE 和DELETE操作的一致性,实现关联表的级联删除操作。

例7-39 利用系统本身提供的ON DELETE CASCADE从句实现参考完整性。

7.3.5.3节中的CREATE TABLE语句中在定义外键时指定了ON DELETE CASCADE从句,即可实现多表的级联删除。当删除父表DEPT中的某个部门时,同时删除子表emp中该部门的所有职员。

利用触发器可以增强参考完整性,利用触发器可以实现级联的更新操作。

例7-40 利用触发器增强参考完整性。假设在定义DEPT和emp表时没有做参考完整性限制,则当DEPT表的deptno发生变化时,emp表相关行的deptno也要跟着进行适当的修改。

CREATE OR REPLACE TRIGGER CASCADEUPDATE

AFTER UPDATE OF DEPTNO ON DEPT

FOR EACH ROW

BEGIN

UPDATE emp SET emp.deptno:new.deptno

WHERE emp.deptno:old.deptno;

END;

/

7.3.5.5 数据的复制

Oracle系统提供了快照功能可以实现表的复制。但快照是按用户定义的时间间隔定期异步地对表进行备份。可以在多个主表上建立快照,快照只能读,不能进行写操作。快照的数据来源可以是远程的一张表或多张表。

例7-41 利用快照拷贝表中数据。在旧金山创建一个建立在纽约的远地表上的快照。远程访问必须建立链路。

SQL>CREATE SNAPSHOT empcopy AS SELECT*FROM emp@ny;/*ny为链路名*/

利用触发器复制表可以实现实时、同步地复制表中数据。触发器只能针对一个主表进行复制。既可以从表的复制中读取数据,也可以写入数据。触发器进行复制时的数据来源只能是一张表。

例7-42 用触发器为一个表做复制。在纽约,为旧金山制作一个emp表的复制。当对旧金山的emp表进行插入操作时,将刚插入的值同时插入到纽约的emp@sf表中。当对旧金山的emp表进行更新操作时,将刚更新的值同时更新到纽约的emp@sf表中。sf为链路名。

CREATE OR REPLACE TRIGGER empreplica

BEFORE INSERT OR UPDATE ON emp

FOR EACH ROW

BEGIN

IF INSERTING THEN

INSERT INTO emp@sf(empno,ename,sal,comm,deptno)

VALUES(:new.empno,new.ename,new.sal,:new.comm,:new.deptno);

EISE

UPDATE emp@sf SET ename:new.ename,sal:new.sal

WHERE empno:new.empno

END IF;

END;

/

小结

本章详细地介绍Oracle系统中存储过程和函数的基本概念、相互之间的差别,以及与第6章中介绍的PL/SQL无名块的差别。同时用实例详细地介绍开发存储过程和函数的步骤,创建存储过程和函数的语法,以及存储过程和函数的形式参数的使用方法。接着介绍存储过程和函数的管理命令和测试方法,以及在不同环境下调用存储过程和函数的语法。

本章用实例详细地介绍Oracle系统中包的概念、包的组成,以及开发包的步骤和语法。同时介绍包的管理和调用命令,以及Oracle系统提供的多个系统包的功能。

最后详细地介绍数据库触发器的概念,触发器的组成和种类,触发器与存储过程函数的区别,以及触发器的开发步骤。接着用实例详细地介绍建立语句级触发器与行级触发器的语法,以及判断触发事件的谓词(INSERTING、UPDATING、DELETING)和标识符(:old.列名和:new.列名)的作用和使用规则。接着用实例详细地介绍触发器的管理命令,触发器的测试方法,以及触发器在安全性、审计、数据完整性、参考完整性和数据复制等方面的应用。

习题

1.开发存储过程和函数的步骤有哪些?

2.试述存储过程中三类模式参数的差别。

3.哪个数据字典视图存储了存储过程、函数、包的源代码?

4.试述存储过程和函数调用方法的差别。

5.试述存储过程和函数的优点。

6.试述包的组成及包的开发步骤。

7.与一张表相关联并且当一个事件发生时自动执行的PL/SQL块的名字是什么?

8.数据库触发器由哪几部分组成?DML触发器有哪几种类型?触发事件和时间有哪些?

9.数据库触发器与存储过程的区别是什么?

10.试述开发数据库触发器的步骤及数据库触发器的测试方法。

11.试述数据库触发器与存储过程在管理上的差别。

12.试述数据库触发器的应用。