高级PL/SQL开发


下载 第1章 PL/SQL介绍 P L / S Q L是一种高级数据库程序设计语言,该语言专门用于在各种环境下对 O r a c l e数据库进 行访问。由于该语言集成于数据库服务器中,所以 P L / S Q L代码可以对数据进行快速高效的处理。 除此之外,可以在O r a c l e数据库的某些客户端工具中,使用 P L / S Q L语言也是该语言的一个特点。 本章的主要内容是讨论引入 P L / S Q L语言的必要性和该语言的主要特点,以及了解 P L / S Q L语言的 重要性和数据库版本问题。还要介绍一些贯穿全书的更详细的高级概念,并在本章的最后就我 们在本书案例中使用的数据库表的若干约定做一说明。 1.1 为什么要引入P L / S Q L语言 O r a c l e数据库是一种关系型数据库。通常我们把用于访问这种关系型数据库的程序设计语言 叫做结构化查询语言,即S Q L语言。S Q L是一种灵活高效的查询语言,其主要功能是对关系数据 库中的数据进行操作和处理。例如,下面的 S Q L语句可以从数据库中将学习营养专业的全部学 生一次删除: DELETE FROM students WHERE major = 'Nutrition'; (本章的最后一节将对本书中使用的包括 s t u d e n t s表在内的各种数据库表进行说明。) S Q L是先进的第四代程序设计语言,使用这种语言只需对要完成的任务进行描述,而不必指 定实现任务的具体方法。以上面例子中的 D E L E T E语句为例,我们并不知道 S Q L语言是如何找到 学习营养专业的学生的。虽然按一般语言的做法推测,数据库服务器要按某种顺序逐个访问数 据库表中的所有学生记录以决定删除满足条件的学生记录。但实际上,我们无法知道这些删除 操作的细节。 第三代程序设计语言如 C语言和C O B O L语言等是面向过程的语言。用第三代语言( 3 G L) 编制的程序是一步一步地实现程序功能的。例如,我们可以用下面的程序段来实现上述的删除 操作: LOOP over each student record IF this record has major = 'Nutrition' THEN DELETE this record; END IF; END LOOP; 面向对象的程序设计语言如 C + +或J a v a也属于第三代程序设计语言。虽然这类语言采用了面 向对象的程序结构,但程序中算法的实现还是要用各种语句逐步指定。 第一部分 PL/SQL介绍及开发环境各种语言都有其自身的优缺点。相对于第三代程序设计语言来说, S Q L一类的第四代程序语 言使用起来非常简单,语言中语句的种类也比较少。但这类语言将用户与实际的数据结构和算 法隔离开来,对数据的具体处理完全由该类语言的运行时系统实现。而在某些情况下,第三代 语言使用的过程结构在表达某些程序过程来说是非常有用的。这也就是引入 P L / S Q L语言的原因, 即P L / S Q L语言将第四代语言的强大功能和灵活性与第三代语言的过程结构的优势融为一体。 P L / S Q L代表面向过程化的语言与 S Q L语言的结合。我们可以从该语言的名称中看出, P L / S Q L是在S Q L语言中扩充了面向过程语言中使用的程序结构,如: • 变量和类型(即可以预定义也可以由用户定义) • 控制语句(如I F - T H E N - E L S E)和循环 • 过程和函数 • 对象类型和方法(P L / S Q L 8.0版本以上) P L / S Q L语言实现了将过程结构与 Oracle SQL的无缝集成,从而为用户提供了一种功能强大 的结构化程序设计语言。例如,我们假设要修改一个学生的专业。如果没有该学生的记录的话, 我们就为该学生创建一个新的记录。用 P L / S Q L编制的程序代码可以实现我们的要求,如下所 示: 节选自在线代码3gl_4gL.sql DECLARE /* Declare variables that will be used in SQL statements */ v_NewMajor VARCHAR2(10) := 'History'; v_FirstName VARCHAR2(10) := 'Scott'; v_LastName VARCHAR2(10) := 'Urman'; BEGIN /* Update the students table. */ UPDATE students SET major = v_NewMajor WHERE first_name = v_FirstName AND last_name = v_LastName; /* Check to see if the record was found. If not, then we need to insert this record. */ IF SQL%NOTFOUND THEN INSERT INTO students (ID, first_name, last_name, major) VALUES(student_sequence.NEXTVAL, v_FirstName, v_LastName, v_NewMajor); END IF; END; 上面的例子中有两个不同的 S Q L语句(U P D AT E和I N S E RT),这两个语句是第四代程序结构, 同时该段程序中还使用了第三代语言的结构(变量声明和 I F条件语句)。 注意 为了运行上面的程序例子,先要创建程序中引用的数据库对象(即表 s t u d e n t s和 序列s t u d e n t _ s e q u e n c e)。可以使用本书C D - R O M中提供的脚本文件r e l Ta b l e . s q l来实现上 述工作。有关创建上述对象的进一步信息,请参见 1 . 3 . 3节的内容。 P L / S Q L语言在将S Q L语言的灵活性及功能与第三代语言的可配置能力相结合方面是独一无 2计计第一部分 PL/SQL介绍及开发环境 下载二的。该语言集成了面向过程语言的过程结构和强大的数据库操作,为设计复杂的数据库应用 提供了功能强大、健壮可靠的程序设计语言。 1.1.1 PL/SQL与网络传输 目前的大多数数据库应用一般都采用客户 /服务器或三层模式。在客户 /服务器模式下,驻留 在客户机上的应用程序使用 S Q L语句向数据库服务器发送服务请求。通常,发送这种请求将导 致过多的网络传输,如图 1 - 1左半部分的框图所示,每个 S Q L语句将引起一次网络传输。现在将 该图的左面与右面相比较,我们可以发现使用 P L / S Q L语言可以将几个 S Q L语句合并为一个 P L / S Q L块发送给服务器,从而减少网络通信流量并提高应用程序的执行速度。 图1-1 在客户/服务器环境下使用S Q L和P L / S Q L的比较 即使在客户机与服务器都运行在同一台设备上时,使用 P L / S Q L编制的应用程序的性能也会 有提高。在这种情况下,虽然不需要进行网络传输,但将 S Q L语句打包传送将减少对数据库的 访问次数。 P L / S Q L语言的打包功能也适用于三层结构应用。在这种模式下,客户机(通常运行在 H T M L浏览器下)与应用服务器进行通信,而应用服务器再与数据库交互操作, P L / S Q L有利于 应用服务器与数据库的通信。有关 P L / S Q L的这种应用环境,我们将在本书的第 2章中讨论。 1.1.2 PL/SQL标准 O r c a l e数据库支持A N S I标准的S Q L语言,即ANSI X3.135-1992文档中“数据库语言 S Q L” 定义的S Q L语言。该标准通常称为 S Q L 9 2标准,只定义了 S Q L语言本身。该标准并没有定义 P L / S Q L所提供语言的3 G L扩充内容。S Q L 9 2指定了三个实现层次:初等级别、中等级别和最高 级别。O r a c l e 7的7 . 0版(包括所有高版本的 O r a c l e 8和O r a c l e 8 i)实现了由美国国家标准技术研究 所认证的S Q L 9 2标准的初等级别。现在 O r a c l e公司正在与美国国家标准协会 A N S I共同努力以确 保O r a c l e数据库和P L / S Q L语言的最新版本将实现S Q L 9 2的最高级别。 第1章 PL/SQL介绍计计3下载 Oracle数据库服务器 客户应用 使用SQL 使用PL/SQL 客户应用 Oracle数据库服务器1.2 PL/SQL的特点 介绍P L / S Q L的不同特点与功能的最好方式是通过实际程序案例演示。本章下面几节将描述 P L / S Q L语言的若干主要特点。 1.2.1 PL/SQL的基本特点 本书的主要内容是介绍 P L / S Q L语言的高级功能。在下面几节中,我们将介绍该语言的基本 特点。如果读者要进一步了解有关信息,请参考 P L / S Q L用户指南及索引手册,或参考《 O r a c l e 8 P L / S Q L程序设计》一书。 1. PL/SQL的块结构 P L / S Q L程序的基本结构是块。所有的 P L / S Q L程序都是由块组成的,这些块之间还可以相互 嵌套。通常,程序中的每一块都实现一个逻辑操作,从而把不同的任务进行分割,由不同的块 来实现。P L / S Q L的块结构如下所示: DECLARE /* Declarative section - PL/SQL variables, types, cursors, and local subprograms go here. */ BEGIN /* Executable section - procedural and SQL statements go here. This is the main section of the block and the only one that is required. */ EXCEPTION /* Exception-handling section - error-handling statements go here. */ END; 在上面演示的块结构中,只有执行部分是必须的,声明部分和异常处理部分都是可选的。 块结构中的执行部分至少要有一个可执行语句。 P L / S Q L块采用的这种分段结构将P L / S Q L程序的 不同功能各自独立出来。 P L / S Q L的这种特点是仿效第三代程序设计语言 A d a采用的程序结构。在A d a语言中使用的很 多程序结构包括A d a使用的块结构都适用于 P L / S Q L语言。除此之外,在 P L / S Q L语言中还可以发 现A d a语言使用的异常处理方法、过程和函数声明以及包的定义的语法等特征。 2. 错误处理 P L / S Q L块中的异常处理部分是用来响应应用程序运行中遇到的错误。把程序的主体部分与 错误处理部分代码相互隔离,这样,程序的结构看起来十分清晰。例如,下面的 P L / S Q L块演示 了将异常发生的时间及将遇到该异常错误的用户名记录在日志表的处理过程。 节选自在线代码Error.sql DECLARE v_ErrorCode NUMBER; -- Code for the error v_ErrorMsg VARCHAR2(200); -- Message text for the error v_CurrentUser VARCHAR2(8); -- Current database user v_Information VARCHAR2(100); -- Information about the error BEGIN 4计计第一部分 PL/SQL介绍及开发环境 下载/* Code that processes some data here */ EXCEPTION WHEN OTHERS THEN -- Assign values to the log variables, using built-in -- functions. v_ErrorCode := SQLCODE; v_ErrorMsg := SQLERRM; v_CurrentUser := USER; v_Information := 'Error encountered on ' || TO_CHAR(SYSDATE) || ' by database user ' || v_CurrentUser; -- Insert the log message into log_table. INSERT INTO log_table (code, message, info) VALUES (v_ErrorCode, v_ErrorMsg, v_Information); END; 注意 上面的例子和许多其他例程都在本书的联机发布信息中。读者如要了解有关详细 信息,请参阅1 . 3 . 3节的内容。 3. 变量和类型 信息在数据库与 P L / S Q L程序之间是通过变量进行传递的。所谓变量就是可以由程序读取或 赋值的存储单元。在上面的例子中, v _ C u r r e n t U s e r, v _ E r r o r C o d e ,和v _ I n f o r m a t i o n都是变量。通 常,变量是在P L / S Q L块的声明部分定义的。 每个变量都有一个特定的类型与其关联。变量的类型定义了变量可以存放的信息类别。如 下所示,P L / S Q L变量可以与数据库列具有同样的类型: DECLARE v_StudentName VARCHAR2(20); v_CurrentDate DATE; v_NumberCredits NUMBER(3); P L / S Q L变量也可以是其他类型: DECLARE v_LoopCounter BINARY_INTEGER; v_CurrentlyRegistered BOOLEAN; 除此之外,P L / S Q L还支持用户自定义的数据类型,如记录类型、表类型等。使用用户自定 义的数据类型可以让你定制程序中使用的数据类型结构。下面是一个用户自定义的数据类型例 子: DECLARE TYPE t_StudentRecord IS RECORD ( FirstName VARCHAR2(10), LastName VARCHAR2(10), CurrentCredits NUMBER(3) ); v_Student t_StudentRecord; 4. 循环结构 第1章 PL/SQL介绍计计5下载P L / S Q L支持多种循环结构。所谓循环就是指可以重复执行的同一代码段。例如,下面的程 序块使用一个简单的循环来把数字 1~5 0插入到表t e m p _ t a b l e中: 节选自在线代码SimpleLoop.sql DECLARE v_LoopCounter BINARY_INTEGER := 1; BEGIN LOOP INSERT INTO temp_table (num_col) VALUES (v_LoopCounter); v_LoopCounter := v_LoopCounter + 1; EXIT WHEN v_LoopCounter > 50; END LOOP; END; P L / S Q L中还提供了一种使用 F O R的循环结构,这种循环的结构更简单。如下所示,我们可 以使用这种F O R循环来实现上面的循环操作: 节选自在线代码NumricLoop.sql BEGIN FOR v_LoopCounter IN 1..50 LOOP INSERT INTO temp_table (num_col) VALUES (v_LoopCounter); END LOOP; END; 5. 游标 游标是用来处理使用S E L E C T语句从数据库中检索到的多行记录的工具。借助于游标的功能, 数据库应用程序可以对一组记录逐个进行处理,每次处理一行。例如,下面的程序块可以检索 到数据库中所有学生的名和姓: 节选自在线代码CursorLoop.sql DECLARE v_FirstName VARCHAR2(20); v_LastName VARCHAR2(20); -- Cursor declaration. This defines the SQL statement to -- return the rows. CURSOR c_Students IS SELECT first_name, last_name FROM students; BEGIN -- Begin cursor processing. OPEN c_Students; LOOP -- Retrieve one row. FETCH c_Students INTO v_FirstName, v_LastName; -- Exit the loop after all rows have been retrieved. EXIT WHEN c_Students%NOTFOUND; /* Process data here */ 6计计第一部分 PL/SQL介绍及开发环境 下载END LOOP; -- End processing. CLOSE c_Students; END; 1.2.2 PL/SQL的高级功能 下面将介绍几个有关 P L / S Q L高级功能的程序例子,这些高级功能将在本书的后几章做详细 讲解。P L / S Q L的高级功能是在其基本功能上实现的。 1. 过程和函数 P L / S Q L中的过程和函数(通称为子程序)是 P L / S Q L块的一种特殊类型,这种类型的子程序 可以以编译的形式存放在数据库中,并为后续的程序块调用。例如,下面的语句创建了一个叫 做P r i n t S t u d e n t s的过程,该过程使用包 D B M S _ O U T P U T将所有学生的姓名以定制的格式显示在 屏幕上: 节选自在线代码PrintStudents.sql CREATE OR REPLACE PROCEDURE PrintStudents( p_Major IN students.major%TYPE) AS CURSOR c_Students IS SELECT first_name, last_name FROM students WHERE major = p_Major; BEGIN FOR v_StudentRec IN c_Students LOOP DBMS_OUTPUT.PUT_LINE(v_StudentRec.first_name || ' ' || v_StudentRec.last_name); END LOOP; END; 一旦创建了该过程并将其存储在数据库中,我们就可以用如下所示的程序块来调用该过程: 节选自在线代码PrintStudents.sql SQL> BEGIN 2 PrintStudents('Computer Science'); 3 END; 4 / Scott Smith Joanne Junebug Shay Shariatpanahy 注意 使用SET SERVEROUTPUT ON命令可以设置D B M S _ O U T P U T的输出功能。有关 该命令的详细信息,请参见本书第 2章的内容。 2. 包 P L / S Q L子程序可以和变量与类型共同组成包。 P L / S Q L的包由两部分组成,即说明部分和包 体。一个包可以带有多个相关的过程。例如,下面的包 R o o m s P k g就有两个过程,一个是插入新 第1章 PL/SQL介绍计计7下载教室信息的过程,另一个是从表 r o o m s中删除一个教室的过程: 节选自在线代码RoomPkg.sql CREATE OR REPLACE PACKAGE RoomsPkg AS PROCEDURE NewRoom(p_Building rooms.building%TYPE, p_RoomNum rooms.room_number%TYPE, p_NumSeats rooms.number_seats%TYPE, p_Description rooms.description%TYPE); PROCEDURE DeleteRoom(p_RoomID IN rooms.room_id%TYPE); END RoomsPkg; CREATE OR REPLACE PACKAGE BODY RoomsPkg AS PROCEDURE NewRoom(p_Building rooms.building%TYPE, p_RoomNum rooms.room_number%TYPE, p_NumSeats rooms.number_seats%TYPE, p_Description rooms.description%TYPE) IS BEGIN INSERT INTO rooms (room_id, building, room_number, number_seats, description) VALUES (room_sequence.NEXTVAL, p_Building, p_RoomNum, p_NumSeats, p_Description); END NewRoom; PROCEDURE DeleteRoom(p_RoomID IN rooms.room_id%TYPE) IS BEGIN DELETE FROM rooms WHERE room_id = p_RoomID; END DeleteRoom; END RoomsPkg; 3. 动态S Q L 借助于动态 S Q L,一个P L / S Q L应用程序可以在运行期间构造并执行 S Q L语句。使用动态 S Q L方法有两种,一种是使用 P L / S Q L 2 . 1版及以上支持的D B M S _ S Q L包,另一种是使用O r a c l e 8 i 或更高版本支持的本地动态 S Q L。下面所示的D r o p Ta b l e就是使用D B M S _ S Q L实现的过程,其功 能是释放指定的工作表: 节选自在线代码DropTable.sql CREATE OR REPLACE PROCEDURE DropTable(p_Table IN VARCHAR2) AS v_SQLString VARCHAR2(100); v_Cursor BINARY_INTEGER; v_ReturnCode BINARY_INTEGER; BEGIN -- Build the string based on the input parameter. v_SQLString := 'DROP TABLE ' || p_Table; -- Open the cursor. 8计计第一部分 PL/SQL介绍及开发环境 下载v_Cursor := DBMS_SQL.OPEN_CURSOR; -- Parse and execute the statement. DBMS_SQL.PARSE(v_Cursor, v_SQLString, DBMS_SQL.NATIVE); v_ReturnCode := DBMS_SQL.EXECUTE(v_Cursor); -- Close the cursor. DBMS_SQL.CLOSE_CURSOR(v_Cursor); END DropTable; 如果使用O r a c l e 8 i或更高的版本,该过程可以使用本地动态 S Q L重写如下: 节选自在线代码PrintStudents.sql CREATE OR REPLACE PROCEDURE DropTable(p_Table IN VARCHAR2) AS v_SQLString VARCHAR2(100); BEGIN -- Build the string based on the input parameter. v_SQLString := 'DROP TABLE ' || p_Table; EXECUTE IMMEDIATE v_SQLString; END DropTable; 4. 对象类型 (O r a c l e 8及以上版本) O r a c l e 8 (包括PL/SQL 8)支持对象类型。O r a c l e中的对象类型由属性和方法组成并可以存储在 数据库表中。下面的例子演示了创建对象类型的方法: 节选自在线代码ch12/objTypes.sql CREATE OR REPLACE TYPE Student AS OBJECT ( ID NUMBER(5), first_name VARCHAR2(20), last_name VARCHAR2(20), major VARCHAR2(30), current_credits NUMBER(3), -- Returns the first and last names, separated by a space. MEMBER FUNCTION FormattedName RETURN VARCHAR2, PRAGMA RESTRICT_REFERENCES(FormattedName, RNDS, WNDS, RNPS, WNPS), -- Updates the major to the specified value in p_NewMajor. MEMBER PROCEDURE ChangeMajor(p_NewMajor IN VARCHAR2), PRAGMA RESTRICT_REFERENCES(ChangeMajor, RNDS, WNDS, RNPS, WNPS), -- Updates the current_credits by adding the number of -- credits in p_CompletedClass to the current value. MEMBER PROCEDURE UpdateCredits(p_CompletedClass IN Class), PRAGMA RESTRICT_REFERENCES(UpdateCredits, RNDS, WNDS, RNPS, WNPS), 第1章 PL/SQL介绍计计9下载-- ORDER function used to sort students. ORDER MEMBER FUNCTION CompareStudent(p_Student IN Student) RETURN NUMBER ); CREATE OR REPLACE TYPE BODY Student AS MEMBER FUNCTION FormattedName RETURN VARCHAR2 IS BEGIN RETURN first_name || ' ' || last_name; END FormattedName; MEMBER PROCEDURE ChangeMajor(p_NewMajor IN VARCHAR2) IS BEGIN major := p_NewMajor; END ChangeMajor; MEMBER PROCEDURE UpdateCredits(p_CompletedClass IN Class) IS BEGIN current_credits := current_credits + p_CompletedClass.num_credits; END UpdateCredits; ORDER MEMBER FUNCTION CompareStudent(p_Student IN Student) RETURN NUMBER IS BEGIN -- First compare by last names IF p_Student.last_name = SELF.last_name THEN -- If the last names are the same, then compare first names. IF p_Student.first_name < SELF.first_name THEN RETURN 1; ELSIF p_Student.first_name > SELF.first_name THEN RETURN -1; ELSE RETURN 0; END IF; ELSE IF p_Student.last_name < SELF.last_name THEN RETURN 1; ELSE RETURN -1; END IF; END IF; END CompareStudent; END; 5. 集合 P L / S Q L的集合类似于其他3 G L中使用的数组。P L / S Q L提供了三种不同的集合类型:按表索 10计计第一部分 PL/SQL介绍及开发环境 下载引(PL/SQL 2.0及更高版本)、嵌套表(PL/SQL 8.0及更高版本)、数组(PL/SQL 8.0及更高版 本)。下面的程序例子介绍了上述三种集合类型的使用方法: 节选自在线代码Collections.sql DECLARE TYPE t_IndexBy IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; TYPE t_Nested IS TABLE OF NUMBER; TYPE t_Varray IS VARRAY(10) OF NUMBER; v_IndexBy t_IndexBy; v_Nested t_Nested; v_Varray t_Varray; BEGIN v_IndexBy(1) := 1; v_IndexBy(2) := 2; v_Nested := t_Nested(1, 2, 3, 4, 5); v_Varray := t_Varray(1, 2); END; 1.2.3 PL/SQL内置包 除了上述P L / S Q L语言提供的功能外, O r a c l e也提供了若干具有特殊功能的内置包。本书自 始至终将贯穿讨论这些功能的细节,并在本书的附录 A中给予总结。下面列出了本书将重点讨论 的内置包: 包 描 述 D B M S _ A L E RT 数据库报警,允许会话间通信。 D B M S _ J O B 任务调度服务。 D B M S _ L O B 大型对象操作。 D B M S _ P I P E 数据库管道,允许会话间通信。 D B M S _ S Q L 动态S Q L。 U T L _ F I L E 文本文件的输入与输出。 总的来说,所有D B M S _ *包都存储在服务器中,只有包 U T L _ F I L E既存储在服务器端又存储 在客户端(某些客户环境,如 O r a c l e表还提供了额外的包)。 1.3 本书的约定 本节介绍作者在本书中使用的几个约定。这些约定中包括用来标识 P L / S Q L版本的图标、引 用O r a c l e文档资料的方法和在线案例所在的文件目录。 1.3.1 PL/SQL和Oracle 数据库版本说明 P L / S Q L是随O r a c l e服务器一同提供的。P L / S Q L的1 . 0版是与O r a c l e 6 . 0版一起发行的。随后发 第1章 PL/SQL介绍计计11下载行的O r a l c e 7提供了P L / S Q L 2 . x系列版本。当 O r a c l e 8发布后,P L / S Q L的版本号也升级到 8系列。 现在使用的O r a c l e 8 i(版本号为8 . 1)中的P L / S Q L的版本号是8 . 1。将来发行的O r a c l e新版本数据 库的P L / S Q L版本号将与该数据库版本号保持一致。如表 1 - 1所示,该表列出了每版 O r a c l e数据库 与对应的P L / S Q L语言新增的功能。本书的重点是讨论 P L / S Q L 2 . 0 - 8 . 1版的内容。本书使用以下的 图标来表示某一版本具有的特殊功能: 该图标所在的段落将讨论PL/SQL 2.1版和更高版本具有的功能,如包 DBMS _SQL。 该图标所在的段落将讨论PL/SQL 2.2版和更高版本具有的功能,如游标变量。 该图标所在的段落将讨论 PL/SQL 2.3版和更高版本具有的功能,如包 UTL _FILE。 该图标所在的段落将讨论PL/SQL 8.0版和更高版本具有的功能,如对象类型。 该图标所在的段落将讨论 PL/SQL 8.1版和更高版本具有的功能,如本地动态 S Q L。 表1-1 Oracle和P L / S Q L版本号与功能对照表 O r a c l e版本 P L / S Q L版本 新增或变更的功能 6 1 . 0 第一版 7 . 0 2 . 0 数据类型C H A R变为定长 存储子程序(包括过程、函数、包和触发器) 用户定义的复合类型— 表和记录 使用D B M S _ P I P E和D B M S _ A L E RT包进行会话间通信 在SQL *PLUS或服务器管理器中使用D B M S _ O U T P U T包进行输出 7 . 1 2 . 1 用户定义的子类型 在S Q L语句中使用用户定义函数的功能 使用D B M S _ S Q L包的动态P L / S Q L 7 . 2 2 . 2 游标变量 用户定义的约束子类型 使用D B M S _ J O B包调度P L / S Q L批处理的功能 7 . 3 2 . 3 增强了游标变量的功能(扩充了对服务器的 f e t c h操作和弱类( weakly typed)) 使用U T L _ F I L E进行文件输入输出操作 P L / S Q L表属性和记录表 以编译格式存储的触发器 8 . 0 8 . 0 对象类型和方法 集合类型— 嵌套表和数组 高级队列功能选项 外部过程 增强的L O B 8 . 1 8 . 1 本地动态S Q L J a v a外部例程 12计计第一部分 PL/SQL介绍及开发环境 下载(续) O r a c l e版本 P L / S Q L版本 新增或变更的功能 调用权利 N O C O P Y参数 自动事务 大容量处理 在使用P L / S Q L语言编程时,确认P L / S Q L的版本号将有助于使用该版本提供的相应先进功能。 当与O r a c l e数据库连接时,初始化字符串中带有该数据库的版本号。请看下面两个连接显示字符 串: Connected to: Oracle8 Enterprise Edition Release 8.0.6.0.0 - Production With the Objects option PL/SQL Release 8.0.6.0.0 - Production 和 Connected to: Oracle8i Enterprise Edition Release 8.1.5.0.0 - Production With the Partitioning and Java options PL/SQL Release 8.1.5.0.0 - Production 这两个字符串都是合法的初始字符串。请注意在上面的显示中, P L / S Q L的版本号是与数据 库的版本号完全对应的。 本书的大部分案例程序是用运行在 S U N公司的Solaris 操作系统上的O r a c l e 8 . 0 . 6实现的。本 书的O r a c l e 8 i案例程序也是用运行在 S U N公司的Solaris 操作系统上的O r a c l e 8 i,8 . 1 . 5版编制的。 本书所用的所有的计算机屏幕图形都是与服务器上的 O r a c l e数据库相连接的 Windows 95或 Windows NT操作系统的窗口图形。 1.3.2 Oracle数据库文档 在本书的有关章节中,我向读者推荐了包含详细信息的 O r a c l e文档资料。由于这些文档手册 的名称因数据库的版本不同而不同,我通常使用缩写的版本号来区别不同的手册。例如, O r a c l e 服务器参考资料指的是 O r a c l e 7、O r a c l e 8或O r a c l e 8 i服务器参考资料,具体是指哪一本资料,这 取决于你正在使用的O r a c l e数据库版本。 1.3.3 本书提供的C D - R O M内容简介 本书附加的C D - R O M中包括下列几类信息: • 本书使用的程序案例的代码。读者也可以在 h t t p : / / w w w. o s b o r n e . c o m站点浏览本书使用的这 些程序案例。 • P L / S Q L开发工具的五个试用版程序。有关这些开发工具的详细信息,请参见本书的第2章 第1章 PL/SQL介绍计计13下载及第3章的内容。 • 本书第1 5章和第1 6章及附录A~C电子版内容。除此之外,本书第 2章使用的屏幕图形也在 C D - R O M中。 有关本书C D - R O M内容的详细信息,请读者阅读C D - R O M根目录下的超文本文件r e a d m e . h t m l。 本书使用的程序案例在C D - R O M中的目录说明 本书中使用的程序案例的第一个注释行指出了该程序的名称。本书使用的所有程序案例的 代码都存放在C D - R O M中C O D E目录下以章节命名的子目录中。例如,请看下面我们在本章中使 用的循环结构示范程序: 节选自在线代码SimpleLoop.sql DECLARE v_LoopCounter BINARY_INTEGER := 1; BEGIN LOOP INSERT INTO temp_table (num_col) VALUES (v_LoopCounter); v_LoopCounter := v_LoopCounter + 1; EXIT WHEN v_LoopCounter > 50; END LOOP; END; 根据第一行的注释行信息,我们可以在 C D - R O M中的目录 c o d e / c h 0 1中找到该源程序文件 s i m p l e . s q l。C D - R O M中C O D E目录中的r e a d m e . h t m l文件对所有的程序案例都有说明。 1.4 本书案例使用的通用数据库表 本书中使用的程序案例是一个大学入学注册系统中的数据库表,这组表中最常用的表有三 个:s t u d e n t s、c l a s s e s和r o o m s。这三个表中带有注册系统所需的记录。除了这三个主表之外,表 r e g i s t e r e d _ s t u d e n t s带有已注册课程学生的信息。下面,我们来详细介绍这些表的结构。 注意 可以使用在线文档中的脚本r e l Ta b l e . s q l来创建上述这些数据库表。 O r a c l e 8使用的 对象类型表可由脚本 o b j Ta b l e s . s q l来实现。上述两个脚本都存储在 C O D E子目录中。本 节只介绍关系型表,有关对象表,请看 o b j Ta b l e s . s q l的内容。 1. 序列 s t u d e n t _ s e q u e n c e序列用来生成表 s t u d e n t s主键的唯一健值,而 r o o m _ s e q u e n c e序列则是为表 r o o m s的主健生成的唯一健值。 CREATE SEQUENCE student_sequence START WITH 10000 INCREMENT BY 1; CREATE SEQUENCE room_sequence START WITH 20000 14计计第一部分 PL/SQL介绍及开发环境 下载INCREMENT BY 1; 2. 表s t u d e n t s 表s t u d e n t s中包括了入学新生的有关信息。 CREATE TABLE students ( id NUMBER(5) PRIMARY KEY, first_name VARCHAR2(20), last_name VARCHAR2(20), major VARCHAR2(30), current_credits NUMBER(3) ); INSERT INTO students (id, first_name, last_name, major, current_credits) VALUES (student_sequence.NEXTVAL, 'Scott', 'Smith', 'Computer Science', 11); INSERT INTO students (id, first_name, last_name, major, current_credits) VALUES (student_sequence.NEXTVAL, 'Margaret', 'Mason', 'History', 4); INSERT INTO students (id, first_name, last_name, major, current_credits) VALUES (student_sequence.NEXTVAL, 'Joanne', 'Junebug', 'Computer Science', 8); INSERT INTO students (id, first_name, last_name, major, current_credits) VALUES (student_sequence.NEXTVAL, 'Manish', 'Murgratroid', 'Economics', 8); INSERT INTO students(id, first_name, last_name, major, current_credits) VALUES(student_sequence.NEXTVAL, 'Patrick', 'Poll', 'History', 4); INSERT INTO students(id, first_name, last_name, major, current_credits) VALUES (student_sequence.NEXTVAL, 'Timothy', 'Taller', 'History', 4); INSERT INTO students(id, first_name, last_name, major, current_credits) VALUES (student_sequence.NEXTVAL, 'Barbara', 'Blues', 'Economics', 7); INSERT INTO students(id, first_name, last_name, major, current_credits) VALUES (student_sequence.NEXTVAL, 'David', 'Dinsmore', 'Music', 4); INSERT INTO students(id, first_name, last_name, major, 第1章 PL/SQL介绍计计15下载current_credits) VALUES (student_sequence.NEXTVAL, 'Ester', 'Elegant', 'Nutrition', 8); INSERT INTO students(id, first_name, last_name, major, current_credits) VALUES (student_sequence.NEXTVAL, 'Rose', 'Riznit', 'Music', 7); INSERT INTO STUDENTS(id, first_name, last_name, major, current_credits) VALUES (student_sequence.NEXTVAL, 'Rita', 'Razmataz', 'Nutrition', 8); INSERT INTO students(id, first_name, last_name, major, current_credits) VALUES (student_sequence.NEXTVAL, 'Shay', 'Shariatpanahy', 'Computer Science', 3); 3. 表m a j o r _ s t a t s 该表中保存不同专业的统计信息。 CREATE TABLE major_stats ( major VARCHAR2(30), total_credits NUMBER, total_students NUMBER); INSERT INTO major_stats (major, total_credits, total_students) VALUES ('Computer Science', 22, 3); INSERT INTO major_stats (major, total_credits, total_students) VALUES ('History', 12, 3); INSERT INTO major_stats (major, total_credits, total_students) VALUES ('Economics', 15, 2); INSERT INTO major_stats (major, total_credits, total_students) VALUES ('Music', 11, 2); INSERT INTO major_stats (major, total_credits, total_students) VALUES ('Nutrition', 16, 2); 4. 表r o o m s 该表存储可用的教室有关信息。 CREATE TABLE rooms ( room_id NUMBER(5) PRIMARY KEY, building VARCHAR2(15), room_number NUMBER(4), number_seats NUMBER(4), description VARCHAR2(50) ); INSERT INTO rooms (room_id, building, room_number, number_seats, 16计计第一部分 PL/SQL介绍及开发环境 下载description) VALUES (room_sequence.NEXTVAL, 'Building 7', 201, 1000, 'Large Lecture Hall'); INSERT INTO rooms (room_id, building, room_number, number_seats, description) VALUES (room_sequence.NEXTVAL, 'Building 6', 101, 500, 'Small Lecture Hall'); INSERT INTO rooms (room_id, building, room_number, number_seats, description) VALUES (room_sequence.NEXTVAL, 'Building 6', 150, 50, 'Discussion Room A'); INSERT INTO rooms (room_id, building, room_number, number_seats, description) VALUES (room_sequence.NEXTVAL, 'Building 6', 160, 50, 'Discussion Room B'); INSERT INTO rooms (room_id, building, room_number, number_seats, description) VALUES (room_sequence.NEXTVAL, 'Building 6', 170, 50, 'Discussion Room C'); INSERT INTO rooms (room_id, building, room_number, number_seats, description) VALUES (room_sequence.NEXTVAL, 'Music Building', 100, 10, 'Music Practice Room'); INSERT INTO rooms (room_id, building, room_number, number_seats, description) VALUES (room_sequence.NEXTVAL, 'Music Building', 200, 1000, 'Concert Room'); INSERT INTO rooms (room_id, building, room_number, number_seats, description) VALUES (room_sequence.NEXTVAL, 'Building 7', 300, 75, 'Discussion Room D'); INSERT INTO rooms (room_id, building, room_number, number_seats, description) VALUES (room_sequence.NEXTVAL, 'Building 7', 310, 50, 'Discussion Room E'); 5. 表c l a s s e s 该表描述了学生可以选择的课程。 CREATE TABLE classes ( department CHAR(3), course NUMBER(3), description VARCHAR2(2000), max_students NUMBER(3), current_students NUMBER(3), 第1章 PL/SQL介绍计计17下载num_credits NUMBER(1), room_id NUMBER(5), CONSTRAINT classes_department_course PRIMARY KEY (department, course), CONSTRAINT classes_room_id FOREIGN KEY (room_id) REFERENCES rooms (room_id) ); INSERT INTO classes(department, course, description, max_students, current_students, num_credits, room_id) VALUES ('HIS', 101, 'History 101', 30, 11, 4, 20000); INSERT INTO classes(department, course, description, max_students, current_students, num_credits, room_id) VALUES ('HIS', 301, 'History 301', 30, 0, 4, 20004); INSERT INTO classes(department, course, description, max_students, current_students, num_credits, room_id) VALUES ('CS', 101, 'Computer Science 101', 50, 0, 4, 20001); INSERT INTO classes(department, course, description, max_students, current_students, num_credits, room_id) VALUES ('ECN', 203, 'Economics 203', 15, 0, 3, 20002); INSERT INTO classes(department, course, description, max_students, current_students, num_credits, room_id) VALUES ('CS', 102, 'Computer Science 102', 35, 3, 4, 20003); INSERT INTO classes(department, course, description, max_students, current_students, num_credits, room_id) VALUES ('MUS', 410, 'Music 410', 5, 4, 3, 20005); INSERT INTO classes(department, course, description, max_students, current_students, num_credits, room_id) VALUES ('ECN', 101, 'Economics 101', 50, 0, 4, 20007); INSERT INTO classes(department, course, description, max_students, current_students, num_credits, room_id) VALUES ('NUT', 307, 'Nutrition 307', 20, 2, 4, 20008); INSERT INTO classes(department, course, description, max_students, current_students, num_credits, room_id) VALUES ('MUS', 100, 'Music 100', 100, 0, 3, NULL); 6. 表r e g i s t e r e d _ s t u d e n t s 该表保存学生目前参加的课程信息。 CREATE TABLE registered_students ( student_id NUMBER(5) NOT NULL, department CHAR(3) NOT NULL, course NUMBER(3) NOT NULL, grade CHAR(1), CONSTRAINT rs_grade CHECK (grade IN ('A', 'B', 'C', 'D', 'E')), 18计计第一部分 PL/SQL介绍及开发环境 下载CONSTRAINT rs_student_id FOREIGN KEY (student_id) REFERENCES students (id), CONSTRAINT rs_department_course FOREIGN KEY (department, course) REFERENCES classes (department, course) ); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10000, 'CS', 102, 'A'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10002, 'CS', 102, 'B'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10003, 'CS', 102, 'C'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10000, 'HIS', 101, 'A'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10001, 'HIS', 101, 'B'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10002, 'HIS', 101, 'B'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10003, 'HIS', 101, 'A'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10004, 'HIS', 101, 'C'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10005, 'HIS', 101, 'C'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10006, 'HIS', 101, 'E'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10007, 'HIS', 101, 'B'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10008, 'HIS', 101, 'A'); 第1章 PL/SQL介绍计计19下载INSERT INTO registered_students (student_id, department, course, grade) VALUES (10009, 'HIS', 101, 'D'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10010, 'HIS', 101, 'A'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10008, 'NUT', 307, 'A'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10010, 'NUT', 307, 'A'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10009, 'MUS', 410, 'B'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10006, 'MUS', 410, 'E'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10011, 'MUS', 410, 'B'); INSERT INTO registered_students (student_id, department, course, grade) VALUES (10000, 'MUS', 410, 'B'); 7. 表R S _ a u d i t 该表用来记录对表r e g i s t e r e d _ s t u d e n t s所做的修改。 CREATE TABLE RS_audit ( change_type CHAR(1) NOT NULL, changed_by VARCHAR2(8) NOT NULL, timestamp DATE NOT NULL, old_student_id NUMBER(5), old_department CHAR(3), old_course NUMBER(3) old_grade CHAR(1), new_student_id NUMBER(5), new_department CHAR(3), new_course NUMBER(3), new_grade CHAR(1) ); 8. 表l o g _ t a b l e 该表用来记录O r a c l e数据库发生的错误信息。 CREATE TABLE log_table ( code NUMBER, 20计计第一部分 PL/SQL介绍及开发环境 下载message VARCHAR2(200), info VARCHAR2(100) ); 9. 表t e m p _ t a b l e 该表用来存放临时数据。 CREATE TABLE temp_table ( num_col NUMBER, char_col VARCHAR2(60) ); 10. 表c o n n e c t _ a u d i t 该表由第6章的程序案例用来记录与数据库的连接和断开信息。 CREATE TABLE connect_audit ( user_name VARCHAR2(30), operation VARCHAR2(30), timestamp DATE); 11. 表d e b u g _ t a b l e 该表由本书第3章的D e b u g包用来保存P L / S Q L调试信息。 CREATE TABLE debug_table ( linecount NUMBER, debug_str VARCHAR2(100) ); 12. 表s o u r c e和d e s t i n a t i o n 这两个表是由第3章中调试程序案例使用的。 CREATE TABLE source ( key NUMBER(5), value VARCHAR2(50) ); CREATE TABLE destination ( key NUMBER(5), value NUMBER); 1.5 小结 我们在本章概述了引入 P L / S Q L语言的目的及该语言的主要特点。除此之外,我们还讨论了 P L / S Q L和数据库版本号的对应规则,介绍了本书 C D - R O M中的内容和程序案例使用的数据库表。 在下面的两章中,我们将讨论 P L / S Q L的各种开发、调试以及运行环境。 第1章 PL/SQL介绍计计21下载下载 第2章 PL/SQL开发和运行环境 P L / S Q L块可以在多种不同特点及功能的环境下运行。我们在本章主要讨论定位 P L / S Q L引擎 的有关问题。除此之外,我们还将讨论各种可用来开发 P L / S Q L应用的环境,其中包括 O r a c l e本 身提供的开发工具和由第三方开发商提供的开发工具。 2.1 应用模式和P L / S Q L 一般的数据库应用可以分为三个部分: • 用户界面,负责提供应用的外观和使用方式。该部分负责处理用户的输入信息和显示处理 结果。 • 应用逻辑,这层的主要功能是控制应用的处理流程。 • 数据库,该层负责可靠地存储应用数据。 目前可以将上述三部分功能分配到不同位置的数据库应用设计模式主要有两类。 为了编译并运行一个P L / S Q L块,程序员必须将该块提交给 P L / S Q L引擎来处理。与J a v a语言 的虚拟机相类似, P L / S Q L引擎也是由编译器和运行时系统组成。借助于 O r a c l e公司和其他开发 商提供的开发工具,P L / S Q L可以用于应用的各个层次,并且 P L / S Q L引擎也可以宿主在不同的系 统中。 2.1.1 两层模式 两层模式,即客户 /服务器模式,是传统的应用设计模式。在这种模式中,应用由客户端程 序和服务器端程序两部分组成。客户端负责处理用户界面,而服务器端管理数据库。这种模式 的应用逻辑分为客户端和服务器端两部分。通常, P L / S Q L引擎驻留在服务器端,在个别情况下, P L / S Q L引擎也可以驻留在客户端。 1. 服务器端的P L / S Q L 从O r a c l e 6 . 0版开始,P L / S Q L就驻留在数据库服务器端,同时,该服务器也是 P L / S Q L引擎的 默认位置。由于数据库服务器可以处理 S Q L语句,所以S Q L语句和P L / S Q L块都可以送到该服务 器进行处理。一个客户应用,不管是用 O r a c l e开发工具实现的或使用其他开发工具编制的,都可 以向数据库服务器提交S Q L语句和P L / S Q L块。SQL *Plus就是一个这种客户应用的案例,该程序 可以在S Q L提示符下接收交互输入的S Q L语句和P L / S Q L命令并将其送往服务器执行。 例如,我们可以假设在SQL *Plus与服务器建立了连接的情况下输入下列的S Q L,P L / S Q L命令: 节选自在线代码SQL_PLSQL.SQL SQL> CREATE OR REPLACE PROCEDURE ServerProcedure AS 2 BEGIN 3 NULL; 4 END ServerProcedure;第2章 PL/SQL开发和运行环境计计23下载 5 / Procedure created. SQL> DECLARE 2 v_StudentRecord students%ROWTYPE; 3 v_Counter BINARY_INTEGER; 4 BEGIN 5 v_Counter := 7; 6 7 SELECT * 8 INTO v_StudentRecord 9 FROM students 10 WHERE id = 10001; 11 12 ServerProcedure; 13 14 END; 15 / PL/SQL procedure successfully completed. SQL> UPDATE classes 2 SET max_students = 70 3 WHERE department = 'HIS' 4 AND course = 101; 1 row updated. 注意 可以在本书的C D - R O M中找到上面的例子的代码。本书中存储在 C D - R O M中的程 序案例的文件名称在该例子的第一行注释中给予了说明,读者可以根据注释中提供的 X X X . s q l在C D - R O M中找到对应的程序文件。 C D - R O M中C O D E目录下的r e a d m e . h m t l文 件提供了对这些程序案例的说明。 图2 - 1演示了在服务器端的 P L / S Q L引擎对P L / S Q L块的处理过程。客户应用可以向服务器提 交P L / S Q L块(该块可以带有过程和包括调用服务器端存储过程的 S Q L语句),以及单独的S Q L语 句。如图所示, P L / S Q L块和S Q L语句通过网络送往服务器。一旦服务器收到了这些内容, S Q L 语句将直接进入服务器内含的 S Q L语句执行器,而 P L / S Q L块则送往P L / S Q L引擎进行语法分析。 在该块的运行期间, P L / S Q L引擎负责执行过程语句(如赋值语句和存储过程调用)。对于该块 中出现的S Q L语句(如S E L E C T语句等),P L / S Q L引擎将它们送往S Q L语句执行器执行。 2. 客户端的P L / S Q L 除了在服务器端的 P L / S Q L引擎外,几种O r a c l e开发工具也带有P L / S Q L引擎。由于这些开发 工具是运行在客户端的,所以它们的 P L / S Q L引擎也可以运行在客户端。这样一来,借助于客户 端的P L / S Q L支持,P L / S Q L块中的过程语句就可以在本地运行,而没有必要送到服务器端。例如 开发工具Oracle Forms(该工具是Oracle Developer的一部分)自身带有 P L / S Q L引擎;在O r a c l e D e v e l o p e r工具包中,如Oracle Reports也带有P L / S Q L引擎。需要指出的是这种引擎与 P L / S Q L服 务器端的引擎有所不同。P L / S Q L块只能出现在客户端应用中,并且该块必须用开发工具来编制。 假设一个Oracle Forms应用包括了触发器和过程,这些语句都在客户端执行。只有该程序中的S Q L语句和调用服务器端存储子程序的语句被送往服务器进行处理。如图 2 - 2所示,客户端的 P L / S Q L引擎可以处理过程语句。 与服务器端的P L / S Q L一样,应用程序提交的单独的 S Q L语句(如U P D AT E语句)直接通过 网络送往服务器端的 S Q L语句执行器。不同的是, P L / S Q L块是在客户端直接处理。任何过程语 句(如赋值语句)的处理都不会引起网络传输。 P L / S Q L块中的S Q L语句要提交给S Q L语句执行 器,对服务器端的存储子程序的调用则是送到服务器端的 P L / S Q L引擎执行。 图2-1 服务器端P L / S Q L引擎 24计计第一部分 PL/SQL介绍及开发环境 下载 客户应用 匿名块被整个发往服务器端 的PL/SQL引擎来执行 过程语句(包括对过程的调 用)由PL/SQL引擎处理 PL/SQL块内的SQL语句通过 PL/SQL引擎,然后到SQL 执行器 SQL 语句执行器 Oracle 数据库服务器 PL/SQL 引擎 过程语句执行器 SQL 语句从客户端直接 发往SQL语句执行器 数据通过网络传送第2章 PL/SQL开发和运行环境计计25 图2-2 客户端P L / S Q L引擎 3. Oracle 预编译器 程序员可以使用Pro *C/C++和Pro *COBOL一类的O r a c l e预编译器创建运行在服务器端的应 用程序。用这种方法实现的应用将不包括 P L / S Q L引擎,因此,由这种应用提交的 S Q L和P L / S Q L 语句都要送到服务器进行处理。 客户应用 匿名块被整个发往处理本地 过程语句的客户端 PL/SQL 引擎 调用服务器端存储过程从客 户端 PL/SQL引擎到服务器 端 PL/SQL引擎 Oracle 数据库引擎 SQL 语句执行器 服务器端PL/SQL引擎 过程语句执行器 数据通过网络传送 SQL 语句也直接从 客户端发往SQL 语句执行器 PL/SQL 块内的SQL语句从 客户端PL/SQL引擎到SQL语 句执行器 过程语句执行器 客户端PL/SQL引擎 下载预编译器本身包括了 P L / S Q L引擎,该引擎在预编译中用来校验应用代码中匿名块的语法和 语义是否正确。预编译功能是 O r a c l e开发工具的一大特点。 4. 引擎间的通信 在图2 - 2所示的流程图中,有两个各自独立但又相互通信的 P L / S Q L引擎。例如,一个运行在 客户端P L / S Q L下的报表(F o r m)中的触发器可以调用在服务器端 P L / S Q L下运行的存储过程, 这一类的网络通信是通过远程过程调用实现的。借助于类似的机制,通过数据库连接可以实现 不同P L / S Q L引擎之间的通信。 在上述情况下,不同引擎中的 P L / S Q L对象可以相互依赖。这种存在依赖关系的类型工作方 式与在同一个数据库中的 P L / S Q L对象几乎完全一样,不同之处是程序中必须提供一些防止产生 误解的声明。本书的第5章提供了进一步的信息。 总的来说,两个P L / S Q L引擎可以是不同的版本。例如, O r a c l e开发器1 . 2版使用了P L / S Q L第 1版,而服务器使用了 P L / S Q L版本2(如果使用了O r a c l e 8,就需要使用PL/SQL 版本8),这就意 味着P L / S Q L版本2或更高的版本提供的功能,如用户自定义表和记录,定长的 C H A R数据类型和 其他一些功能可能不会出现在客户端引擎中。尽管 O r a c l e开发器第二版以上的工具中包括了 P L / S Q L 8 . 0版,但在引擎之间仍然存在着版本差别,因此,每一个版本可用的功能也不一样。 2.1.2 三层模式 在三层模式中,用户界面,应用逻辑和数据库是三个各自独立的部分。该模式下的客户是 典型的瘦客户类型,如浏览器一类的客户软件。应用层逻辑全部位于称为应用服务器的独立层 中。在这种环境下,P L / S Q L引擎通常只放置在服务器中。 O r a c l e应用服务器(O A S)具有一般应用服务器的全部功能。通过 P L / S Q L盒式磁带机,读 者可以运行服务器上的存储过程并返回 H T M L页面形式的处理结果,这项功能可以借助于 O A S 提供的PL/SQL We b工具箱实现。图2 - 3演示了三层模式的内部结构。有关 P L / S Q L盒式磁带机和 We b工具箱的进一步介绍,请读者参考 O r a c l e文档。 图2-3 三层模型示意图 2.2 PL/SQL开发工具介绍 开发调试P L / S Q L应用可以使用多种不同的开发工具,每种开发工具都有其优点与不足。表 26计计第一部分 PL/SQL介绍及开发环境 下载 客户浏览 客户提交对能生成 HTML 输出的服务 器端子程序的调用 对服务器而言,P L / S Q L盒式磁带机相当 于客户,发送P L / S Q L 块和S Q L语句到服务 器去执行 Oracle 数据库服务器 SQL 语句执行器 服务器PL/SQL 引擎 过程语句执行器PL/SQL Web 盒 式磁带机 应用服务器2 - 1介绍了我们在本章详细讨论的开发工具的基本情况。从表中我们可以看出, SQL *Plus是 O r a c l e公司与服务器一起提供的开发工具,其他的开发工具则是由第三方开发商提供的。本书的 C D - R O M中带有第三方开发工具的试用版,我们将在下面几节中将介绍这些开发工具的使用方 法。在第3章,我们将详细介绍这些开发工具的调试功能。 注意 C D - R O M中的开发工具存放在Development To o l s目录中,C D - R O M中根目录下的 r e a d m e . h t m l文件中有对开发工具的说明。 表2-1 PL/SQL开发环境一览 工具名称 开发商 We b站点地址 是否随C D - R O M提供 SQL *Plus O r a c l e公司 w w w. o r a c l e . c o m 不 Rapid SQL E m b a r c a d e r o技术公司 w w w. e m b a r c a d e r o . c o m 是 X P E D I T E R / S Q L C o m p u w a r e w w w. c o m p u w a r e . c o m 是 SQL Navigator Quest Software w w w. q u e s t . c o m 是 TO A D Quest Software w w w. q u e s t . c o m 是 w w w. t o a d s o f t . c o m S Q L - P r o g r a m m e r Sylvain Faust International w w w. s f i - s o f t w a r e . c o m 是 为了保持一致性,下面讨论的每一种开发工具都采用相同的模式,即使用几种不同类型的 P L / S Q L对象案例来进行说明,以便了解每一种工具在同一个环境下的优缺点。表 2 - 2描述了样本 模式的具体内容,该样本模式的每一项都可以在执行安装脚本 r e l Ta b l e s . s q l后运行表中指示的脚 本文件来自动创建。本书的在线文件 S e t u p . s q l也可以用来运行这些脚本文件。需要指出的是,在 创建外部过程和函数时,需要提供系统特权 C R E ATE LIBRARY。 注意 在下面几节中,每种开发工具都带有图形窗口。由于受到本书空间的限制,某些 工具的图形窗口只能存放在本书的 C D - R O M中,有关这些图形窗口的说明,请看 C D - R O M中目录online Chapters中的文件c h 0 2 S c r e e n S h o t s . h t m l。 表2-2 样本模式一览表 对象名称 对象类型 可运行的脚本 A d d N e w S t u d e n t 过程 c h 0 4 / A d d N e w S t u d e n t . s q l A l m o s t F u l l 函数 c h 0 4 / A l m o s t F u l l . s q l M o d e Te s t 过程 c h 0 4 / M o d e Te s t . s q l C l a s s P a c k a g e 包和包体 c h 0 4 / C l a s s P a c k a g e . s q l P o i n t 对象类型和类型体 c h 1 2 / P o i n t . s q l O u t p u t S t r i n g 外部过程和函数 c h 1 0 / O u t p u t S t r i n g . s q l 2.2.1 SQL*Plus SQL *Plus可能是最简单的 P L / S Q L开发工具。该工具允许用户交互式地从输入提示符输入 S Q L语句和P L / S Q L块,这些输入的语句直接送到数据库执行,并将执行结果显示在终端屏幕上。 该工具支持字符环境,没有内置的本地 P L / S Q L引擎。 第2章 PL/SQL开发和运行环境计计27下载一般,SQL *Plus是与O r a c l e服务器捆绑销售,并作为标准 O r a c l e安装过程的一部分执行。 读者可以参阅SQL *Plus用户指南和参考手册来了解该工具的详细说明和有关命令。 SQL *Plus的命令不区分大小写字母。例如,下面三个命令都可以声明连接变量: SQL> VARIABLE v_Num NUMBER SQL> variable v_Char char(3) SQL> vaRIAbLe v_Varchar VarCHAR2(5) 1. 连接数据库 在SQL *Plus下输入S Q L或P L / S Q L命令之前,必须先实现与数据库服务器的连接。下面是实 现数据库连接的两种常用方法: • 在SQL *Plus命令行输入用户标识(U s e r i d)和口令,或输入连接字服串。 • 进入SQL *Plus后使用C O N N E C T语句。 在下面的连接例子中,读者可以看到,如果没有定义口令的话, SQL *Plus将不会为用户提 示口令内容并且不将输入字符回送到屏幕显示。 $ sqlplus example/example SQL*Plus: Release 8.0.6.0.0 - Production on Wed Nov 3 10:29:11 1999 (c) Copyright 1999 Oracle Corporation. All rights reserved. Connected to: Oracle8 Enterprise Edition Release 8.0.6.0.0 - Production With the Objects option PL/SQL Release 8.0.6.0.0 - Production SQL> exit Disconnected from Oracle8 Enterprise Edition Release 8.0.6.0.0 - Production With the Objects option PL/SQL Release 8.0.6.0.0 - Production $ sqlplus example SQL*Plus: Release 8.0.6.0.0 - Production on Wed Nov 3 10:29:15 1999 (c) Copyright 1999 Oracle Corporation. All rights reserved. Enter password: Connected to: Oracle8 Enterprise Edition Release 8.0.6.0.0 - Production With the Objects option PL/SQL Release 8.0.6.0.0 - Production SQL> connect example/example Connected. SQL> connect example/example@v806_tcp Connected. 2. SQL *Plus中的块操作 当在SQL *Plus中执行一个S Q L语句时,语句末尾的分号标识了该语句的结束。该分号不属 于语句本身,它是一个独立的语句终止符。当 SQL *Plus读到一个分号时,它就知道该语句已经 28计计第一部分 PL/SQL介绍及开发环境 下载第2章 PL/SQL开发和运行环境计计29下载 结束并将其发送到数据库执行。另一方面,对于 P L / S Q L块来说,分号则是块本身的语法部分, 而不是语句终止符。当输入关键字 D E L C A R E或B E G I N时,SQL *Plus就能够检测到该关键字并 确认正在输入的是 P L / S Q L块,不是S Q L语句。在这种情况下, SQL *Plus仍然需要确认输入的 P L / S Q L块何时结束,我们使用一个正斜杠来表示块结束,这种正斜杠是 SQL *Plus RUN命令的 缩写形式。 请注意,图 2 - 4中更新表 r e g i s t e r e d _ s t u d e n t s的P L / S Q L块之后有一个正斜杠。该块后面的 S E L E C T语句由于使用了分号而不用再输入斜杠(如果需要的话,读者也可以在 S Q L语句中使用 斜杠来代替分号)。 图2-4 在SQL *Plus中输入P L / S Q L块 3. 替换和连接(b i n d)变量 由于P L / S Q L是服务器端的专用语言,所以该语言不支持用户输入和输出。文件输入输出操 作是通过包U T L _ F I L E实现的(需PL/SQL 2.3版和更高版本支持,本书第 7章有专门介绍),借助 于B F I L E接口,O r a c l e 8可以读入外部文件(第 1 5~1 6章有专文论述)。除此之外,与SQL *Plus 一样,使用D B M S _ O U T P U T包可以实现有限的屏幕输出操作(详细内容请参阅第 3章)。 上述的各种方法都不支持接收用户的输入,其主要原因是 P L / S Q L所在的运行环境所致。例 如,Pro *C 程序可以使用C语言的 I / O库函数功能来接收用户输入并将其传入 P L / S Q L中。 SQL *Plus提供了两种不同类型的变量来接收用户的输入并通过多个运行存储信息,这两种 变量就是替换和连接变量。 替换变量 替换变量在 P L / S Q L块或 S Q L语句中是用字符符号“ &”描述的(使用 S E T D E F I N E命令可以指定替换“&”的字符)。SQL *Plus在将P L / S Q L块或S Q L语句发送到服务器前 要对变量进行彻底的原文替换,类似于 C语言对宏的处理。 图2 - 5中的P L / S Q L块中使用了替换变量。该块运行了两次,每次都对变量 v _ S t u d e n t I D给予 了不同的初始值。用户分别输入了数值 1 0 0 0 4和1 0 0 0 5,这两个数值在该块中被& s t u d e n t - i d替换。图2-5 SQL*Plus替换变量 需要注意的是替换变量实际上不会占用内存空间。 SQL *Plus在把该块发送到数据库执行前 使用用户输入的数值来置换替换变量。由于这种原因,替换变量只能用于输入。下面讨论的连 接变量则可以用于输入和输出双向操作。 提示 假设现在从提示符S Q L >下输入下列S Q L语句: SQL> SELECT * FROM students WHERE first_name = &first_name; 输入该语句后,当 SQL *Plus提示输入时,用户必须要在输入的字符串两端使用单 引号,如‘S C O T T’。现在请比较下面的语句: SQL> SELECT * FROM students WHERE first_name = '&first_name'; 在这种情况下,用户不必使用单引号,因为该语句中已经有了单引号。单引号的使 用与否取决于要替换的内容。 连接变量 我们在上面讲过,替换变量不会占用内存空间,然而, SQL *Plus可以分配内存 空间供P L / S Q L块和S Q L语句内部使用。因为这种存储空间位于块的外部,所以它可以为一个以 上的连续的块和 S Q L语句共用,并且该存储空间的内容还可以在块结束退出后打印显示,我们 描述的这种变量就是连接变量。该类变量的图解说明在 C D - R O M中提供的图C D 2 - 1中。连接变量 v _ C o u n t是用S Q L * P l u s的VA R I A B L E命令为其分配内存的。请注意, VA R I A B L E命令只能在S Q L 提示符下使用,它不能用在 P L / S Q L块中。在块内部,连接变量是用起始处的冒号来定界的。在 该块结束后,命令 P R I N T将显示该变量的值。 S Q L * P l u s允许使用的合法连接变量类型包括 VA R C H A R 2、C H A R和N U M B E R。连接变量R E F C U R S O R只能用于SQL *Plus3.2版和更高版本。 30计计第一部分 PL/SQL介绍及开发环境 下载第2章 PL/SQL开发和运行环境计计31下载 连接变量N C H A R、N VA R C H A R 2、C L O B和N C L O B只能用于SQL *Plus8.0版及更高版本。如果 没有特殊指定连接变量类型 VA R C H A R 2、C H A R、N C H A R或N VA R C H A R 2的长度,这些变量的 默认长度都为1,N U M B E R类型的连接变量不受精度和比例的限制。 4. 变量和数据库对象 由于替换变量在 P L / S Q L块被发送到服务器之前就被进行了替换,所以这类变量可以作为 S Q L语句和数据库对象的语法部分使用,而连接变量则不能在这种情况下使用。下面的 S Q L * P l u s对话演示了这种规则。 节选自在线代码Variables.sql SQL> SELECT &columns 2 FROM classes; Enter value for columns: department, course old 1: SELECT &columns new 1: SELECT department, course DEP COURSE --- --------- HIS 101 HIS 301 CS 101 ECN 203 CS 102 MUS 410 ECN 101 NUT 307 MUS 100 9 rows selected. SQL> SELECT first_name, last_name 2 FROM students 3 WHERE &where_clause; Enter value for where_clause: ID = 10001 old 3: WHERE &where_clause new 3: WHERE ID = 10001 FIRST_NAME LAST_NAME -------------------- -------------------- Margaret Mason SQL> -- But a bind variable cannot be used in this manner: SQL> VARIABLE where_clause VARCHAR2(100) SQL> SQL> BEGIN 2 :where_clause := 'WHERE ID = 10001'; 3 END; 4 /32计计第一部分 PL/SQL介绍及开发环境 下载 PL/SQL procedure successfully completed. SQL> PRINT :where_clause WHERE_CLAUSE ------------------------------------------------------------ WHERE ID = 10001 SQL> SELECT first_name, last_name 2 FROM students 3 WHERE :where_clause; WHERE :where_clause * ERROR at line 3: ORA-00920: invalid relational operator 读者可以使用动态S Q L在运行期间建立S Q L语句和P L / S Q L块,并使用P L / S Q L变量来构造这 些语句。有关详细内容,请看本书的第 8章。 5. 使用E X E C U T E命令调用存储过程 存储过程调用只能从P L / S Q L块的可执行语句部分或其异常处理部分中执行。 SQL *Plus为这 种调用提供了一个有用的简写方式,即命令 E X E C U T E。该命令实现的功能是在其命令参数之前 加上B E G I N,而在参数之后加上E N D。并同时在调用语句后加入一个分号,表示调用语句结束。 经过这种处理后形成的块就被提交给数据库执行。例如,假设我们在 S Q L命令提示符下输入下 列命令: SQL> EXECUTE ClassPackage.AddStudent(10006,'CS',102) 则SQL *Plus就可以生成下面的P L / S Q L块: BEGIN ClassPackage.AddStudent(10006,'CS',102); END; 该块被发送到数据库执行。关键字 E X E C U T E之后的分号是该命令的选择项,这对所有的 SQL *Plus命令都是适用的。并且如果使用了该分号的话,它将被忽略不记,就像命令 P R I N T, VA R I A B L E一样,E X E C U T E是SQL *Plus的命令,它不能用在P L / S Q L块内部。 提示 E X E C U T E命令并不在S Q L缓冲区中存储生成的匿名块,但如果匿名块是直接输 入的话,该命令可以实现这种功能。 6. 使用文件 SQL *Plus可以把当前的P L / S Q L块或S Q L语句保存在文件中,并在需要时,将该文件读入系 统执行。SQL *Plus的这种功能对于实现先开发 P L / S Q L程序后运行调试来说非常有用。例如, 读者可以把命令C R E AT E或R E P L A C E存储在一个文件中。利用这种方法,任何对该过程的修改 都可以通过该文件实现。为了保存数据库的修改,我们可以简单地把该文件读如到 SQL *Plus中。 文件中可以存放一个以上的命令。 SQL *Plus的G E T命令将文件从磁盘读入到本地缓冲区中存放。输入一个正斜杠就可以运行 读入的文件,就像该文件中的命令是从键盘直接输入的一样。如果文件的结尾处有一个正斜杠 的话,就可以使用G E T命令的缩写形式@将该文件读入并执行。例如,假设文件 f i l e . s q l包含下列命令行: 节选自在线代码File.sql BEGIN FOR v_Count IN 1..10 LOOP INSERT INTO temp_Table (num_col, char_col) VALUES (v_Count, 'Hello World!'); END LOOP; END; / SELECT * FROM temp_table; 现在,我们可以从S Q L提示符模式下使用下面的命令运行该文件。 SQL> @file 执行该文件的输出结果在本书 C D - R O M中的图C D 2 - 2所示。命令SET ECHO ON(该图中的 第一条命令)告诉SQL *Plus在读入该文件时把其内容送到屏幕显示。 7. 使用SHOW ERRORS命令 在创建存储过程时,有关该过程的信息存储在数据字典中。所有的编译错误都存储在 u s e r _ e r r o r s的数据字典中。 SQL *Plus提供了一个可以查询该数据字典并报告错误的命令 S H O W E R R O R S。本书的C D - R O M上的图C D 2 - 3演示了该命令的使用方法。命令 SHOW ERRORS可以 用在SQL *Plus报告了下列错误信息后: Warning: Procedure created with compilation errors. 2.2.2 Rapid SQL 由Embarcadero Te c h n o l o g i e s公司开发的Rapid SQL(快速S Q L)提供了图形用户界面的开发 环境,其主要功能如下: • PL/SQL和S Q L语句的自动格式化 • PL/SQL调试器 • 支持O r a c l e 8的对象类型和表分区 • SQL 任务调度 • 工程管理 • 支持Wi n d o w s活动脚本(active scripting) • 支持第三方版本控制系统 • 集成数据库开发和We b程序设计 有关该工具的安装和使用方法,请参考有关在线帮助。 1. 连接数据库 当第一次启动运行 Rapid SQL时,该程序将显示如图 2 - 6所示的窗口。该窗口左面的窗格中 显示了按数据源分类的可浏览数据库对象。右面的窗格是工作窗口,用来显示正在浏览的不同 类型的对象,该窗口中的数据源记录了远程数据库的所有相关信息,如数据库类型、用户标识、 第2章 PL/SQL开发和运行环境计计33下载34计计第一部分 PL/SQL介绍及开发环境 下载 口令和连接信息。当Rapid SQL第一次运行时,它将通过检索当前计算机上的 SQL *Net或N e t 8配 置自动地来发现可用的数据源。双击某个特殊的数据源将会启动一个对话框来接收连接使用的 用户标识和口令,读者可以参考 C D - R O M中的图C D 2 - 4来了解该操作过程。除此之外,也可以通 过数据源菜单增加新的数据源。 图2-6 Rapid SQL的主窗口 如果选择已建立的数据源的话,Rapid SQL将自动使用用户标识和口令,而不需要再次输入。 Rapid SQL允许同时打开多个数据库连接。 2. 浏览数据库对象 一旦建立了数据库连接,我们就可以浏览左窗格中的对象。单击对象类型将使显示的目录 中增加所选对象,这时,我们就可以进一步观察列表目录中某个单独的对象。双击某个对象将 会启动创建该对象的S Q L或P L / S Q L。例如,图2 - 7中显示了对象浏览器中的包和表以及在右面的 工作区中显示了表c l a s s e s的D D L。 3. 编辑器类型 Rapid SQL提供了两类不同的窗口供用户输入 S Q L语句和P L / S Q L块,每种窗口适用于不同 的任务: • S Q L编辑器是通用的编辑器,适用于输入 SQL DML和D D L语句,以及匿名 P L / S Q L块和 C R E AT E语句。 • D D L编辑器允许用户选择待创建的对象类型并自动地用结构对其进行填充。 这两种编辑器可以从主窗口的 File | New 的子菜单中选择。C D - R O M中的图C D 2 - 5图示了使用匿名块的P L / S Q L编辑器。单击绿色的三角形将把该块发送到服务器运行。 图2-7 浏览表c l a s s e s 4. 自动格式 任何编辑器都可以对 S Q L和P L / S Q L语句进行自动格式。自动格式操作包括缩进和各种语法 元素的识别。这种功能非常有助于根据程序员自己的程序设计风格来格式化 P L / S Q L块。C D - R O M中的图C D 2 - 6中列举了自动格式的选择项。 5. 观察错误 把P L / S Q L块提交给服务器后,只要单击 E r r o r s工具条就可以显示任何错误信息。这时浏览 器中与错误对应的对象上也标有 X标记,表示该对象有语法错误。 C D - R O M中图2 - 7演示了显示 错误的窗口图形。错误将显示在 P L / S Q L文本下面的窗格中。单击某个错误将使光标移动到产生 错误的语句所在的位置。 6. 作业调度 Rapid SQL将Wi n d o w s 9 8和N T提供的作业调度合并入该软件中,这种功能的引入可以使用户 在确定的时间独立于Rapid SQL Pro运行作业。 7. 集成We b开发功能 借助于附加的编辑器, Rapid SQL还可以编辑J a v a代码和H T M L页面。有关这方面的信息, 请参阅在线文档。 2.2.3 XPEDITER/SQL X P E D I T E R / S Q L是由Compuware 公司发行的开发工具,它也提供图形用户界面开发环境。 第2章 PL/SQL开发和运行环境计计35下载36计计第一部分 PL/SQL介绍及开发环境 下载 该工具的主要功能如下: • 自动格式P L / S Q L和S Q L语句 • 提供P L / S Q L调试器 • 支持O r a c l e 8对象类型 • 工程管理 • 对任何应用提供调试和跟踪功能 • 支持第三方版本控制系统 1. 连接数据库 当XP E D I T E R / S Q L首次运行时,该软件将提示用户建立数据库连接。为了方便建立数据库 连接,我们可以事先存储不同数据库连接所需的配置信息,这些信息包括用户标识和数据库连 接字符串。可以将用户标识的格式指定为“用户标识 /口令”的格式,这样一来,用户口令也可 以存储在配置文件中。建立数据库连接的对话框的窗口在本书 C D - R O M中的图 C D 2 - 8显示。 XP E D I T E R / S Q L允许同时建立多个连接。 2. 服务器端的安装 为了正确的使用X P E D I T E R / S Q L,必须在服务器端进行安装操作。该安装将创建一个包括 存储XP E D I T E R / S Q L所需信息的表的数据库用户。该数据库用户的创建可以在安装过程中完成, 也可以在安装结束后,使用数据库安装向导来实现。该向导的窗口在 C D - R O M中的图C D 2 - 9中演 示。数据库安装向导允许用户安装,卸载或为多个数据库更新服务器端对象。 图2-8 浏览对象P o i n t类型的窗口第2章 PL/SQL开发和运行环境计计37下载 3. 浏览数据库对象 X P E D I T E R / S Q L的对象浏览窗口是用来观察数据库对象信息和对话特权的专用窗口。 XP E D I T E R / S Q L允许用户为同一个数据库连接打开多个浏览器,但每个浏览器只能显示一个连 接的信息。对象浏览器的窗口如图 2 - 8所示。该窗口的左窗格显示了对象的树形目录,用户可以 查看希望的对象的类型和拥有者。该窗口右面的窗格则显示选择的对象的有关信息,包括该对 象是否带有经过编译的调试信息和其他相关信息。(用户可以通过选择菜单项 Object | Debug On 或 Objetc | Debug Off来编译带有或不带有调试信息的对象。) 4. 编辑数据库对象 双击对象浏览器中的某个对象将会在 S Q L的N o t e p a d编辑器窗口中打开该对象文件。 S Q L的 N o t e p a d编辑器窗口可以用来编辑所有类型的数据库对象,及 P L / S Q L存储过程和匿名块。例如, 图2 - 9中所示的窗口显示了在 N o t e p a d编辑器中对象P o i n t的类型说明。编辑器对 P L / S Q L代码自动 进行了格式设置。除此之外,单击该窗口中的红色的三角形按钮可以运行编辑器中的 S Q L语句, 如果单击窗口中的绿色三角形按钮可以对 S Q L语句进行调试。 5. 显示错误 单击位于SQL Notepad编辑器底部窗格中的SQL Errors标签,用户可以查看编译后报告的对 象错误信息。这时显示的是当前对象或块的错误信息。如果单击某个错误,该编辑器将把光标 定位到发生错误所在过程的语句行上。 C D - R O M中图C D 2 - 1 0显示了过程To o M a m y E r r o r s的错误。 除此之外,非法的对象也在对象浏览中被标明。 图2-9 编辑对象P o i n t的窗口6. 使用模板 X P E D I T E R/ S Q L的另一个常用的工具是模板编辑器。该编辑窗口可以在 SQL Notepad编辑器 窗口中通过选择Tools | Templates Editor子菜单激活。在该窗口中,用户可以把预定义的 P L / S Q L 代码段插入到激活的 N o t e p a d中。除了可以使用创建新过程,函数,触发器,包和类型外的语法 外,模板编辑器还包括对公用内置包(如 D B M S _ S Q L,D B M S _ O U T P U T等)的调用语句。本书 C D - R O M中的图C D 2 - 11有模板编辑器的图形窗口的样板。除此之外,在该窗口中双击某个模板 将把该模板的文本复制到N o t e p a d窗口中。 7. DBPartner 我们讨论的X P E D I T E R / S Q L的最后一个功能是工具 D B P a r t n e r。该实用工具允许用户从任何 程序中捕捉S Q L语句并在X P E D I T E R / S Q L中对其进行编辑和调试。利用这种功能,用户可以在 没有源程序的情况下调整或调试程序。 2.2.4 SQL Navigator S Q L N a v i g a t o r是由Quest Software公司提供的开发工具。它也提供了图形用户界面,其主要 功能如下: • 自动格式P L / S Q L和S Q L语句 • 提供P L / S Q L调试器 • 数据库浏览器 • 支持O r a c l e 8对象类型和O r a c l e 8 i类型 • 代码模板 • 支持第三方版本控制系统 1. 连接数据库 与上述X P E D I T E R / S Q L开发工具一样,SQL Navigator在其第一次启动运行时也要请求用户 建立数据库连接。 SQL Navigator可以自动保存连接配置文件,但用户的口令不能与该文件一起 保存。用来建立数据库连接的程序窗口如 C D - R O M中图C D 2 - 1 2所示。如果用户在初次启动时没 有建立数据库连接,该工具将在一个编辑窗口或浏览窗口中用同一个对话框请求用户建立连接。 SQL Navigator支持同时对不同数据库的多连接操作。 2. 安装服务器端的对象 SQL Navigator的多个选择项要求在服务器中创建一个名为 S Q L N AV的用户。C D - R O M中图 C D 2 - 1 3所示的服务器端安装向导可以帮助建立必要的用户和对象。该向导可以作为安装程序的 一部分运行,也可以在安装完成后,选择菜单项 Tools | Server Side Installation Wi z a r d启动该程 序运行。服务器端的安装是实现 SQL Navigator的程序员组编程、第三方版本控制、 S Q L Navigator Tu n e r等功能的必要步骤。 3. 浏览数据库对象 SQL Navigator的DB 浏览器窗口是用来浏览数据库对象的工具。用户可以从该窗口提供的 树形视图中选择要查看的对象的类型和所有者。该浏览器的一个独特的功能是提供了过滤器。 使用过滤器,用户可以选择希望显示的那些对象类型。该浏览器提供了多个预定义的过滤器, 38计计第一部分 PL/SQL介绍及开发环境 下载用户也可以创建自己的过滤器。图 2 - 1 0所示的DB 浏览器窗口中使用了三个过滤器。 图2-10 带有三个过滤器的DB 浏览器窗口 如果用户建立了与Oracle8i 数据库的连接,SQL Navigator还在其数据库浏览中支持O r a c l e 8 i J a v a存储过程(J S P)。这时,在浏览器的树形目录中将增加有关 J a v a类、Java 数据源和资源的目 录入口。虽然用户不能直接在 SQL Navigator中对J a v a代码进行编辑,但可以对已在数据库中的 Java 数据源进行编译。有关在O r a c l e 8 i中使用J S P的方法,请看本书第1 0章的内容。 4. 编辑数据库对象 SQL Navigator提供了三种编辑窗口: • SQL编辑器,该编辑器只能编辑一个 S Q L语句或脚本。 • 触发器编辑器,该编辑器用来创建或编辑数据库触发器。除此之外,该编辑器还支持 O r a c l e 8的Instead-of 触发器类型。 • 存储程序编辑器,该编辑器用来创建或编辑存储的 P L / S Q L代码。 当用户单击 D B浏览器中的对象时,相应的编辑器就会开始运行。用户也可以使用 S Q L N a v i g a t o r的工具条直接打开各种编辑器。图 2 - 11显示了在存储程序编辑器中的函数 A l m o s t F u l l。 该窗口突出显示了该函数中的 P L / S Q L代码。 5. 显示错误 如果在编译P L / S Q L对象的过程中出现错误的话,这些错误将在用户在存储程序编辑器中编 译对象时显示出来。单击突出显示错误的源程序行,以及双击一个错误时都将启动启动一个来 自于 O r a c l e文档资料中指示错误原因和方式的窗口。 C D - R O M中的图 C D 2 - 1 4显示了过程 To o M a n y E r r o r s的错误信息。 第2章 PL/SQL开发和运行环境计计39下载图2 - 11 编辑函数A l m o s t F u l l的窗口 6. 代码模板 可以在SQL Navigator工具菜单中启动的代码助理( Code Assistant)提供了P L / S Q L和S Q L结 构中通用库。加亮显示某个结构将在代码助理信息窗口显示该结构的描述信息,双击该描述区 将把该信息复制到现行的编辑窗口中并可由用户进行定制。代码助理的窗口图形显示在 C D - R O M中图C D 2 - 1 5中。 2.2.5 TO A D TO A D是O r a c l e应用开发者工具的缩写。该工具最早是从S Q L N a v i g a t o r分离出来的。现在,该工 具是由Quest Software和SQL Navigator共同开发销售。这样做的结果导致这两个工具在某些方面(如 许可机制)具有相同的功能,但是,我们马上也会看到它们之间的不同。TO A D提供了下述功能: • 自动格式P L / S Q L和S Q L语句 • 提供P L / S Q L调试器 • 数据库浏览器 • 支持O r a c l e 8对象类型 • 代码模板 • 支持第三方版本控制系统 TO A D是一种功能完善的轻量级开发工具。该工具所需的磁盘和内存空间都比其他工具要小的 多。 1. 连接数据库 TO A D可支持多数据库连接。当该应用首次启动运行时,它使用一个对话框提示用户建立数 40计计第一部分 PL/SQL介绍及开发环境 下载第2章 PL/SQL开发和运行环境计计41下载 据库连接(该对话框在C D - R O M中的图C D 2 - 1 6中)。如果需要建立更多的连接的话,用户可以通 过菜单命令File | New Connection来实现。一旦建立了连接,该连接就将保持到用户通过执行菜 单命令 File | Close Connection来将其关闭为止。连接使用的口令可以存储在连接配置文件中。 TO A D管理连接的一种特殊功能是与任何窗口关联的连接可以动态地进行变更。该功能使应 用程序可以工作在多数据库环境下时将其他不用的窗口最小化。但对于一个给定的工作窗口来 说,它只能与一个对话相关联。 2. 浏览数据库对象 TO A D提供了两种不同的数据库浏览器,它们是模式浏览器( Schema Browser)和对象浏览 器(Object Browser)。图2 - 1 2所示的模式浏览器允许用户选择 O r a c l e 7类型的表、过程和包。该 浏览器提供的目录结构与我们在上面介绍的其他三种开发工具所提供的树形目录结构不同,模 式浏览器中有选择对象类型的标签,这些标签显示在窗口中左面的窗格中。该窗口的右窗格显 示对象的详细内容,用户可以从该浏览器中编译或删除对象。 对象浏览器只能用来查看 O r a c l e 8的对象类型和类型体。类似于模式浏览器,对象浏览器允 许用户查看和修改对象类型和类型体的属性。显示对象 P o i n t的对象浏览器的图形窗口在 C D - R O M中的图C D 2 - 1 7中。 3. 编辑数据库对象 TO A D提供了两种编辑窗口: S Q L编辑窗口和存储过程编辑窗口。正如这两个窗口的名称所 表示的含意那样,S Q L编辑窗口是用于编辑单个 S Q L语句或S Q L脚本的,而存储过程编辑窗口则 用来编辑存储过程、函数、包和触发器。在存储过程编辑窗口下,用户可以编译,运行,或调 试过程。图2 - 1 3所示的是在存储过程编辑窗口中显示的过程 O u t p u t S t r i n g,该过程是一个用 C语 图2-12 模式浏览器窗口言实现的外部过程。存储过程编辑窗口可以从数据库对象或文件中载入并用来创建新的对象。 图2-13 存储过程编辑窗口 4. 显示错误信息 如果在编译过程中出现了编译错误,这些错误将显示在存储过程编辑窗口中下面的错误显 示窗格中。当使用蓝色三角形按钮单击错误时,与产生这些错误有关的源程序行将突出显示, 本书C D - R O M中的图C D 2 - 1 8演示了这种情况。除此之外,也可以使用模式浏览器来查看非法对 象的错误。 5. 代码模板 TO A D支持通用P L / S Q L和S Q L结构使用的代码模板。在该代码模板下,可以不用输入所需 的结构,用户只要使用键盘快捷键就可以实现。其具体操作是,先在编辑窗口中输入键盘快捷 键,然后再按C T R L - S PA C E B A R,这时该快捷键将被一个完整的结构替代。代码模板可以在编 辑选择项窗口中查看,如C D - R O M中的图C D 2 - 1 9所示。除此之外,用户还可以编辑现存的模板, 或加入自己建立的模板。 2.2.6 SQL-P r o g r a m m e r 最后一个介绍的图形用户界面开发工具是 S Q L - P r o g r a m m e r,该工具是由 Sylvain Faust I n t e r n a t i o n a l开发的。它支持下列功能: • 自动格式P L / S Q L和S Q L语句 • 提供P L / S Q L调试器 • 数据库浏览器 42计计第一部分 PL/SQL介绍及开发环境 下载• 支持O r a c l e 8对象类型 • 代码模板 • 支持第三方版本控制系统 • 数据库对象脚本编制 1. 连接数据库 S Q L - P r o g r a m m e r支持对不同数据库的多点同时连接。 C D - R O M中的图C D 2 - 2 0是连接对话框 的窗口显示。虽然连接口令不能存储在连接配置文件中,但系统可以为不同的服务器存储不同 的连接配置文件。连接对话框还可以显示已打开的连接。 2. 浏览数据库对象 S Q L - P r o g r a m m e r的S Q L浏览窗口提供了查看数据库对象的功能,该窗口中显示的对象按模 式和对象类型进行排序。如果当前有一个以上的连接处于活动状态的话,则单个 S Q L浏览窗口 就可以浏览几个连接中的对象。图2 - 1 4中的窗口演示了正在显示函数 A l m o s t F u l l的S Q L浏览窗口。 从该浏览器中,用户可以观察对象的属性,编译对象,或者将对象删除。 图2-14 SQL浏览窗口 3. 编辑数据库对象 双击S Q L浏览窗口中显示的某个对象将启动 S Q L - P r o g r a m m e r的开发窗口(S P D W)对其进 行编辑。S P D W提供查询或修改所显示对象信息的功能。图 2 - 1 5是显示函数A l m o s t F u l l的S P D W 窗口,如图所示,该窗口中还显示了变量。用户可以直接从 S P D W中执行过程,这时,该窗口将 显示对话框等待用户输入匿名块中使用的输入参数的值。 4. 显示错误信息 如果过程在编译出现错误的话, S P D W将在文本窗口下的错误窗格中显示这些错误。 C D - R O M中图C D 2 - 2 1就是演示S P D W该功能的图形窗口。双击该错误窗格中的某个错误将使与该错 第2章 PL/SQL开发和运行环境计计43下载误有关的代码段突出显示。同时, S P D W还将过程标识为非法。 图2-15 显示A l m o s t F u l l的S P D W窗口 5. 代码模板 S Q L - P r o g r a m m e r可以为创建新的对象提供模板。当用户在 S Q L浏览窗口创建新的对象时, 该浏览器将自动为创建的对象填充相应的模板。用户可以在窗口中的选择菜单中指定模板。 C D - R O M中的图C D 2 - 2 2显示了过程使用的默认模板。在该窗口中,除了过程代码外还有代码注解。 6. 脚本 S Q L - P r o g r a m m e r的脚本界面允许用户为任何数据库对象的组合创建脚本。这种脚本中包括 了可以自动地重建任何其他服务器所需的代码。因此,这种脚本提供了一种可以在数据库之间 复制单独的对象,或全部模式的机制。 C D - R O M中图C D 2 - 2 3显示了这种脚本的窗口界面。 2.2.7 PL/SQL开发工具小结 到目前为之,我们所讨论的所有开发工具都没有提供客户端的 P L / S Q L引擎。这些开发工具 都可以用于开发和调试 P L / S Q L应用。表2 - 3给出了这些工具之间主要性能的比较。有关这些开发 工具的详细信息,请看本书 C D - R O M中提供的联机帮助和访问本章表 2 - 1中列出的开发商站点。 支持版本控制的开发工具通常借助于第三方数据源控制系统实现开发功能。不同的开发工 具支持不同的数据源控制系统。有关进一步的信息,请参阅 C D - R O M中的联机帮助。 44计计第一部分 PL/SQL介绍及开发环境 下载表2-3 PL/SQL开发工具性能比较 功能 S Q L * P L U S Rapid X P E D I T E R SQL TO A D S Q L - S Q L / S Q L N a v i g a t o r P r o g r a m m e r 提供方式 随O r a c l e提供 单独出售 单独出售 单独出售 单独出售 单独出售 G U I界面环境 无 有 有 有 有 有 对象浏览器 无 有 有 有 有 有 代码模板 无 有 有 有 有 有 工程管理 无 有 有 无 无 无 代码格式化 无 有 有 有 有 有 作业调度 无 有 无 无 无 无 版本控制 无 有 有 有 有 有 支持Oracle8 类型 有 有 有 有 有 有 支持Oracle8i 类型 有 有 无 有 无 无 安装服务器端要求 无 无 有 有 无 无 支持同时多个连接 无 有 无 有 有 有 2.3 小结 我们在本章介绍了六个用于创建 P L / S Q L应用的开发工具。它们分别是 O r a c l e的SQL *PLus, Embarcadero Te c h n o l o g i e s公司的Rapid SQL,C o m p u w a r e公司的X P E D I T E R / S Q L,Qest Software 公司的TO A D和SQL Navigator以及Sylvain-Faust International的S Q L - P r o g r a m m e r。这些工具中, 除了SQL *Plus是随O r a c l e数据库一起提供的外,其他工具的试用版程序都在本书的 C D - R O M中。 我们在下一章将介绍这些开发工具提供的调试功能。 第2章 PL/SQL开发和运行环境计计45下载下载 第3章 跟踪和调试 没有几个程序能够不经过调试就可以实现其设计功能。这是因为除了程序本身不可能一次 就可以完全书写正确外,而且在开发过程中,对程序的要求也可能发生变化,需要对程序进行 重新编制。综上所述,程序需要经过彻底的测试才可以正常工作并实现预期的要求。我们将在 本章讨论几种调试和测试P L / S Q L程序的技术,其中即包括图形和非图形的 P L / S Q L调试技术。除 此之外,我们还要讨论O r a c l e 8 i提供的跟踪和配置工具。 3.1 问题分析 每个程序错误都有其特殊之处,这就使得程序的调试和测试技术面临挑战。虽然,我们在 开发过程中可以借助于测试和质量分析( Q A)来减少程序错误的数量,但是,如果你具有使用 开发工具的经验,毫无疑问,你将会在自己或其他人编制的程序中发现新的错误和问题。 3.1.1 调试指导原则 尽管每个程序错误都是不同的,并且对于每个给定错误的修改方法也有多种形式,但发现 和修改错误的过程是可以确切定义的。经过前几年作者在调试自己和其他程序员编制的程序中 所获得的经验,我总结了几条指导确定程序错误的原则。这些调试指导原则适用于所有的程序 设计语言,而不仅仅是适用于 P L / S Q L。 1. 发现错误发生的位置 发现错误发生的位置是修改代码问题的关键一步。如果一个大型的复杂程序一开始就不能 运行,诊断问题的第一步就是确认该程序出现错误的准确位置。实现错误定位的复杂程度取决 于程序代码的复杂性。发现错误发生点的最简单方法是在程序运行时进行动态跟踪,查看数据 结构的值以确定发生错误的原因。 2. 判定错误原因 一旦知道了程序中错误发生的位置,我们就需要进一步来判定发生错误的原因,是否是返 回的O r a c l e错误?或是程序中计算部分返回了错误结果?还是把错误数据插入到数据库引发的错 误?总之,为了修改错误,必须要搞清错误发生的原因。 3. 简化程序以便调试 当我们不能确认错误发生的位置时,一种有效的方法是把程序简化为简单的可测试的程序 段。其方法是先删去程序的一部分代码,然后再返回到程序运行。如果错误还存在的话,就说 明临时删除的程序部分不会导致错误。如果错误没有出现就要检查所删除的程序段中出现的错 误。 请记住程序中某段代码的错误可能会显现在程序中的另一部分。例如,某个过程可能会返 回一个不正确的值,但是该返回值可能在当前的程序中没有使用,而在其后的主程序段才会影第3章 跟踪和调试计计47下载 响程序执行。虽然问题看起来出现在主程序中,但是实际上是该过程的代码错误。如果我们将 该过程调用去掉并直接把正确的返回值赋值给返回变量将揭示问题所在。我们在本章的后几节 讨论这种特殊的情况。 4. 建立测试环境 在理想的情况下,测试和调试都不应在实用系统中进行,最好是通过尽可能多地复制实用 系统来维持一个测试环境,例如,可以使用带有少量数据的同一个应用的数据库结构来进行测 试。通过这种方法,在不影响正在运行的应用系统的前提下来开发测试应用的新版本。如果在 应用系统中出现了问题,首先应在测试中尽可能地重现该问题。其方法就是把问题缩小在一个 较小的测试案例中。测试案例可能不仅仅只包括程序代码,通常 P L / S Q L语句的执行需要使用数 据库结构和表中的数据,因此,与代码相关的部分也要复制并缩小。 3.1.2 调试程序包 P L / S Q L的主要功能是处理存储在 O r a c l e数据库中的数据。该语言的结构就是基于数据处理 的并具有优良的性能。但是,对于某些特殊的用途,我们需要一些工具来帮助我们编制和调试 程序。 在下面的几节中,我们将详细分析调试 P L / S Q L代码的不同方法。每一节都将集中讨论一种 不同的程序错误并遵循上面的调试指导原则使用不同的方法来将程序问题孤立出来解决。每一 节中将先描述通用的调试方法,然后给出要解决的问题的描述。我们将同时讨论非图形和图形 调试技术。在讨论解决非图形问题的内容中,我们将开发不同版本的调试程序包, D e b u g,该包 将可以用于程序员自己程序的调试。根据每个程序员所使用的环境和要求,本章提供的每个程 序包将具有不同的用途。 3.2 非图形调试技术 尽管市场上有多种用于P L / S Q L的图形调试器(我们将在本章的后面讨论这些调试器),但是 在很多的情况下简单的基于字符的调试器还是很有用的。基于图形用户界面的调试工具并不是 随处可用的,并且在某些复杂的 P L / S Q L运行环境下,这种基于图形用户界面的调试工具可能无 法进行安装。我们在本章要讨论的两种简单的调试技术,即插入调试表和在屏幕上打印数据是 最简单并且又不需要专门调试工具的实用技术。 3.2.1 在程序中插入调试用表 最简单的调试方法是把局部变量的值插入到程序维持的临时表中,当该程序运行结束时, 我们可以查询该临时表中的变量数据值。这种调试方法实现起来最容易并且不需特定的运行环 境,其原因是我们只需在程序中插入 I N S T E RT语句。 1. 问题1 假设我们要编制一个根据当前已注册学生计算并返回每个班级平均分数值的函数。该函数 的代码如下: 节选自在线代码AverageGradel.sqlCREATE OR REPLACE FUNCTION AverageGrade ( /* Determines the average grade for the class specified. Grades are stored in the registered_students table as single characters A through E. This function will return the average grade, again, as a single letter. If there are no students registered for the class, an error is raised. */ p_Department IN VARCHAR2, p_Course IN NUMBER) RETURN VARCHAR2 AS v_AverageGrade VARCHAR2(1); v_NumericGrade NUMBER; v_NumberStudents NUMBER; CURSOR c_Grades IS SELECT grade FROM registered_students WHERE department = p_Department AND course = p_Course; BEGIN /* First we need to see how many students there are for this class. If there aren't any, we need to raise an error. */ SELECT COUNT(*) INTO v_NumberStudents FROM registered_students WHERE department = p_Department AND course = p_Course; IF v_NumberStudents = 0 THEN RAISE_APPLICATION_ERROR(-20001, 'No students registered for ' || p_Department || ' ' || p_Course); END IF; /* Since grades are stored as letters, we can't use the AVG function directly on them. Instead, we can use the DECODE function to convert the letter grades to numeric values, and take the average of those. */ SELECT AVG(DECODE(grade, 'A', 5, 'B', 4, 'C', 3, 'D', 2, 'E', 1)) INTO v_NumericGrade FROM registered_students WHERE department = p_Department AND course = p_Course; /* v_NumericGrade now contains the average grade, as a number from 1 to 5. We need to convert this back into a letter. The DECODE function can be used here as well. Note that we are selecting the result into v_AverageGrade rather than assigning to it, 48计计第一部分 PL/SQL介绍及开发环境 下载because the DECODE function is only legal in a SQL statement. */ SELECT DECODE(ROUND(v_NumericGrade),5, 'A', 4, 'B', 3, 'C', 2, 'D', 1, 'E') INTO v_AverageGrade FROM dual; RETURN v_AverageGrade; END AverageGrade; 假设表r e g i s t e r e d _ s t u d e n t s的内容如下: SQL> select * from registered_students; STUDENT_ID DEP COURSE G ---------- --- --------- 10000CS 102 A 10002 CS 102 B 10003 CS 102 C 10000 HIS 101 A 10001 HIS 101 B 10002 HIS 101 B 10003 HIS 101 A 10004 HIS 101 C 10005 HIS 101 C 10006 HIS 101 E 10007 HIS 101 B 10008 HIS 101 A 10009 HIS 101 D 10010 HIS 101 A 10008 NUT 307 A 10010 NUT 307 A 10009 MUS 410 B 10006 MUS 410 E 10011 MUS 410 B 10000 MUS 410 B 20 rows selected. 注意 表r e g i s t e r e d _ s t u d e n t s由联机代码r e l Ta b l e s . s q l脚本中上面的2 0行数据填充。有关 该表的结构情况,请看第1章的内容。 该表中已有四个班级学生进行了注册。它们分别是计算机科学 1 0 2、历史 1 0 1、营养 3 0 7和 音乐 4 1 0。现在我们可以用这四个班级来调用函数 Av e r a g e G r a d e。如果我们使用了其他的班级, 该函数将会引发“无学生注册”的错误。下面是开发工具 SQL *Plus的输出样本: 节选自在线代码callAG.sql SQL> VARIABLE v_AveGrade VARCHAR2(1) SQL> exec :v_AveGrade := AverageGrade('HIS', 101) PL/SQL procedure successfully completed. 第3章 跟踪和调试计计49下载50计计第一部分 PL/SQL介绍及开发环境 下载 SQL> print v_AveGrade V_AVEGRADE -------------------------------- B SQL> exec :v_AveGrade := AverageGrade('NUT', 307) PL/SQL procedure successfully completed. SQL> print v_AveGrade V_AVEGRADE -------------------------------- A SQL> exec :v_AveGrade := AverageGrade('MUS', 410) PL/SQL procedure successfully completed. SQL> print v_AveGrade V_AVEGRADE -------------------------------- C SQL> exec :v_AveGrade := AverageGrade('CS', 102) begin :v_AveGrade := AverageGrade('CS', 102); end; * ERROR at line 1: ORA-20001: No students registered for CS 102 ORA-06512: at "EXAMPLE.AVERAGEGRADE", line 29 对该函数的最后一次调用产生了错误,返回了错误信息 O R A - 2 0 0 1,尽管我们会发现实际上 计算机科学已经有学生注册。 2. 问题1的调试程序包 我们用来发现上述错误的调试程序如下所示。该程序中的过程 D e b u g . D e b u g是该程序包的主 过程,它带有两个参数,一个描述和一个变量。这两个参数是连接在一起的并被插入到表 d e b u g _ t a b l e中存储。过程D e b u g . R e s e t要在程序的开始就调用执行,以便初始化表 d e b u g _ t a b l e和 内部行记数器(该行记数器过程也被包的初始化代码调用)。行记数器的作用是保证表 d e b u g _ t a b l e中的行可以按它们插入时的顺序进行选择。 节选自在线代码Debugl.sql CREATE OR REPLACE PACKAGE Debug AS /* First version of the debug package. This package works by inserting into the debug_table table. In order to see the output, select from debug_table in SQL*Plus with: SELECT debug_str FROM debug_table ORDER BY linecount; */ /* This is the main debug procedure. p_Description will be concatenated with p_Value, and inserted into debug_table. */PROCEDURE Debug(p_Description IN VARCHAR2, p_Value IN VARCHAR2); /* Resets the Debug environment. Reset is called when the package is instantiated for the first time, and should be called to delete the contents of debug_table for a new session. */ PROCEDURE Reset; END Debug; CREATE OR REPLACE PACKAGE BODY Debug AS /* v_LineCount is used to order the rows in debug_table. */ v_LineCount NUMBER; PROCEDURE Debug(p_Description IN VARCHAR2, p_Value IN VARCHAR2) IS BEGIN INSERT INTO debug_table (linecount, debug_str) VALUES (v_LineCount, p_Description || ': ' || p_Value); COMMIT; v_LineCount := v_LineCount + 1; END Debug; PROCEDURE Reset IS BEGIN v_LineCount := 1; DELETE FROM debug_table; END Reset; BEGIN /* Package initialization code */ Reset; END Debug; 注意 表d e b u g _ t a b l e是用脚本r e l Ta b l e s . s q p中的下列语句创建的: CREATE TABLE debug_table ( linecount NUMBER PRIMARY KEY, debug_str VARCHAR2(200)); 3. 使用问题1的调试程序包 为了发现程序Av e r a g e G r a d e中的错误,我们需要观察该过程使用变量的值的情况。我们可以 在该程序中加入调试语句来实现。为了使用上面的调试程序包,我们需要在程序 Av e r a g e G r a d e 的开始处调用过程 D e b u g . R e s e t,并在凡是我们要查看变量的地方调用过程 D e b u g . D e b u g。下面 是已经加入调试语句的 Av e r a g e G r a d e程序。为了便于查看程序,我们删除了该程序的某些注释 行。 节选自在线代码AverageGrade2.sql CREATE OR REPLACE FUNCTION AverageGrade ( p_Department IN VARCHAR2, p_Course IN NUMBER) RETURN VARCHAR2 AS 第3章 跟踪和调试计计51下载v_AverageGrade VARCHAR2(1); v_NumericGrade NUMBER; v_NumberStudents NUMBER; CURSOR c_Grades IS SELECT grade FROM registered_students WHERE department = p_Department AND course = p_Course; BEGIN Debug.Reset; Debug.Debug('p_Department', p_Department); Debug.Debug('p_Course', p_Course); /* First we need to see how many students there are for this class. If there aren't any, we need to raise an error. */ SELECT COUNT(*) INTO v_NumberStudents FROM registered_students WHERE department = p_Department AND course = p_Course; Debug.Debug('After select, v_NumberStudents', v_NumberStudents); IF v_NumberStudents = 0 THEN RAISE_APPLICATION_ERROR(-20001, 'No students registered for ' || p_Department || ' ' || p_Course); END IF; SELECT AVG(DECODE(grade, 'A', 5, 'B', 4, 'C', 3, 'D', 2, 'E', 1)) INTO v_NumericGrade FROM registered_students WHERE department = p_Department AND course = p_Course; SELECT DECODE(ROUND(v_NumericGrade), 5, 'A', 4, 'B', 3, 'C', 2, 'D', 1, 'E') INTO v_AverageGrade FROM dual; 52计计第一部分 PL/SQL介绍及开发环境 下载RETURN v_AverageGrade; END AverageGrade; 现在我们可以再次调用程序 Av e r a g e G r a d e并从表d e b u g _ t a b l e中选择下列的结果来观察: SQL> EXEC :v_AveGrade := AverageGrade('CS', 102) begin :v_AveGrade := AverageGrade('CS', 102); end; * ERROR at line 1: ORA-20001: No students registered for CS 102 ORA-06512: at "EXAMPLE.AVERAGEGRADE", line 25 ORA-06512: at line 1 SQL> SELECT debug_str FROM debug_table ORDER BY linecount; DEBUG_STR ---------------------------------------------------------- p_Department: CS p_Course: 102 After select, v_NumberStudents: 0 从上面的代码中,我们可以看到变量 v _ N u m b e r S t u d e n t s的值是0 ,这就解释了为什么程序提示 出现O R A - 2 0 0 0 1错误的原因。这样一来,我们就可以把错误的范围缩小到 S E L E C T语句来检查, 因为S E L E C T语句在没有与表中的任何行相匹配时会返回 0。现在我们可以进一步来详细检查下 面所示的S E L E C T语句中的W H E R E子句是否有问题: SELECT COUNT(*) INTO v_NumberStudents FROM registered_students WHERE department = p_Department AND course = p_Course; 根据调试程序的输出结果来看,变量p _ D e p a r t m e n t和p _ C o u r s e的值似乎没有问题。但实际上, 在这些变量的字符串的最后可能存在着空白字符,因此使看到的字符串与实际的字符串不符。 现在让我们来调用D e b u g . D e b u g把变量p _ D e p a r t m e n t和p _ C o u r s e用引号扩号起来。这样一来我们 就可以发现是否在这些变量的前面或后面存在着空格。 节选自在线代码AverageGrade3.sql CREATE OR REPLACE FUNCTION AverageGrade ... BEGIN Debug.Reset; Debug.Debug('p_Department', '''' || p_Department || ''''); Debug.Debug('p_Course', '''' || p_Course || ''''); /* First we need to see how many students there are for this class. If there aren't any, we need to raise an error. */ SELECT COUNT(*) INTO v_NumberStudents FROM registered_students WHERE department = p_Department 第3章 跟踪和调试计计53下载54计计第一部分 PL/SQL介绍及开发环境 下载 AND course = p_Course; Debug.Debug('After select, v_NumberStudents', v_NumberStudents); ... 现在当我们再次运行Av e r a g e G r a d e并查询表d e b u g _ t a b l e时,我们就可以得到下面的结果; SQL> EXEC :v_AveGrade := AverageGrade('CS', 102) begin :v_AveGrade := AverageGrade('CS', 102); end; * ERROR at line 1: ORA-20001: No students registered for CS 102 ORA-06512: at "EXAMPLE.AVERAGEGRADE", line 25 ORA-06512: at line 1 SQL> SELECT debug_str FROM debug_table ORDER BY linecount; DEBUG_STR ----------------------------------------------------------- p_Department: 'CS' p_Course: '102' After select, v_NumberStudents: 0 我们可以看到,变量 p _ N u m b e r S t u d e n t s的尾部没有多余的空格。这就是问题所在。表 r e g i s t e r e d _ s t u d e n t s的列d e p a r t m e n t定义为C H A R(3),但变量p _ D e p a r m e n t是VA R C H A R 2。这就 意味着带有‘ C S’的数据库列后面有一个空格,从而也说明了为什么 S E L E C T语句没有发现匹 配的数据库行,因此给变量 p _ N u m b e r S t u d e n t s赋值为0的原因。 提示 内置函数D U M P可以用来检查数据库列的准确内容。例如,我们可以用下面的命 令在表r e g i s t e r e d _ s t u d e n t s中确认列d e p a r t m e n t的内容。 SQL> SELECT DISTINCT DUMP(department) 2 FROM registered_students 3 WHERE department = 'CS'; DUMP(DEPARTMENT) -------------------------------------- Typ=96 Len=3: 67,83,32 如上所示,该列的类型是 9 6,代表C H A R类型。该列的最后一个字符是 3 2 ,它就是 A S C I I码的空格(实际的二进制数值取决于数据库系统采用的字符集),这就说明该列 是用空格填补的。有关数据类型代码请参阅 Oracle SQL 参考资料。 修改该错误的一种方法是把变量 p _ D e p a r t m e n t的类型改变为如下所示的C H A R: CREATE OR REPLACE FUNCTION AverageGrade ( p_Department IN CHAR, p_Course IN NUMBER) RETURN VARCHAR2 AS ... BEGIN ... END AverageGrade;在做了上述修改后,程序Av e r a g e G r a d e就可以正确运行了: SQL> EXEC :v_AveGrade := AverageGrade('CS', 102) PL/SQL procedure successfully completed. SQL> print v_AveGrade V_AVEGRADE -------------------------------- B 上述的修改能够起作用的原因就是在 W H E R E子句中的两个值都是 C H A R类型并且使用了空 格填充比较语法。 提示 如果我们在函数中使用了属性 % T Y P E的话,变量p _ D e p a r t m e n t的类型就是正确 的类型了,这就是我推荐使用 % T Y P E的原因。同样,由于程序 Av e r a g e G r a d e的返回值 是长度为 1的字符串,并且永远为 1 ,所以我们可以将 R E T U R N子句定义为定长类型的 C H A R。现在,程序Av e r a g e G r a d e的声明部分如下所示: 节选自在线代码AverageGrade4.sql CREATE OR REPLACE FUNCTION AverageGrade ( p_Department IN registered_students.department%TYPE, p_Course IN registered_students.course%TYPE) RETURN CHAR AS v_AverageGrade CHAR(1); v_NumericGrade NUMBER; v_NumberStudents NUMBER; ... BEGIN ... END AverageGrade; 4. 问题1解决方案的评价 该版本的D e b u g非常简单。虽然它的功能只是插入一个调试表 d e b u g _ t a b l e,但是我们可以借 助于该程序发现程序Av e r a g e G r a d e中的错误。这种调试技术的主要优点如下: • 由于该程序只使用 S Q L,所以它可以在任何环境下使用。显示输出的 S E L E C T语句可以在 SQL *Plus或在其他的开发工具中运行。 • 由于调试程序Debug 代码很少,所以它不会对正在调试的程序增加太多的开销。 但是这种调试技术也有下面的缺点: • 程序Av e r a g e G r a d e在处理过程中引发了异常,这就使该程序中已执行的 S Q L语句返回到开 始状态。因此,就要在程序 D e b u g . D e b u g中使用命令C O M M I T来保证插入调试表的操作不 会返回到起点。这时,如果该过程中的其他处理不需要执行提交的话,该提交命令的执行 将回引起错误。同时,该提交命令也将会使所有打开的 SELECT FOR UPDAT E游标无效。 (在O r a c l e 8 i中,D e b u g . D e b u g可以编制为自动事务,从而避免上述问题。有关内容请看第 第3章 跟踪和调试计计55下载56计计第一部分 PL/SQL介绍及开发环境 下载 11章。) • 按照现在的编制方法,如果程序中同时有一个以上的会话时,程序 D e b u g将不能正常工作。 S E L E C T语句将从多个会话中返回结果。该问题可以通过在 D e b u g包和表debug_table 中加 入唯一标识会话的数据列来解决。 我们将在下面的一节中通过使用 D B M S _ O U T P U T包来解决D e b u g程序存在的问题。 3.2.2 将结果打印到屏幕 我们在上一节看到的 D e b u g程序的第一个版本初步实现了有限的 I / O操作(对数据库的I / O操 作,而不是输出到屏幕 )。P L / S Q L没有提供内置的输入输出功能,这是因为 P L / S Q L是一种专门对 存储在数据库中的数据进行操作的专用语言 ,它不需要具有打印变量和数据结构的功能。然而 ,输 入输出的确是非常有用的调试工具,所以从 P L / S Q L 2 . 0版开始,通过内置包D B M S _ O U T P U T,扩充 了输出功能。我们将在本节使用 D B M S _ O U T P U T来实现D e b u g程序的第二个版本。 提示 P L / S Q L直到现在仍然没有内置的输入功能 ,但SQL *Plus中的替换变量(我们在第2 章使用过)可以用来解决该问题。 P L / S Q L 2 . 3及以上的版本提供的包 U T L _ F I L E可以用来 读写系统文件。本书的第 7章将介绍包U T L _ F I L E和可以把输出写入到文件中的 D e b u g 程序。 1. DBMS_OUTPUT包 在我们开始讨论本节的调试程序之前,我们需要对包 D B M S _ O U T P U T做一些分析介绍。就 象P L / S Q L的其他包一样,D B M S _ O U T P U T是属于Oracle user SYS的。创建D B M S _ O U T P U T的 脚本授权该包的 E X E C U T E命令具有 P U L B I C的属性并为其创建了公用命令。这就是说任何 O r a c l e用户都可以不在该包名的前面冠以前缀 SYS 就直接调用D B M S _ O U T P U T中的子程序。 D B M S _ O U T P U T是如何工作的呢?该包的两个基本操作, G E T和P U T是借助于该包中的过 程实现的。P U T操作带有参数并将其放入内部缓冲区存储。 G E T操作执行从该缓冲区的读操作 并把读入的内容作为参数返回给该过程。 D B M S _ O U T P U T还提供一个叫做E N A B L E的过程用来 设置缓冲区的容量。 该包中的P U T例程有P U T、P U T _ L I N E和N E W _ L I N E。G E T例程有G E T _ L I N E和G E T _ L I N E S。 而程序E N A B L E和D I S A B L E则用于控制缓冲区。 例程P U T和P U - L I N E P U T和P U T _ L I N E的调用语法如下: PROCEDURE PUT( a VARCHAR2); PROCEDURE PUT( a NUMBER); PROCEDURE PUT( a DATE); PROCEDURE PUT_LINE( a VARCHAR2); PROCEDURE PUT_LINE( a NUMBER); PROCEDURE PUT_LINE( a DATE); 命令中的字母a是存放在缓冲区中的参数。请注意上面的过程是由参数的类型重载的(我们 在第 4章讨论重载)。由于有三种不同版本的 P U T和P U T _ L I N E例程 ,缓冲区可以存储类型为第3章 跟踪和调试计计57下载 VA R C H A R 2,N U M B E R和D AT E的值。这些类型的数据都以其原始格式存储。 缓冲区是按行使用的,每一行最多可以存储 2 5 5个字节。PUT_LINE 把一个换行符追加到其 参数的尾部。P U T命令则没有这种功能。 P U T _ L I N E命令的操作等价于在执行 P U T命令后再执行 N E W _ L I N E操作。 例程N E W _ L I N E 调用N E W _ L I N E的语法是: PROCEDURE NEW_LINE; N E W _ L I N E把换行字符放入缓冲区中,标识一行的结束。缓冲区对其中的字符行数没有限 制。缓冲区的大小是在E N A B L E命令中的说明的。 例程G E T _ L I N E 调用G E T _ L I N E的语法是: PROCEDURE GET_LINE(line OUT VARCHAR2,status OUT INTEGER); 其中l i n e是缓冲区内一行的字符串, s t a t u s指示l i n e是否检索成功。一行的最大长度是 2 5 5个 字符。如果检索到指定行的话,则 s t a t u s为0;如果缓冲区为空的话,则 s t a t u s为1。 注意 尽管缓冲区行的最大长度是 2 5 5个字符,但是输出变量行可以大于 2 5 5个字符。例 如,缓冲区行可以由日期量组成,日期值占用 7个存储字节,但通常我们把日期量转换 为长度大于7个字符的字符串存储。 例程G E T _ L I N E S 该过程有一个按表索引的参数。其表类型和该过程的语法如下: TYPE CHARARR IS TABLE OF VARCHAR2(255) INDEX BY BINARY_INTEGER; PROCEDURE GET_LINES( lines OUT CHARARR, numlines IN OUT INTEGER); 其中参数 l i n e是包括来自缓冲区的多行表索引, n u m l i n e s指出所需的行数。当指定 G E T _ L I N E S为输入时,n u m l i n e s说明所需的行数。当为输出方式时, n u m l i n e s为实际返回的行 数,该行数要小于或等于指定的行数。 G E T _ L I N E S相当于执行多次对G E T _ L I N E的调用。 D B M S _ O U I P U T 中还定义了类型 C H A R A R R 。如果要在自己的代码中显示地调用 G E T _ L I N E S时,就必须声名一个类型为 D B M S _ O U I P U T.C H A R A R R的变量。例如: 节选自在线代码DBMSOutput.sql DECLARE /* Demonstrates using PUT_LINE and GET_LINE. */ v_Data DBMS_OUTPUT.CHARARR; v_NumLines NUMBER; BEGIN -- Enable the buffer first. DBMS_OUTPUT.ENABLE(1000000); -- Put some data in the buffer first, so GET_LINES will -- retrieve something. DBMS_OUTPUT.PUT_LINE('Line One'); DBMS_OUTPUT.PUT_LINE('Line Two'); DBMS_OUTPUT.PUT_LINE('Line Three');58计计第一部分 PL/SQL介绍及开发环境 下载 -- Set the maximum number of lines that we want to retrieve. v_NumLines := 3; /* Get the contents of the buffer back. Note that v_Data is declared of type DBMS_OUTPUT.CHARARR, so that it matches the declaration of DBMS_OUTPUT.GET_LINES. */ DBMS_OUTPUT.GET_LINES(v_Data, v_NumLines); /* Loop through the returned buffer, and insert the contents into temp_table. */ FOR v_Counter IN 1..v_NumLines LOOP INSERT INTO temp_table (char_col) VALUES (v_Data(v_Counter)); END LOOP; END; 注意 G E T _ L I N E和G E T _ L I N E S都只从缓冲区中进行检索并返回字符串。当执行 G E T操 作时,缓冲区的内容将根据默认的数据类型转换规则被转换为字符串。如果要为转换说 明格式的话,使用显式地的 TO _ C H A R来调用P U T,而不是调用G E T。 过程E N A B L E和D I S A B L E E N A B L E和D I S A B L E的调用语法如下: PROCEDURE ENABLE(buffer_size IN INTEGER DEFAULT 20000); PROCEDURE DISABLE; 其中b u ff e r _ s i z e是内部缓冲区初始字节数,默认的缓冲区是 20 000个字节,该缓冲区的最大 容量是1 000 000个字节。执行该过程后,P U T和P U T _ L I N E的参数将被放入到该缓冲区中,这些 参数是以内部格式存储的,其所占用的空间由这些参数的结构决定。如果调用 D I S A B L E的话, 缓冲区的内容将被清除,对 P U T和P U T _ L I N E的后续调用将无效。 2. 使用D B M S _ O U T P U T 包D B M S _ O U T P U T自身并不具有任何打印机制,该包只是简单地实现了先入先出( F I F O)的 数据结构。但这样如何实现打印呢? SQL *Plus和服务器管理器都提供了叫做 S E RV E R O U T P U T 的选择项。另外,许多第三方提供的开发工具(包括图形开发和调试工具)提供了显示 D B M S _ O U T P U T数据的功能。如果指定 S E RV E R O U T P U T选项,SQL *Plus将在P L / S Q L块结束 时自动调用D B M S _ O U T P U T. G E T _ L I N E S并把结果打印到屏幕。图3 - 1是打印窗口。 SQL *Plus的命令SET SERVEROUTPUT ON隐式地调用D B M S _ O U T P U T. E N A B L E来建立内 部缓冲区。可以用下面的命令指定缓冲区的容量: SET SERVEROUTPUT ON SIZE buffer_size 其中b u ff e r _ s i z e用来作为缓冲区的初始长度(即 D B M S _ O U T P U T. E N A B L E的参数)。在设置 S E RVEROUTPUT ON时,SQL *Plus将在P L / S Q L块结束运行后调用D B M S _ O U T P U T. GET_LINES。 这就意味着输出将在块结束时和该块不运行时送往屏幕显示。通常这样在 D B M S _ O U T P U T用于调 试时,该功能不会带来其他问题。警告 D B M S _ O U T P U T是专门用于调试的,它不适于用来生成报表。如果程序员需要 为查询指定输出格式的话,我们推荐使用专用工具 Oracle Reports,尽量不要用 D B M S _ D U T P U T和S Q L * P l u s。 图3-1 使用S E RV E R O U T P U T和P U T _ L I N E选项时的输出窗口 内部缓冲区受到其最大容量限制(由 D B M S _ O U T P U T. E N A B L E说明),并且每一行的最大 长度为 2 5 5个字节。因此,调用 D B M S _ O U T P U T. P U T,D B M S _ O U T P U T. P U T _ L I N E和 D B M S _ O U T P U T. N E W _ L I N E时可能会引发如下两种异常: ORA-20000: ORU-10027: buffer overflow, limit of bytes. 或 ORA-20000: ORU-10028: line length overflow, limit of 255 bytes per line. 错误信息的内容取决于所超出的限制类型。 提示 养成应用SET SERVEROUTPUT ON命令来说明缓冲区长度的习惯是非常好的。尽 管DBMS_OUTPUT.ENABLE的默认值是20 000个字节,但是如果在SET SERVEROUTPUT O N中没有显式地说明缓冲区长度的话,SQL *Plus调用D B M S _ O U T P U T. E N A B L E时用的 缓冲区长度将是2 000个字符。 3. 问题2 第3章 跟踪和调试计计59下载表s t u d e n t s中有一个记录已注册学生的学分的列,表 r e g i s t e r e d _ s t u d e n t则包含学生所在班级 的信息。如果学生变更注册的话(因此表 r e g i s t e r e d _ s t u d e n t中的信息要变更),我们也要更新表 s t u d e n t s中的列c u r r e n t _ c r e d i t s。我们可以编制一个叫做 C o u n t C r e d i t s的函数来实现更新,该函数 将计算注册学生的总学分。该函数如下: 节选自在线代码CountCreditsl.sql CREATE OR REPLACE FUNCTION CountCredits ( /* Returns the number of credits for which the student identified by p_StudentID is currently registered */ p_StudentID IN students.ID%TYPE) RETURN NUMBER AS v_TotalCredits NUMBER; -- Total number of credits v_CourseCredits NUMBER; -- Credits for one course CURSOR c_RegisteredCourses IS SELECT department, course FROM registered_students WHERE student_id = p_StudentID; BEGIN FOR v_CourseRec IN c_RegisteredCourses LOOP -- Determine the credits for this class. SELECT num_credits INTO v_CourseCredits FROM classes WHERE department = v_CourseRec.department AND course = v_CourseRec.course; -- Add it to the total so far. v_TotalCredits := v_TotalCredits + v_CourseCredits; END LOOP; RETURN v_TotalCredits; END CountCredits; 由于函数 C o u n t C r e d i t s不修改数据库和包的状态,所以我们可以从 S Q L语句中直接调用它 (适用P L / S Q L 2 . 1及更高版本)。(S Q L语句调用功能将在第5章详细讨论)这样,我们就可以通过 表s t u d e n t s的选择来得到所有学生的总学分。其命令如下: SQL> SELECT ID, CountCredits(ID) "Total Credits" 2 FROM students; ID Total Credits --------- ------------- 10000 10001 10002 10003 10004 10005 10006 10007 60计计第一部分 PL/SQL介绍及开发环境 下载10008 10009 10010 10011 12 rows selected. 从上面的输出可以看出函数 Count Credits没有输出结果,这就说明该函数的返回值为空,没 有得到正确的处理结果。 4. 问题2的调试程序包 现在我们使用包D B M S _ O U T P U T来定位函数C o u n t C r e d i t s的错误。下面的程序段是一个解决 该问题的调试程序,该程序具有与上一节使用的第一版调试程序相同的接口,因此我们只需要 修改该包的包体就可以了。 节选自在线代码Debug2.sql CREATE OR REPLACE PACKAGE BODY Debug AS PROCEDURE Debug(p_Description IN VARCHAR2, p_Value IN VARCHAR2) IS BEGIN DBMS_OUTPUT.PUT_LINE(p_Description || ': ' || p_Value); END Debug; PROCEDURE Reset IS BEGIN /* Disable the buffer first, then enable it with the * maximum size. Since DISABLE purges the buffer, this * ensures that we will have a fresh buffer whenever * Reset is called. */ DBMS_OUTPUT.DISABLE; DBMS_OUTPUT.ENABLE(1000000); END Reset; BEGIN /* Package initialization code */ Reset; END Debug; 在该程序中,我们不在使用表 d e b u g _ t a b l e;取而代之的是包 D B M S _ O U T P U T。这样一来, 该版本的D e b u g将只能工作在能够自动调用 D B M S _ O U T P U T.G E T _ L I N E S和打印缓冲区内容(如 SQL *Plus)的工具中。除此之外,在使用 D e b u g之前,S E RV E R O U T P U T也要设置为O N。 5. 使用问题2的调试程序包 函数C o u n t C r e d i t s现在返回的是空的结果。现在,先让我们来验证上述的现象并看一下在循 环中把什么数值赋予变量v _ To t a l C r e d i t s。我们在下面的程序中加入对 D e b u g的调用: 节选自在线代码CountCredits2.sql CREATE OR REPLACE FUNCTION CountCredits ( /* Returns the number of credits for which the student identified by p_StudentID is currently registered */ p_StudentID IN students.ID%TYPE) RETURN NUMBER AS 第3章 跟踪和调试计计61下载v_TotalCredits NUMBER; -- Total number of credits v_CourseCredits NUMBER; -- Credits for one course CURSOR c_RegisteredCourses IS SELECT department, course FROM registered_students WHERE student_id = p_StudentID; BEGIN Debug.Reset; FOR v_CourseRec IN c_RegisteredCourses LOOP -- Determine the credits for this class. SELECT num_credits INTO v_CourseCredits FROM classes WHERE department = v_CourseRec.department AND course = v_CourseRec.course; Debug.Debug('Inside loop, v_CourseCredits', v_CourseCredits); -- Add it to the total so far. v_TotalCredits := v_TotalCredits + v_CourseCredits; END LOOP; Debug.Debug('After loop, returning', v_TotalCredits); RETURN v_TotalCredits; END CountCredits; 现在该函数的输出结果是: SQL> VARIABLE v_Total NUMBER SQL> SET SERVEROUTPUT ON SQL> exec :v_Total := CountCredits(10006); Inside loop, v_CourseCredits: 4 Inside loop, v_CourseCredits: 3 After loop, returning: PL/SQL procedure successfully completed. SQL> print v_Total V_TOTAL --------- SQL> 注意 我们是用SQL *Plus的连接变量来代替从表s t u d e n t s中选择该函数的值来测试函数 C o u n t C r e d i t s的。这样做的原因是函数C o u n t C r e d i t s现在调用了非纯函数D B M S _ O U T P U T 的缘故。如果我们在S Q L语句内部使用函数C o u n t C r e d i t s,我们将得到错误信息“ O R A - 6 5 7 1 :函数可能会更新数据库”(O r a c l e 8 i中取消了这条限制)。请看本书第5章的有关该错 误和从S Q L语句中调用存储函数的介绍。 基于该调试程序的输出看起来为每一个班级计算的分数是正确的。程序中的循环执行了两 次,每次分别返回学分 4和3。但实际上,该程序没有把学分加到总分中。现在让我们来增加几 个调试语句: 62计计第一部分 PL/SQL介绍及开发环境 下载节选自在线代码CountCredits3.sql CREATE OR REPLACE FUNCTION CountCredits ( /* Returns the number of credits for which the student identified by p_StudentID is currently registered */ p_StudentID IN students.ID%TYPE) RETURN NUMBER AS v_TotalCredits NUMBER; -- Total number of credits v_CourseCredits NUMBER; -- Credits for one course CURSOR c_RegisteredCourses IS SELECT department, course FROM registered_students WHERE student_id = p_StudentID; BEGIN Debug.Reset; Debug. Debug('Before loop, v_Totalcredits', v_TotalCredits); FOR v_CourseRec IN c_RegisteredCourses LOOP -- Determine the credits for this class. SELECT num_credits INTO v_CourseCredits FROM classes WHERE department = v_CourseRec.department AND course = v_CourseRec.course; Debug.Debug('Inside loop, v_CourseCredits', v_CourseCredits); -- Add it to the total so far. v_TotalCredits := v_TotalCredits + v_CourseCredits; Debug.Debug('Inside loop, V_TotalCredits', v_TotalCredits); END LOOP; Debug.Debug('After loop, returning', v_TotalCredits); RETURN v_TotalCredits; END CountCredits; 新修改的函数C o u n t C r e d i t s的输出如下所示: SQL> EXEC :v_Total := CountCredits(10006); Before loop, v_TotalCredits: Inside loop, v_CourseCredits: 4 Inside loop, v_TotalCredits: Inside loop, v_CourseCredits: 3 Inside loop, v_TotalCredits: After loop, returning: PL/SQL procedure successfully completed. 我们可以从该输出中发现程序的错误。我们可以发现变量 v _ To t a l C r e d i t s的值在循环开始前 为空,在循环中也一直为空,这是因为我们在该变量的声名中没有对其进行初始化。该问题可 以用下面的最终版本来解决,我们在该版本中删除了调试语句。 节选自在线代码CountCredits4.sql CREATE OR REPLACE FUNCTION CountCredits ( /* Returns the number of credits for which the student 第3章 跟踪和调试计计63下载64计计第一部分 PL/SQL介绍及开发环境 下载 identified by p_StudentID is currently registered */ p_StudentID IN students.ID%TYPE) RETURN NUMBER AS v_TotalCredits NUMBER := 0; -- Total number of credits v_CourseCredits NUMBER; -- Credits for one course CURSOR c_RegisteredCourses IS SELECT department, course FROM registered_students WHERE student_id = p_StudentID; BEGIN FOR v_CourseRec IN c_RegisteredCourses LOOP -- Determine the credits for this class. SELECT num_credits INTO v_CourseCredits FROM classes WHERE department = v_CourseRec.department AND course = v_CourseRec.course; -- Add it to the total so far. v_TotalCredits := v_TotalCredits + v_CourseCredits; END LOOP; RETURN v_TotalCredits; END CountCredits; 该版本的输出如下: SQL> EXEC: V_Total:=countCredits(10006); PL/SQL procedure successfully completed. SQL> print v_Total V_TOTAL --------- 7 SQL> SELECT ID, CountCredits(ID) "Total Credits" 2 FROM students; ID Total Credits --------- ------------- 10000 11 10001 4 10002 8 10003 8 10004 4 10005 4 10006 7 10007 4 10008 8 10009 7 10010 8第3章 跟踪和调试计计65下载 10011 3 12 rows selected. 我们可以看到函数 C o u n t C r e d i t s在计算单个学生和全表学生的分数时都可以正常工作了。如 果某个变量在其声明时没有进行初始化,该变量就被赋予空值 N U L L。根据计算N U L L表达式的 规则,该空值将在加法操作中保持为空。 6. 问题2解决方案的评价 该D e b u g版本的功能与上节的第一个版本有所不同。也就是说,我们取消了该程序对表 d e b u g _ t a b l e的依赖。这样做的优点如下: • 由于每个会话都有其自己的 D B M S _ O U T P U T内部缓冲区,从而解决了多数据库会话之间 的相互干扰问题。 • 我们不需要在程序D e b u g . D e b u g中再发送提交命令C O M M I T。 • 只要S E RV E R O U T P U T处于O N的状态,就不需要额外的 S E L E C T语句来查看输出。同样, 我们可以将S E RV E R O U T P U T设置为O F F状态来关闭调试状态。如果 S E RV E R O U T P U T为 O F F的话,调试信息仍然将被写入 D B M S _ O U T P U T缓冲区中,但不再输出到屏幕。 从另一方面来说,该版本的调试程序还要注意下列问题: • 如果我们不使用象 SQL *Plus或服务器管理器一类的工具,则调试输出将不能自动地发送 到屏幕显示。该程序包仍然可以从其他的 P L / S Q L运行环境下使用(如 Pro *C 或O r a c l e F o r m s ),但你必须显式地调用 D B M S _ O U T P U T .G E T _ L I N E 或D B M S _ O U T P U T. G E T _ L I N E S,自己实现显示输出结果。 • 调试输出的数量受到了 D B M S _ O U T P U T缓冲区容量的限制。该问题会影响每行的长度和 缓冲区的总长度。如果发现输出的内容太多并且缓冲区不能满足需要,这时使用上节第一 个版本的D e b u g可能会更好一些。 3.3 PL/SQL调试器 目前有几种集成了调试器的 P L / S Q L开发工具可以使用。带有调试器的开发工具是非常有用 的,这是因为这种工具在程序处于运行的状态下提供了逐行查看 P L / S Q L源码以及分析各个变量 的数值的功能。除此之外,我们还可以在不同的点设置程序断点并观察特殊变量的值。 3.3.1 PL/SQL调试器功能概述 图形调试工具与非图形调试工具相比有下述几个优点: • 不需要我们在应用程序中增加任何调试代码;我们只要在受控的调试环境下运行该应用程 序就可以实现调试。 • 由于应用程序的代码没有任何变动,也不需要重新编译就可以运行并观察不同的变量,所 以调试过程非常方便。 • 所有的工具都提供了集成的开发环境,具有浏览和编辑功能。 然而,在某些情况下,图形调试工具环境无法满足调试要求。例如,并非所有的操作系统 平台都支持G U I(图形用户界面)工具。在这些情况下,非图形调试技术,或者我们在本章后面 要讨论的跟踪和配置工具可能会满足特殊的要求。在下面的几节中,我们将分析在第 2章中提到的G U I工具的调试功能,这些工具都存储在本 书的C D - R O M中。每种工具的调试功能在表 3 - 1中给予了总结。有关这些工具的详细信息,请看 本书C D - R O M中的联机文档。 表3-1 PL/SQL调试器功能比较 功能 Rapid SQL X P E D I T E R / S Q L SQL Navigator TO A D SQL Programmer 动画 否 是 否 否 否 观察点 是 是 是 是 是 自动显示当前变量 是 是 是 否 否 异常结束 否 是 否 是 是 动态修改变量值 是,只有观察点 是 是 否 是 服务器端必须安装调 否 是 是 否 否 试器 调试匿名块 否 是 否 否 否 连接外部进程 否 是(通过D B P a r t n e r) 是 否 否 3.3.2 问题3 请看下面的过程: 节选自在线代码CreditLoop1.sql CREATE OR REPLACE PROCEDURE CreditLoop AS /* Inserts the student ID numbers and their current credit values into temp_table. */ v_StudentID students.ID%TYPE; v_Credits students.current_credits%TYPE; CURSOR c_Students IS SELECT ID FROM students; BEGIN OPEN c_Students; LOOP FETCH c_Students INTO v_StudentID; v_Credits := CountCredits(v_StudentID); INSERT INTO temp_table (num_col, char_col) VALUES (v_StudentID, 'Credits = ' || TO_CHAR(v_Credits)); EXIT WHEN c_Students%NOTFOUND; END LOOP; CLOSE c_Students; END CreditLoop; C r e d i t L o o p只是在表 t e m p _ t a b l e中记录每个学生的分数。该过程调用前一节使用的函数 C o u n t C r e d i t s来计算每个学生的分数。当开始运行函数 C r e d i t L o o p并在 SQL *Plus中查询表 t e m p _ t a b l e时,我们得到下列输出结果: 节选自在线代码callCL.sql SQL> EXEC CreditLoop; 66计计第一部分 PL/SQL介绍及开发环境 下载PL/SQL procedure successfully completed. SQL> SELECT * 2 FROM temp_table 3 ORDER BY num_col; NUM_COL CHAR_COL --------- ---------------- 10000 Credits = 11 10001 Credits = 4 10002 Credits = 8 10003 Credits = 8 10004 Credits = 4 10005 Credits = 4 10006 Credits = 7 10007 Credits = 4 10008 Credits = 8 10009 Credits = 7 10010 Credits = 8 10011 Credits = 3 10011 Credits = 3 13 rows selected. 该输出的问题是最后两行被插入了两次,也就是说,学生 1 0 0 11有两行分数,而其他所有学 生只有一行。 1. 问题3 :使用Rapid SQL进行调试 在Rapid SQL中调试存储过程的第一步是在 P L / S Q L编辑器窗口中打开该过程。接着,我们 可以通过选择菜项 Debug | Start Debugging,或单击如图3 - 2所示的图标Debug PL/SQL开始调试。 调试开始时,Rapid SQL将初始化一个调试会话并建立如图 3 - 3所示的环境。 调试会话包括四个附加的窗口,它们由 Rapid SQL自动打开并放置在屏幕的底部: • 监视窗口,该窗口显示用户指定变量的值,不管变量是否在其作用域中,你都可以在编辑 窗口中将任何变量选中并将其拖动到监视窗口中进行观察。 • 变量窗口,该窗口显示处于作用域中的变量的数值。 • 调用栈窗口,该窗口显示当前运行的源程序行,以及完整的 P L / S Q L调用栈。 • 相关树窗口(Dependency tree window),该窗口显示当前对象之间的相关树形结构。如图 3-3 所示,对象C r e d i t L o o p依赖于对象C o u n t C r e d i t s。 除此之外,编辑窗口中有一个箭头指向当前行的下面。我们可以单击工具条中的 Setp into 按钮来启动该过程开始运行。在此之前,我们要设置一个断点来观察程序的运行情况,当启动 程序运行到断点处来观察变量窗口中本地变量的值。设置断点的方法是单击所希望的源程序行, 接着再单击I n s e r t或R e m o v e断点按钮来设置或取消断点。对于过程 C r e d i t L o o p来说,我们希望断 点设置在程序的第 13 行,在F E T C H语句后停止。如图 3 - 4所示,一旦建立了断点,我们就可以 单击G o按钮运行程序到断点为止。图 3 - 5显示了程序运行到断点处的窗口。如该窗口所示,我们 已经取出了第一行,变量 v _ S t u d e n t I D的值是1 0 0 0 0 ,变量v _ C r e d i t s的值是空(N U L L),这表明程 序运行正常。 第3章 跟踪和调试计计67下载图3-2 准备开始调试过程 C r e d i t L o o p的窗口 图3-3 初始化的C r e d i t L o o p调试会话窗口 从该点开始,我们可以单击按钮 G o进行单步调试,观察每个 F E T C H语句执行后变量 v _ S t u d e n t I D的值。在执行过程中,我们可以看到v _ S t u d e n t I D的值是在正常范围内变化。当程序执行 到最后一个F E T C H语句时,返回的v _ S t u d e n t I D值是1 0 0 11。现在,我们把该值插入到表t e m p _ t a b l e 中并再次开始执行循环,而发现下一个F E T C H语句没有改变v _ S t u d e n t I D的值,它仍然是1 0 0 11,因 68计计第一部分 PL/SQL介绍及开发环境 下载第3章 跟踪和调试计计69下载 此,该值被执行了两次插入。在第二个插入语句I N S E RT后,该循环由于c _ S t u d e n t s % N O T F O U N D 为真而结束循环。这就是问题所在。也就是说,退出语句EXIT应该在FETCH语句后立即执行才对。 现在,我们在P L / S Q L编辑器中来修改C r e d i t L o o p并再次对其进行测试以确保其功能正确。图3 - 6显 示的是正确运行的CreditLoop 程序。该程序的在线代码名称为CreditLoop2.sql。 图3-4 在窗口中设置断点 解决了C r e d i t L o o p存在的问题后,我们可以从SQL *Plus中再次运行该程序,其输出结果如下: 节选自在线代码callCL.sql SQL> EXEC CreditLoop PL/SQL procedure successfully completed. SQL> SELECT * FROM temp_table 2 ORDER BY num_col; NUM_COL CHAR_COL --------- ------------------------------- 10000 Credits = 11 10001 Credits = 4 10002 Credits = 8 10003 Credits = 8 10004 Credits = 4 10005 Credits = 4 10006 Credits = 7 10007 Credits = 4 10008 Credits = 8 10009 Credits = 7 10010 Credits = 8 10011 Credits = 3 12 rows selected.70计计第一部分 PL/SQL介绍及开发环境 下载 图3-5 停止在断点的运行窗口 图3-6 CreditLoop的正确版本2. 问题3: 评论 游标提取循环一直运行到返回 N O _ D ATA _ F O U N D为止(用属性%NOTFOUND 进行检测)。 一旦检测到了 % N O T F O U N D为真,由于已经没有数据可取,该循环就停止运行。输出变量 v _ S t u d e n t I D将保持前一次循环迭代的值。 提示 读者也可以使用游标F O R循环,该类循环可以隐式地打开游标,在循环中每次执 行一次取操作,并在结束循环结束时关闭该游标。 3.3.3 问题4 如果游标存储在包中的话,该游标将一直维持到某个包的子程序调用生存期之外。这样一 来就使我们可以编制一个从该游标每次取出若干行的子程序。这也就是包 S t u d e n t F e t c h的实际功 能。 节选自在线代码StudentFetch1.sql CREATE OR REPLACE PACKAGE StudentFetch AS TYPE t_Students IS TABLE OF students%ROWTYPE INDEX BY BINARY_INTEGER; -- Opens the cursor for processing. PROCEDURE OpenCursor; -- Closes the cursor. PROCEDURE CloseCursor; -- Returns up to p_BatchSize rows in p_Students, and returns -- TRUE as long as there are still rows to be fetched. FUNCTION FetchRows(p_BatchSize IN NUMBER := 5, p_Students OUT t_Students) RETURN BOOLEAN; -- Prints p_BatchSize rows from p_Students. PROCEDURE PrintRows(p_BatchSize IN NUMBER, p_Students IN t_Students); END StudentFetch; CREATE OR REPLACE PACKAGE BODY StudentFetch AS CURSOR c_AllStudents IS SELECT * FROM students ORDER BY ID; -- Opens the cursor for processing. PROCEDURE OpenCursor IS BEGIN OPEN c_AllStudents; 第3章 跟踪和调试计计71下载END OpenCursor; -- Closes the cursor. PROCEDURE CloseCursor IS BEGIN CLOSE c_AllStudents; END CloseCursor; -- Returns up to p_BatchSize rows in p_Students, and returns -- TRUE as long as there are still rows to be fetched. FUNCTION FetchRows(p_BatchSize IN NUMBER := 5, p_Students OUT t_Students) RETURN BOOLEAN IS v_Finished BOOLEAN := TRUE; BEGIN FOR v_Count IN 1..p_BatchSize LOOP FETCH c_AllStudents INTO p_Students(v_Count); IF c_AllStudents%NOTFOUND THEN v_Finished := FALSE; EXIT; END IF; END LOOP; RETURN v_Finished; END FetchRows; -- Prints p_BatchSize rows from p_Students. PROCEDURE PrintRows(p_BatchSize IN NUMBER, p_Students IN t_Students) IS BEGIN FOR v_Count IN 1..p_BatchSize LOOP DBMS_OUTPUT.PUT('ID: ' || p_Students(v_Count).ID); DBMS_OUTPUT.PUT(' Name: ' || p_Students(v_Count).first_name); DBMS_OUTPUT.PUT_LINE(' ' || p_Students(v_Count).last_name); END LOOP; END PrintRows; END StudentFetch; 每次我们调用S t u d e n t F e c t h . F e t c h R o w s时,该程序应返回下一批行。下面的 SQL *Plus会话演 示了该程序的执行过程。 节选自在线代码callSF1.sql SQL> DECLARE 2 v_BatchSize NUMBER := 5; 3 v_Students StudentFetch.t_Students; 4 BEGIN 5 StudentFetch.OpenCursor; 6 WHILE StudentFetch.FetchRows(v_BatchSize, v_Students) LOOP 7 StudentFetch.PrintRows(v_BatchSize, v_Students); 8 END LOOP; 72计计第一部分 PL/SQL介绍及开发环境 下载第3章 跟踪和调试计计73下载 9 StudentFetch.CloseCursor; 10 END; 11 / ID: 10000 Name: Scott Smith ID: 10001 Name: Margaret Mason ID: 10002 Name: Joanne Junebug ID: 10003 Name: Manish Murgatroid ID: 10004 Name: Patrick Poll ID: 10005 Name: Timothy Taller ID: 10006 Name: Barbara Blues ID: 10007 Name: David Dinsmore ID: 10008 Name: Ester Elegant ID: 10009 Name: Rose Riznit PL/SQL procedure successfully completed. 然而,该过程有一个问题,即我们没有取出表 students 所有的记录。该程序只是返回了1 0行, 但实际上,该表有1 2行。 1. 问题4 :使用X P E D I T E R / S Q L进行调试 下面我们将使用 X P E D I T E R / S Q L调试器来解决上述问题。首先,我们要在该调试器窗口中 载入调用块。这可以通过在记事本窗口中打开该调用块实现,然后单击图 3 - 7所示的D e b u g按钮。 最后的调试窗口如图3 - 8所示。 图3-7 准备调试调用块的窗口74计计第一部分 PL/SQL介绍及开发环境 下载 图3-8 调试窗口显示的调用块 图3-9 设置断点第3章 跟踪和调试计计75下载 现在我们可以单步运行该代码到 F e t c h R o w s这一行。我们要在第 2 7行设置一个断点来观察变 量v _ F i n i s h e d何时被置为FA L S E。设置该断点的方法是双击该语句所在的行号。图 3 - 9是设置了 该断点的窗口。现在我们可以运行该程序直到设置的断点为止。在运行过程中,除了运行之外, 我们还可以使用X P E D I T E R / S Q L的动画功能,该功能自动地按用户设置的执行速度单步运行到 断点。执行动画功能后到达断点的窗口如图 3 - 1 0所示。 图3-10 停在断点的调试窗口 问题现在搞清楚了。我们可以从前面的显示内容以及从变量 v _ C o u n t的当前值是3(本地变 量显示在图3 - 1 0的窗口的工具箱中)的情况下确认对 F e t c h R o w s的调用取回了两行记录。在该断 点,我们将变量v _ f i n i s h e d设置为FA L S E并立即返回该值。但是调用块不会立即打印这些额外的 行记录,这是因为该调用块只在 F e t c h R o w s返回T R U E时才调用打印语句P r i n t R o w s。 解决上面的问题需要做两处修改。首先,函数 F e c t c h R o w s要能够返回检索到的实际行数, 改动的代码如下所示: 节选自在线代码StudentFetch2.sql -- Returns up to p_BatchSize rows in p_Students, and returns -- TRUE as long as there are still rows to be fetched. -- The actual number of rows fetched is returned in p_BatchSize. FUNCTION FetchRows(p_BatchSize IN OUT NUMBER, p_Students OUT t_Students) RETURN BOOLEAN IS v_Finished BOOLEAN := TRUE;BEGIN FOR v_Count IN 1..p_BatchSize LOOP FETCH c_AllStudents INTO p_Students(v_Count); IF c_AllStudents%NOTFOUND THEN v_Finished := FALSE; p_BatchSize := v_Count - 1; EXIT; END IF; END LOOP; RETURN v_Finished; END FetchRows; 第二,我们要在最后一个f e t c h操作后调用打印函数P r i n t R o w s,修改的程序如下: 节选自在线代码callSF2.sql SQL> DECLARE 2 v_BatchSize NUMBER := 5; 3 v_Students StudentFetch.t_Students; 4 BEGIN 5 StudentFetch.OpenCursor; 6 WHILE StudentFetch.FetchRows(v_BatchSize, v_Students) LOOP 7 StudentFetch.PrintRows(v_BatchSize, v_Students); 8 END LOOP; 9 -- Print any extra rows from the last batch. 10 IF v_BatchSize != 0 THEN 11 StudentFetch.PrintRows(v_BatchSize, v_Students); 12 END IF; 13 StudentFetch.CloseCursor; 14 END; 15 / ID: 10000 Name: Scott Smith ID: 10001 Name: Margaret Mason ID: 10002 Name: Joanne Junebug ID: 10003 Name: Manish Murgatroid ID: 10004 Name: Patrick Poll ID: 10005 Name: Timothy Taller ID: 10006 Name: Barbara Blues ID: 10007 Name: David Dinsmore ID: 10008 Name: Ester Elegant ID: 10009 Name: Rose Riznit ID: 10010 Name: Rita Razmataz ID: 10011 Name: Shay Shariatpanahy PL/SQL procedure successfully completed. 2. 问题4 :评论 一开始,该f e t c h循环看起来非常象问题 3中分析过的循环。然而,该循环的每个 f e c t c h操作 可以返回多达 5行记录,而不是一个记录。该循环类似于成组循环操作,需要注意的是在结束 f e t c h的条件出现后,我们必须继续处理剩余的行。 76计计第一部分 PL/SQL介绍及开发环境 下载3.3.4 问题5 斐波纳契序列是由如 Fi b ( n ) =Fi b ( n - 1 ) +Fi b ( n - 2 )一系列数组成的。 Fi b ( 0 )的值为0,Fi b ( 1 )的值 为1。由此定义了下面的序列: 0,1,1,2,3,5,8,13,... 我们可以用P L / S Q L写一个函数来计算第n个斐波纳契数。该函数如下: 节选自在线代码Fibonacci.sql CREATE OR REPLACE FUNCTION Fib(n IN BINARY_INTEGER) RETURN BINARY_INTEGER AS BEGIN RETURN Fib(n - 1) + Fib(n - 2); END Fib; 然而,当我们从SQL *Plus中运行该函数时,该函数没有返回结果并出现内存溢出错误。 1. 问题5:使用SQL Navigator进行调试 下面,我们来使用SQL Navigator调试器来解决上述问题。类似于其他的 P L / S Q L调试器,我 们可以直接在SQL Navigator内部调试函数Fi b的全部代码,如图3 - 11所示,我们只要在存程序编 辑器中打开该函数并执行单步操作就可以进入调试。这时,该窗口内将出现一个会话框来接收 计算需要的初值n ,并构造一个匿名块来调用该函数。 图3 - 11 准备直接调试函数Fi b 但现在我们要从SQL *Plus的窗口中调用函数Fi b并且把SQL Navigator的会话与SQL *Plus的 窗口连接。这种功能类似某些 C调试器具有的可以把运行的进程与其连接调试的能力。为了使用 第3章 跟踪和调试计计77下载78计计第一部分 PL/SQL介绍及开发环境 下载 P L / S Q L实现这种调试方法,调用程序必须首先调用包 D B M S _ D E B U G中的两个子程序来启动调 试器与其连接,下面是实现该功能的命令: 节选自在线代码attachFib.sql SQL> -- First initialize the debugger SQL> ALTER SESSION SET PLSQL_DEBUG = TRUE; Session altered. SQL> DECLARE 2 v_DebugID VARCHAR2(30); 3 BEGIN 4 v_DebugID := DBMS_DEBUG.INITIALIZE('DebugMe'); 5 DBMS_DEBUG.DEBUG_ON; 6 END; 7 / PL/SQL procedure successfully completed. A LTER SESSION语句命令 P L / S Q L编译器来编译带有 D E B U G选项的未来匿名块( F u t u r e anonymous block)。D B M S _ D E B U G . I N I T I A L I Z E带有供服务器标识的字符串, D B M S _ D E B U G . D E B U G _ O N告诉服务器下面的块包括了要调试的调用。如下所示,我们通过发布一个 对Fi b的调用来实现该功能。 SQL> -- And now execute the call to Fib. This will hang until you SQL> -- attach to this session in SQL Navigator. SQL> exec DBMS_OUTPUT.PUT_LINE(Fib(3)); 图3-12 连接会话对话框现在,我们通过选择菜单项 Debug | Attach External Session来连接SQL Navigator的会话。 执行该选择后将启动一个对话框,请求输入在调用 I N I T I A L I Z E中使用的标识符,该对话框如图 3 - 1 2所示。一旦我们输入了会话标识符,屏幕上就出现一个运行状态窗口,同时还有一个显示 我们从SQL *Plus中运行的匿名块的调用栈的子窗口。现在,我们可以进入该块对函数 Fi b进行调 试了。Fi b的调试窗口如图3 - 1 3所示。 图3-13 调试函数Fi b的窗口 我们现在可以为n 设置一个观察点并继续单步执行程序。设置变量的观察点的方法是先选中 变量,接着右键单击该变量,最后从上下文菜单中选择 Add Wa t c h p o i n t来建立观察点。我们可以 继续单步运行该程序来观察当函数返回时 n有什么变化。运行该程序几次后, n的值如图3 - 1 4所 示,从该图中,我们可以发现出现问题的原因。 可以看出,我们在函数 Fi b中没有设置结束条件。第一个调用将从 n中减去1 ,接着循环调用 Fi b函数。这次调用也进行减 1操作,并再反复调用。该过程继续下去,不断地从 n中减去数值, 同时把另外的调用加入到堆栈,直到发生存储器溢出错位。解决该问题的方法是在 Fi b中加入停 止条件,下面是经过改动的程序: 节选自在线代码Fibonacci.sql CREATE OR REPLACE FUNCTION Fib(n IN BINARY_INTEGER) RETURN BINARY_INTEGER AS BEGIN IF n = 0 OR n = 1 THEN RETURN n; 第3章 跟踪和调试计计79下载ELSE RETURN Fib(n - 1) + Fib(n - 2); END IF; END Fib; 图3-14 变量的负值 上面的版本确实可以返回正确的结果,下面是 SQL *Plus的会话: 节选自在线代码Fibonacci.sql SQL> BEGIN 2 -- Some calls to Fib. 3 FOR v_Count IN 1..10 LOOP 4 DBMS_OUTPUT.PUT_LINE( 5 'Fib(' || v_Count || ') is ' || Fib(v_Count)); 6 END LOOP; 7 END; 8 / Fib(1) is 1 Fib(2) is 1 Fib(3) is 2 Fib(4) is 3 Fib(5) is 5 Fib(6) is 8 Fib(7) is 13 Fib(8) is 21 80计计第一部分 PL/SQL介绍及开发环境 下载第3章 跟踪和调试计计81下载 Fib(9) is 34 Fib(10) is 55 PL/SQL procedure successfully completed. 2. 问题5 :评论 递归函数可以提供简单而精致的解决方案,但其前提是它们必须具有正确的停止条件。如 果没有设置这种条件,这种函数将会导致内存溢出错误。 即使设置了停止条件,然而,递归函数并不是解决某类特殊问题的最有效方式,这是因为 递归函数涉及了重复的函数调用。我们将在本章的基于 P L / S Q L的配置一节中介绍函数Fi b的迭代 版本程序。 3.3.5 问题6 在很多情况下, P L / S Q L程序的问题并不是在程序的本身,而是与程序处理的数据有关。例 如,请看下面的S Q L脚本,该脚本完成从表s o u r c e向表d e s t i n a t i o n复制数据的任务: 节选自在线代码CopyTables.sql CREATE OR REPLACE PROCEDURE CopyTables AS v_Key source.key%TYPE; v_Value source.value%TYPE; CURSOR c_AllData IS SELECT * FROM source; BEGIN OPEN c_AllData; LOOP FETCH c_AllData INTO v_Key, v_Value; EXIT WHEN c_AllData%NOTFOUND; INSERT INTO destination (key, value) VALUES (v_Key, TO_NUMBER(v_Value)); END LOOP; CLOSE c_AllData; END CopyTables; 上面的表s o u r c e和表d e s t i n a t i o n都是用下面的语句创建的: 节选自在线代码relTables.sql CREATE TABLE source ( key NUMBER(5), value VARCHAR2(50)); CREATE TABLE destination ( key NUMBER(5), value NUMBER);82计计第一部分 PL/SQL介绍及开发环境 下载 请注意表s o u r c e的列值是VA R C H A R 2 ,但表d e s t i n a t i o n的列值是N U M B E R类型。假设我们使 用下面的P L / S Q L块向表s o u r c e填充5 0 0行的话,对于这5 0 0行来说,其中的4 9 9行是合法的字符串 (可以转换为N U M B E R类型)。然而,其中一行(使用第 4章中的R a n d o m程序包随机生成)具有 非法值。 节选自在线代码populate.sql DECLARE v_RandomKey source.key%TYPE; BEGIN -- First fill up the source table with legal values. FOR v_Key IN 1..500 LOOP INSERT INTO source (key, value) VALUES (v_Key, TO_CHAR(v_Key)); END LOOP; -- Now, pick a random number between 1 and 500, and update that -- row to an illegal value. v_RandomKey := Random.RandMax(500); UPDATE source SET value = 'Oops, not a number!' WHERE key = v_RandomKey; COMMIT; END; 如果我们现在调用C o p y t a b l e s,我们将得到编号为O R A - 1 7 2 2的错误信息: SQL> exec CopyTables begin CopyTables; end; * ERROR at line 1: ORA-01722: invalid number ORA-06512: at "EXAMPLE.COPYTABLES", line 15 ORA-06512: at line 1 可以看出,该错误发生在执行 I N S E RT语句时,当我们不知道哪个值有问题,为了找出该非 法值,我们可以使用调试器来确认错误发生的时间。 1. 问题6:使用TO A D调试器 为了在TO A D调试器下运行P L / S Q L过程,首先必须从“ Stored Procedure Edit/Compile”窗 口下运行该程序,如图3 - 1 5所示,C o p y Ta b l e s出现在打开的窗口中。接着,我们可以通过选择菜 单项 Debug | Trace Into,或单击调试器窗口中工具条中的 Trace Into按钮开始单步调试该过程。 如图3 - 1 6所示,该过程被启动执行,并停在第一行的位置。 下一步是为两个本地变量 v _ K e y和v _ Va l u e,设置观察点。我们只要选中这两个变量并单击 Add Wa t c h按钮就可以将它们加入到变量观察窗口中。这时的窗口如图 3 - 1 7所示。现在,这两个 变量的值都为空( N U L L),这是因为我们还没有对它们赋值的原因。我们可以继续运行该过程, TO A D将在有错误发生时自动停止运行,发生错误的窗口如图 3 - 1 8所示。当我们继续运行该程序第3章 跟踪和调试计计83下载 时,本地变量的值将显示在观察窗口中。该窗口中显示的内容告诉我们有问题的数据是在变量 v _ K e y的值为4 3 4的地方,显示该错误数据的窗口如图 3 - 1 9所示。 图3-15 显示E d i t / C o m p i l e窗口中的C o p y Ta b l e s 图3-16 停止在过程第一行的窗口图3-17 设置观察点 图3-18 停止在错误处的调试窗口 84计计第一部分 PL/SQL介绍及开发环境 下载第3章 跟踪和调试计计85下载 图3-19 有错误的数据 2. 问题6 :评论 尽管上述只是一个简单的问题,但是,数据可能在更复杂、更微妙的方式下处于非法状态。 在上述情况下,数据的类型有错误,因此不能进行类型转换。另外,也存在着数据的值超出取 值范围的问题。请注意非法数据并不是每次都会引发异常,异常的引发取决于程序中的错误处 理机制和数据不相容的性质,不同的情况会出现不同的结果。 值得注意的是,在上述情况下, P L / S Q L代码本身并没有问题。问题的存在与代码无关,它 只与代码处理的数据有关。因此,我们可以通过查询数据库表来发现错误数据,而不必对程序 进行调试。 3.3.6 问题7 请看下面的过程: 节选自在线代码RSLoop1.sql CREATE OR REPLACE PROCEDURE RSLoop AS v_RSRec registered_students%ROWTYPE; CURSOR c_RSGrades IS SELECT * FROM registered_students ORDER BY grade; BEGIN -- Loop over the cursor to determine the last row.86计计第一部分 PL/SQL介绍及开发环境 下载 FOR v_RSRec IN c_RSGrades LOOP NULL; END LOOP; -- And print it out. DBMS_OUTPUT.PUT_LINE( 'Last row selected has ID ' || v_RSRec.student_id); END RSLoop; 过程R S L o o p查询表r e g i s t e r e d _ s t u d e n t s (该表按分数排序),并打印从最后一行取出的 I D。然 而,当我们运行该程序时,其执行结果为空( N U L L): SQL> exec RSLoop; Last row selected has ID PL/SQL procedure successfully completed. 1. 问题7 :使用S Q L - P r o g r a m m e r进行调试 调试该程序的第一步是在如图 3 - 2 0中所示的开发窗口中打开过程 R S L o o p。接着,我们可以 使用单步按钮来调试该过程代码,图 3 - 2 1演示了处于调试状态的窗口。 图3-20 准备调试过程R S L o o p的窗口 下一步是为变量v _ R S R e c设置观察点。其做法是该代码中选中该变量,然后将其拖入观察窗 口中,如图3 - 2 2所示。我们可以看到,由于我们刚启动该过程,该变量的值为空( N U L L)。当 单步调试代码时,我们应该能够看到在循环过程中变量 v _ R S R e c的值每次都在变化。但实际上,如图3 - 2 3所示的观察窗口所显示的,该变量始终为空值。从该窗口中,我们还可以看到程序处 理到第八行,但变量v _ R S R e c的值仍然为空。 图3-21 停在过程代码第一行的调试窗口 图3-22 观察变量v _ R S R e c的调试窗口 第3章 跟踪和调试计计87下载图3-23 显示v _ R S R e c仍然为空的观察窗口 为什么该程序的循环没有对 v _ R S R e c进行赋值呢?问题出在该循环声明了一个隐式变量,其 名称也叫v _ R S R e c。在循环体内部,隐式声明的变量把显式声明的变量隐藏起来,因此该变量不 能赋值。一种修改的方法是把该循环用显式 F E T C H循环取代,如下所示,这种循环将使用显示 声名的变量: 节选自在线代码RSLoop2.sql CREATE OR REPLACE PROCEDURE RSLoop AS v_RSRec registered_students%ROWTYPE; CURSOR c_RSGrades IS SELECT * FROM registered_students ORDER BY grade; BEGIN -- Loop over the cursor to determine the last row. OPEN c_RSGrades; LOOP FETCH c_RSGrades INTO v_RSRec; EXIT WHEN c_RSGrades%NOTFOUND; END LOOP; CLOSE c_RSGrades; -- And print it out. DBMS_OUTPUT.PUT_LINE( 'Last row selected has ID ' || v_RSRec.student_id); END RSLoop; 88计计第一部分 PL/SQL介绍及开发环境 下载SQL> exec RSLoop Last row selected has ID 10006 PL/SQL procedure successfully completed. 2. 问题7: 评论 隐式声明的循环变量(包括游标 F o r循环的记数器和数值 F O R循环的循环记数器)的作用域 仅限制在循环期间,这些变量在循环体内可以将同名变量隐藏起来。我们还可以通过将隐式变 量换名,并在循环内将其值赋予显式声明的变量来解决该问题。 3.4 跟踪和配置 到目前为止,我们所讨论的调试技术可以用来指出应用程序中特殊问题的原因,然而,这 些技术还不能处理所有可能遇到的程序问题,如程序的性能问题。为了弥补这种缺点, P L / S Q L 提供了几种不同的跟踪和配置工具。跟踪应用程序的结果是生成一份显示应用中调用子程序和 所发生异常的清单。配置功能使在跟踪应用程序产生的报告中增加了时序信息。 基于事件的P L / S Q L跟踪功能是在O r a c l e 7.3.4版本中首次提供的; O r a c l e数据库的O r a c l e 8 i第 一版(8 . 1 . 5)提供了跟踪PL/SQL API功能;而提供配置功能的是 Oracle8i 第二版(8 . 1 . 6)。我们 将在下面讨论这些调试方法。 我们在下面几节使用的所有案例都将使用如下的过程和包。其中包 R a n d o m在本书的第4章 中。 节选自在线代码traceDemo.sql -- Returns fib(n), equivalent to fib(n-1) + fib(n-2). CREATE OR REPLACE FUNCTION RecursiveFib(n IN BINARY_INTEGER) RETURN BINARY_INTEGER AS BEGIN IF n = 0 OR n = 1 THEN RETURN n; ELSE RETURN RecursiveFib(n - 1) + RecursiveFib(n - 2); END IF; END RecursiveFib; CREATE OR REPLACE FUNCTION IterativeFib(n IN BINARY_INTEGER) RETURN BINARY_INTEGER AS v_Result BINARY_INTEGER; v_Sum1 BINARY_INTEGER := 1; v_Sum2 BINARY_INTEGER := 1; BEGIN IF n = 1 OR n = 2 THEN RETURN 1; ELSE FOR v_Count IN 2..n - 1 LOOP v_Result := v_Sum1 + v_Sum2; v_Sum2 := v_Sum1; v_Sum1 := v_Result; 第3章 跟踪和调试计计89下载END LOOP; RETURN v_Result; END IF; END IterativeFib; CREATE OR REPLACE PROCEDURE RaiseIt(p_Exception IN NUMBER) AS e_MyException EXCEPTION; BEGIN IF p_Exception = 0 THEN NULL; ELSIF p_Exception < 0 THEN RAISE e_MyException; ELSIF p_Exception = 1001 THEN RAISE INVALID_CURSOR; ELSIF p_Exception = 1403 THEN RAISE NO_DATA_FOUND; ELSIF p_Exception = 6502 THEN RAISE VALUE_ERROR; ELSE RAISE_APPLICATION_ERROR(-20001, 'Exception ' || p_Exception); END IF; END RaiseIt; CREATE OR REPLACE PROCEDURE CallRaise(p_Exception IN NUMBER := 0) AS BEGIN RaiseIt(p_Exception); EXCEPTION WHEN OTHERS THEN NULL; END CallRaise; CREATE OR REPLACE PROCEDURE RandomRaise(p_NumCalls IN NUMBER := 1) AS v_Case NUMBER; BEGIN FOR v_Count IN 1..p_NumCalls LOOP v_Case := Random.RandMax(6); IF v_Case = 1 THEN CallRaise(-1); ELSIF v_Case = 2 THEN CallRaise(0); ELSIF v_Case = 3 THEN CallRaise(1001); ELSIF v_Case = 4 THEN CallRaise(1403); ELSIF v_Case = 5 THEN CallRaise(6502); ELSE CallRaise(v_Case); END IF; 90计计第一部分 PL/SQL介绍及开发环境 下载第3章 跟踪和调试计计91下载 END LOOP; END RandomRaise; CREATE OR REPLACE PROCEDURE CallMe1 AS BEGIN NULL; END CallMe1; CREATE OR REPLACE PROCEDURE CallMe2 AS BEGIN NULL; END CallMe2; CREATE OR REPLACE PROCEDURE CallMe3 AS BEGIN NULL; END CallMe3; CREATE OR REPLACE PACKAGE CallMe AS PROCEDURE One; PROCEDURE Two; PROCEDURE Three; END CallMe; CREATE OR REPLACE PACKAGE BODY CallMe AS PROCEDURE One IS BEGIN NULL; END One; PROCEDURE Two IS BEGIN NULL; END Two; PROCEDURE Three IS BEGIN NULL; END Three; END CallMe; CREATE OR REPLACE PROCEDURE RandomCalls(p_NumCalls IN NUMBER := 1) AS v_Case NUMBER; BEGIN FOR v_Count IN 1..p_NumCalls LOOP v_Case := Random.RandMax(6); IF v_Case = 1 THEN CallMe1;92计计第一部分 PL/SQL介绍及开发环境 下载 ELSIF v_Case = 2 THEN CallMe2; ELSIF v_Case = 3 THEN CallMe3; ELSIF v_Case = 4 THEN CallMe.One; ELSIF v_Case = 5 THEN CallMe.Two; ELSE CallMe.Three; END IF; END LOOP; END RandomCalls; 3.4.1 基于事件的跟踪 这种类型的跟踪功能需要设置数据库事件的允许或禁止标志。 P L / S Q L和R D B M S都提供了 叫做数据库事件的调试工具。设置事件有以下两种方式: • 在特别的会话中,使用A LTER SESSION语句。其语法是: ALTER SESSION SET EVENTS 'event event_string'; 其中e v e n t是事件号,e v e n t _ s t r i n g描述了设置该事件的方式。使用上述命令,该事件只与这 个特别的会话建立连接,并不会影响其他的数据库会话。 • 对于全部数据库的设置,在数据库初始化文件中( i n i t . o r a)使用如下所示的参数: event="event event_string" 其中,e v e n t是事件号,e v e n t _ s t r i n g用于描述该事件的设置方式。使用上述命令后,该事件 在数据库被关闭并重启动后,将与所有数据库会话建立连接。 不同的事件可以启动不同种类的事件跟踪功能。在各种跟踪中,跟踪信息都被写入会话跟 踪文件中,该文件存储在由数据库初始文件参数 U S E R _ D U M P _ D E S T指示的目录中。 提示 如果不知道U S E R _ D U M P _ D E S T的值,你可以通过查询v $ d a t a b a s e _ p a r a m e t e r s数 据字典窗口来找到该值,也可以使用服务器管理器( Server Manager)的命令S H O W PA R A M E T E R S,或使用SQL *Plus 8i及更高版本的工具来确认该值。 由于附加信息都将被写入到跟踪文件中,设置事件连接将不可避免地影响程序性能。数据 库事件除了有在本节中描述的用途外,还可用于多种其他目的。设置额外事件将可能会带来一 定的影响,因此事件的设置要在 O r a c l e支持服务的指导下实施。 1. 跟踪特殊的错误 设置等价于某个特殊错误号的事件将导致数据库把该错误发生时的信息转储到跟踪文件中, 特别是,跟踪文件将包括发生错误时的当前 S Q L语句和调用队栈。为了设置具有这种功能的事 件,应使用下列事件字符串: event_num trace name errorstack 其中,e v e n t _ n u m是指定的错误号。例如,假设我们提交下列匿名块 :第3章 跟踪和调试计计93下载 节选自在线代码event6502.sql SQL> -- First set the events in the session SQL> ALTER SESSION SET EVENTS '6502 trace name errorstack'; Session altered. SQL> -- And then raise ORA-6502 SQL> BEGIN 2 RaiseIt(6502); 3 END; 4 / BEGIN * ERROR at line 1: ORA-06502: PL/SQL: numeric or value error ORA-06512: at "EXAMPLE.RAISEIT", line 13 ORA-06512: at line 2 该块将生成包括类似下面内容的跟踪文件: *** SESSION ID:(7.4) 2000-01-10 17:54:08.710 *** 2000-01-10 17:54:08.710 ksedmp: internal or fatal error ORA-06502: PL/SQL: numeric or value error Current SQL statement for this session: BEGIN RaiseIt(6502); END; ----- PL/SQL Call Stack ----- object line object handle number name 802da2a0 2 anonymous block 该跟踪文件指出当前的 S Q L语句是调用R a i s e I t的匿名块。同时,该文件还包括了 P L / S Q L调 用栈,该调用栈也指出了匿名块是可能出错的地方。然而,真正的错误发生在 R a i s e I t内部,而 不是在匿名块中。为什么该调用栈不能提供发生错误时完整的 P L / S Q L栈呢?为了回答这个问题, 我们要进一步介绍跟踪文件生成的过程。 当一个P L / S Q L块发送到服务器运行时,服务器的影子进程( Shadow process)将其作为 P L / S Q L块接收。这时,该块被送往 P L / S Q L引擎执行,而不是提交给 S Q L语句执行器。当 P L / S Q L引擎返回时,所有的处理结果都将送回到客户端。如果 P L / S Q L引擎返回错误(如上面叙 述的情况),服务器将在事件建立了连接的情况下,把这些错误信息写入跟踪文件。在该服务器 记录错误信息时,该错误已经从内部过程传播到了匿名块中。因此,调用栈仅指示该匿名块有 错。 注意 以这种方式生成的跟踪文件还包括了其他信息,如 C的调用栈和C P U寄存器的转 储信息,虽然,这类信息对确定源代码中出现的错误没有什么作用。但是它们对定位 O r a c l e代码中发生的错误非常有用。 2. 调用和异常跟踪94计计第一部分 PL/SQL介绍及开发环境 下载 这一级别的跟踪功能只能在 O r a c l e 7版本的7 . 3 . 4以及O r a c l e 8版的8 . 0 . 5和更高版本中使用。低 于O r a c l e 8版8 . 0 . 5的版本都不具备该功能。 基于事件的调用和异常的跟踪允许用户跟踪 P L / S Q L的三类事件:调用存储子程序,引发异 常和连接变量的值。该类跟踪可以在指定的事件发生时把输出记录到跟踪文件中,或者把跟踪 数据存储在循环缓冲区中,并在一定的条件下进行转储。使用循环缓冲区的好处是可以限制跟 踪文件的容量过大。 事件级别 为了启动调用和异常跟踪,可以使用 event 10938来实现。该事件字符串的语法 是: 10938 trace name context level level_num 其中,l e v e l _ n u m是下面列出的值按位“或”(O R)。 跟踪名称 十六进制值 十进制值 注释 T R A C E _ A C A L L 0 x 0 0 0 1 1 跟踪所有调用 T R A C E _ E C A L L 0 x 0 0 0 2 2 跟踪允许的调用 T R A C E _ A E X C P 0 x 0 0 0 4 4 跟踪所有的异常 T R A C E _ E E X C P 0 x 0 0 0 8 8 跟踪允许的异常 T R A C E _ C I R C U L A R 0 x 0 0 1 0 1 6 使用环形缓冲区 T R A C E _ B I N D _ VA R S 0 x 0 0 2 0 3 2 跟踪连接变量 表中所述的允许调用是已经用 D E B U G选项进行编译的子程序,允许异常则是从允许的子程 序中引发的异常(详细介绍请看下文的“允许调用和异常”)。为了计算所希望的级别,可以取 需要跟踪类型的按位或的值作为跟踪级别(等于各个跟踪类型的十进制数的和)。例如: • 级别17(ACALL | CIRCULAR)可以跟踪所有的调用,并使用环形缓冲区。 • 级别22(ECALL | AEXCP)将跟踪允许的调用和所有的异常,使用环形缓冲区。 • 级别3 2 ( B I N D _ VARS )将跟踪连接变量,不使用缓冲区。 • 级别53(ACALL | AEXCP | CIRCULAR | BIND_VA R S )将是最高级别的跟踪,并使用缓冲 区。 • 级别37(ACALL | AEXCP | BIND_VA R S )是最高级别的跟踪,不使用缓冲区。 假设,我们把下列S Q L语句提交给数据库: 节选自在线代码AllCallsExceptions.sql SQL> -- Enable tracing of all calls and all exceptions. 5 is the SQL> -- bitwise OR of 0x01 and 0x04. SQL> ALTER SESSION SET EVENTS '10938 trace name context level 5'; Session altered. SQL> -- Anonymous block which raises some exceptions. SQL> BEGIN 2 CallRaise(1001); 3 RaiseIt(-1); 4 END; 5 / BEGIN* ERROR at line 1: ORA-06510: PL/SQL: unhandled user-defined exception ORA-06512: at "EXAMPLE.RAISEIT", line 7 ORA-06512: at line 3 该命令将在跟踪文件中生成下列输出: ------------ PL/SQL TRACE INFORMATION ----------- Levels set : 1 4 Trace: ANONYMOUS BLOCK: Stack depth = 1 Trace: PROCEDURE EXAMPLE.CALLRAISE: Call to entry at line 3 Stack depth = 2 Trace: PROCEDURE EXAMPLE.RAISEIT: RAISEIT Stack depth = 3 Trace: Pre-defined exception - OER 1001 at line 9 of PROCEDURE EXAMPLE.RAISEIT: Trace: PROCEDURE EXAMPLE.RAISEIT: RAISEIT Stack depth = 2 Trace: User defined exception at line 7 of PROCEDURE EXAMPLE.RAISEIT: 对服务器的每个调用都将生成与上面类似的输出,这些输出具有下列特点: • 在位于跟踪文件的开始处,打印“ PL/SQL TRACE INFORMAT I O N”,接着是当前设置的 跟踪级别。如上所示的跟踪文件中指出的级别 1和 4表示允许 T R A C E _ A C A L L 和 T R A C E _ A E X C P跟踪。 • 在开始行之后,当跟踪事件发生时,就在该文件中增加一行信息。在上面的例子中,由于 允许T R A C E _ A C A L L和T R A C E _ A E X C P跟踪,因此,每当调用一个子程序或发生异常时, 就在跟踪文件登录一项。 • 对于子程序调用,该文件中打印了堆栈的长度。其中的文本信息也显示了堆栈的长度。随 着堆栈的延伸,跟踪文件的每一行的长度也在增加。跟踪文件的每行的最大长度为 5 1 2个 字符,这就限制了堆栈的最大长度。 • 每当异常发生时,跟踪文件中都有记录。异常的记录与处理异常的程序所在的块无关。 在下面几节中,我们将会看到不同类型的跟踪文件的例子。 允许调用和异常 我们在上面提到过允许子程序是用 D E B U G选项进行编译的子程序。指定 调用和异常的方法有两个,第一个是提交下面的语句: ALTER SESSION SET PLSQL_DEBUG= TRUE; 执行该语句后,任何P L / S Q L块或子程序都将用D E B U G参数进行编译。也可以使用下面的语 句重编存储子程序: ALTER [PROCEDURE | FUNCTION | PACKAGE BODY | TYPE BODY ] object_name COMPILE DEBUG; 匿名块只可以通过提交 A LTER SESSION语句使用D E B U G项进行编译。例如,我们可以向 数据库提交下列S Q L语句: 节选自在线代码EnabledCallsExceptions.sql SQL> -- Enable tracing of enabled calls and exceptions. 10 is the SQL> -- bitwise OR of 0x02 and 0x08. SQL> ALTER SESSION SET EVENTS '10938 trace name context level 10'; Session altered. 第3章 跟踪和调试计计95下载SQL> ALTER PROCEDURE RaiseIt COMPILE DEBUG; Procedure altered. SQL> -- Anonymous block which raises some exceptions. SQL> BEGIN 2 CallRaise(1001); 3 RaiseIt(-1); 4 END; 5 / BEGIN * ERROR at line 1: ORA-06510: PL/SQL: unhandled user-defined exception ORA-06512: at "EXAMPLE.RAISEIT", line 7 ORA-06512: at line 3 R a i s e I t是唯一允许的块。因此,跟踪文件只显示下列内容: ---------- PL/SQL TRACE INFORMATION ----------- Levels set : 2 8 Trace: PROCEDURE EXAMPLE.RAISEIT: RAISEIT Stack depth = 3 Trace: Pre-defined exception - OER 1001 at line 9 of PROCEDURE EXAMPLE.RAISEIT: Trace: PROCEDURE EXAMPLE.RAISEIT: RAISEIT Stack depth = 2 Trace: User defined exception at line 7 of PROCEDURE EXAMPLE.RAISEIT: 该文件中登录的入口都来自于 R a i s e I t。如果在非允许的块中(没有使用 D E B U G选项编译过 的)引发了异常,该异常不在跟踪文件中记录。 跟踪对打包的子程序调用 当调用一个打包子程序时,该包内指定的子程序将不记录在跟 踪文件中。例如,假设我们在 SQL *Plus下提交下列匿名块: 节选自在线代码PackgeCalls.sql SQL> -- Enable tracing of all calls and all exceptions. 5 is the SQL> -- bitwise OR of 0x01 and 0x04. SQL> ALTER SESSION SET EVENTS '10938 trace name context level 5'; Session altered. SQL> -- Anonymous block which calls packaged procedures. SQL> BEGIN 2 CallMe.One; 3 CallMe.Two; 4 CallMe.Three; 5 END; 6 / PL/SQL procedure successfully completed. 该块将在跟踪文件中生成下列的内容。 ------------ PL/SQL TRACE INFORMATION ----------- Levels set : 1 4 Trace: ANONYMOUS BLOCK: Stack depth = 1 Trace: PACKAGE BODY EXAMPLE.CALLME: Call to entry at line 4 Stack depth = 2 96计计第一部分 PL/SQL介绍及开发环境 下载第3章 跟踪和调试计计97下载 Trace: PACKAGE BODY EXAMPLE.CALLME: Call to entry at line 9 Stack depth = 2 Trace: PACKAGE BODY EXAMPLE.CALLME: Call to entry at line 14 Stack depth = 2 从该跟踪文件中可以看出,除了每个登录行有包体的名称外,还提供了有关该行的信息, 利用这些信息,我们可以识别指定的打包子程序。 连接变量 如果设置了调试级别 T R A C E _ B I N D _ VA R S的话,连接变量的信息就会被记录在 跟踪文件中。其实现方法是在 P L / S Q L得到连接变量信息时,为每个连接变量发生的事件在跟踪 文件中记录一行。连接变量信息的打印与它所在的块是否已建立了事件连接无关。下面的 S Q L * P l u s的会话将显示包含连接变量的 P L / S Q L块。 节选自在线代码BindVariables.sql SQL> -- First set up the variables SQL> VARIABLE v_String1 VARCHAR2(20); SQL> VARIABLE v_String2 VARCHAR2(20); SQL> BEGIN 2 :v_String1 := 'Hello'; 3 :v_String2 := ' World!'; 4 END; 5 / PL/SQL procedure successfully completed. SQL> -- Enable tracing for all calls and bind variables. SQL> ALTER SESSION SET EVENTS '10938 trace name context level 33'; Session altered. SQL> BEGIN 2 DBMS_OUTPUT.PUT_LINE(:v_String1 || :v_String2); 3 END; 4 / Hello World! PL/SQL procedure successfully completed. 该块生成类似于下面的跟踪文件。 ------------ PL/SQL TRACE INFORMATION ----------- Levels set : 1 32 Trace: ANONYMOUS BLOCK: Stack depth = 1 op: GBVAR; pos: 1; buf: 10bd7b8; len: 5; ind: 0; bfl: 20; op: GBVAR; pos: 2; buf: 10bd7d8; len: 7; ind: 0; bfl: 20; Trace: PACKAGE BODY SYS.DBMS_OUTPUT: Call to entry at line 1 Stack depth = 2 Trace: PACKAGE BODY SYS.DBMS_OUTPUT: Call to entry at line 1 Stack depth = 3 Trace: PACKAGE BODY SYS.STANDARD: Call to entry at line 793 Stack depth = 4 Trace: PACKAGE BODY SYS.STANDARD: ICD vector index = 45 Stack depth = 4 Trace: PACKAGE BODY SYS.STANDARD: Call to entry at line 564 Stack depth = 5 除了调用 D B M S _ O U T P U T(该包依次再调用包 S TA N D A R D),上面的跟踪信息还显示 P L / S Q L接收了使用伪操作码G B VA R的两个连接变量。 提示 上述类型的跟踪不显示连接变量的值,而只是报告连接变量的类型和长度。我们 可以通过把 event 10046的跟踪级别设置为 4来显示连接变量的值。这类事件还将生成98计计第一部分 PL/SQL介绍及开发环境 下载 S Q L _ T R A C E信息。我们将在3 . 4 . 1节中专门介绍这种跟踪方法。 使用环形缓冲区 当跟踪调用时,特别是跟踪长时间运行的程序时,跟踪文件有可能变的非 常巨大。一般来说,只有这种文件的最后部分对调试有用,这是因为该部分包括了程序运行的 最后信息。为了保留这部分信息, P L / S Q L提供了环形结构的缓冲区。在这种记录方式下,调试 信息不再直接送往跟踪文件,而是先发送到该缓冲区中存放。当该缓冲区装满时,后续的输出 信息将覆盖该缓冲区的开始部分。因此,该缓冲区中将保留跟踪数据的最后一部分。最后,该 缓冲区的内容可以按不同的条件转储到跟踪文件中。该环形缓冲区受到下列三个参数和事件的 控制: • T R A C E _ C I R C U L A R位必须与event 10938事件一起设置。该设置将把跟踪信息送到缓冲区, 而不直接写入跟踪文件。 • 环形缓冲区的容量要使用下面的事件字符串与 event 10940一同设置: 10940 trace name context level buffer_size 其中,b u ff e r _ s i z e是该缓冲区的以K为单位的容量(1 K B = 1 0 2 4字节),默认值是8 K B。该事 件既可以使用A LTER SESSION 语句设置,也可在数据库初始文件中设置。 • 将缓冲区的内容送往跟踪文件的条件由数据库初始参数 _ P L S Q L _ D U M P _ B U F F E R _ E V E N T S指定(请注意开始的下画线)。可以设置的事件如下所示,请注意转储事件由逗 号分隔,所有的事件都要使用大写字母,中间没有空格: 转 储 事 件 说 明 O N _ E X I T 只要P L / S Q L解释程序退出,如调用结束,缓冲区的内容将被转储。 错误号 只要发生指定的错误,缓冲区的内容将转储。该错误必须是一个运行时错误,而不 能是编译错误。 A L L _ E X C E P T I O N S 只要发生错误,缓冲区就进行转储。 下面的列表演示了某些合法的转储参数设置: • _ P L S Q _ D U M P _ B U F F E R _ E V E N T S ="1 , 6 5 0 2 , 1 0 0 1"将在引发ORA_1,ORA_1001, ORA_6502 错误时将缓冲区的内容转储到跟踪文件。 • _ P L S Q L _ D U M P _ B U F F E R _ E V E N T S ="O N _ E X I T, 6 5 0 2"在解释程序退出并且引发 O R A _ 6 5 0 2错误时,转储缓冲区的内容。 • _ P L S Q L _ D U M P _ B U F F E R _ E V E N T S ="A L L _ E X C E P T I O N S , O N _ E X I T"在解释程序退出并 且引发任何错误时,转储缓冲区内容。 3. 伪指令跟踪 该级别的跟踪适用于所有版本的 P L / S Q L。这种跟踪将把所有的P L / S Q L伪指令操作的输出以 及源代码的行号(如果带有行号的话)在其运行时送往跟踪文件。伪指令操作类似于汇编语言 指令,它们是由P L / S Q L编译器生成的机器代码并由 P L / S Q L的运行引擎执行。尽管伪指令本身并 没有记录下来,但这种类型的跟踪对确认 P L / S Q L执行的行信息很有帮助。除此之外,伪指令也 对O r a c l e系统自身调试非常有益。 伪指令跟踪的启动要求把event 10928的级别设置为0级以上,并使用下面的事件字符串语法: 10928 trace name context level 1该事件可以使用ATLER SESSION语句来设置会话级别,或者在数据库初始化文件中设置全 数据库通用。该类跟踪不使用环形缓冲区,因此,所有的跟踪信息都将写入跟踪文件。这将导 致该文件过大,这时要求主机提供足够的硬盘空间。 注意 跟踪文件的最大容量可以使用初始化参数 M A X _ D U M P _ F I L E _ S I Z E来设置。当跟 踪文件达到该容量时,就不在对该文件进行写入操作。 例如,请考虑下面的匿名块的情况: 节选自在线代码PseudoCode.sql ALTER SESSION SET EVENTS '10928 trace name context level 1'; BEGIN CallMe1; CallMe2; CallRaise(100); END; 在SQL *Plus中运行该块将在跟踪文件中记录下列输出信息: *** SESSION ID:(12.4415) 2000-03-03 13:45:11.164 Entry #1 00001: ENTER 44, 0, 1, 1 00009: INFR DS[0]+36 Frame Desc Version = 1, Size = 19 # of locals = 1 TC_SSCALAR: FP+8, d=FP+16, n=FP+40 00014: INSTB 1, STPROC 00018: XCAL 1, 1 Entry #1 EXAMPLE.CALLME1: 00001: ENTER 4, 0, 1, 1 [Line 4] [Line 4] END CallMe1; EXAMPLE.CALLME1: 00009: RET 00023: INSTB 2, STPROC 00027: XCAL 2, 1 Entry #1 EXAMPLE.CALLME2: 00001: ENTER 4, 0, 1, 1 [Line 3] NULL; EXAMPLE.CALLME2: 00009: RET 00032: CVTIN HS+0 =100=, FP+8 00037: INSTB 4, STPROC 00041: MOVA FP+8, FP+4 00046: XCAL 4, 1 Entry #1 EXAMPLE.CALLRAISE: 00001: ENTER 8, 0, 1, 1 第3章 跟踪和调试计计99下载[Line 3] RaiseIt(p_Exception); EXAMPLE.CALLRAISE: 00009: INSTB 2, STPROC EXAMPLE.CALLRAISE: 00013: MOVA AP[4], FP+4 EXAMPLE.CALLRAISE: 00018: XCAL 2, 1 Entry #1 EXAMPLE.RAISEIT: 00001: ENTER 228, 0, 1, 1 EXAMPLE.RAISEIT: 00009: INFR DS[0]+120 Frame Desc Version = 1, Size = 52 # of locals = 6 TC_SSCALAR: FP+16, d=FP+80, n=FP+104 TC_SSCALAR: FP+24, d=FP+108, n=FP+132 TC_SSCALAR: FP+32, d=FP+136, n=FP+160 TC_SSCALAR: FP+40, d=FP+164, n=FP+188 TC_SSCALAR: FP+48, d=FP+192, n=FP+216 TC_VCHAR: FP+60, d=FP+220, n=FP+224, mxl=4000, CS_IMPLICIT [Line 4] IF p_Exception = 0 THEN EXAMPLE.RAISEIT: 00014: CVTIN HS+0 =0=, FP+16 EXAMPLE.RAISEIT: 00019: CMP3N AP[4], FP+16, PC+22 =00041:= EXAMPLE.RAISEIT: 00029: BRNE PC+12 =00041:= [Line 6] ELSIF p_Exception < 0 THEN EXAMPLE.RAISEIT: 00041: CVTIN HS+0 =0=, FP+24 EXAMPLE.RAISEIT: 00046: CMP3N AP[4], FP+24, PC+27 =00073:= EXAMPLE.RAISEIT: 00056: BRGE PC+17 =00073:= [Line 8] ELSIF p_Exception = 1001 THEN EXAMPLE.RAISEIT: 00073: CVTIN HS+8 =1001=, FP+32 EXAMPLE.RAISEIT: 00078: CMP3N AP[4], FP+32, PC+27 =00105:= EXAMPLE.RAISEIT: 00088: BRNE PC+17 =00105:= [Line 10] ELSIF p_Exception = 1403 THEN EXAMPLE.RAISEIT: 00105: CVTIN HS+16 =1403=, FP+40 EXAMPLE.RAISEIT: 00110: CMP3N AP[4], FP+40, PC+27 =00137:= EXAMPLE.RAISEIT: 00120: BRNE PC+17 =00137:= [Line 12] ELSIF p_Exception = 6502 THEN EXAMPLE.RAISEIT: 00137: CVTIN HS+24 =6502=, FP+48 EXAMPLE.RAISEIT: 00142: CMP3N AP[4], FP+48, PC+27 =00169:= EXAMPLE.RAISEIT: 00152: BRNE PC+17 =00169:= [Line 15] RAISE_APPLICATION_ERROR(-20001, 'Exception ' || p_Exception); EXAMPLE.RAISEIT: 00169: CVTNC AP[4], FP+60 EXAMPLE.RAISEIT: 00174: CONC3 HS+32='Exception '=, FP+60, FP+56 EXAMPLE.RAISEIT: 00181: INSTS 2 EXAMPLE.RAISEIT: 00184: INSTB 2, SPEC EXAMPLE.RAISEIT: 00188: MOVA HS+56, FP+4 EXAMPLE.RAISEIT: 00193: MOVA FP[56], FP+8 EXAMPLE.RAISEIT: 00198: MOVA HS+0, FP+12 EXAMPLE.RAISEIT: 00203: ICAL 2, 1, 1, 3 Exception handler: OTHER Line 3-3. PC 9-28. [Line 6] NULL; EXAMPLE.CALLRAISE: 00029: CLREX 100计计第一部分 PL/SQL介绍及开发环境 下载EXAMPLE.CALLRAISE: 00030: BRNCH PC+6 =00036:= EXAMPLE.CALLRAISE: 00036: RET 00051: RET Entry #1 00001: ENTER 212, 0, 1, 1 00009: INFR DS[0]+32 Frame Desc Version = 1, Size = 29 # of locals = 1 _TC_iVCHAR: FP+32, d=FP+196, n=FP+208, mxl=0, CS_IMPLICIT # of bind proxies = 1 _TC_iVCHAR: FP+12, d=FP+52, n=FP+192, ubn(mxl)=128, CS_IMPLICIT 00014: GBVAR SQLT_CHR(1), 1, FP+12 00021: INSTS 4 00024: INSTB 4, SPEC_BODY 00028: MOVA FP+12, FP+4 00033: MOVA FP+32, FP+8 00038: XCAL 4, 1 Entry #1 SYS.DBMS_APPLICATION_INFO: 00001: ENTER 12, 1, 1, 1 [shrink-wrapped frame] SYS.DBMS_APPLICATION_INFO: 00009: MOVA AP[4], FP+4 SYS.DBMS_APPLICATION_INFO: 00014: MOVA AP[8], FP+8 SYS.DBMS_APPLICATION_INFO: 00019: ICAL 0, 7, 1, 2 SYS.DBMS_APPLICATION_INFO: 00028: RET 00043: RET 连同伪指令本身,上面的输出信息显示了被编译入伪指令的源程序行。该跟踪文件描述了 下述事件序列: 1) 匿名块的入口。该事件由指令 ENTER 指示。该块的代码不在显示之列,因为该代码没有 存储在数据库中,这时跟踪信息中将显示 。 2) C a l l M e 1入口和立即返回。由于 C a l l M e 1存储在数据库中,所以,我们在跟踪文件中可以 看到该源代码的行。在源程序行之后,显示了另外一个 ,指示将返回到匿 名块中。 3) C a l l M e 2入口和立即返回。在这里,我们再次看到了源程序行,并返回到匿名块中。 4) 进入C a l l R a i s e的入口和下面的R a i s e I t入口。每次调用都显示源代码。 5) 单步进入 R a i s eIt 的IF THEN 语句,使用相关联的伪指令进行每次测试。测试在调用 R A I S E _ A P P L I C AT I O N _ E R R O R处停止并进入异常处理程序。 6) 从异常处理和匿名块退出。 7) 现在我们看到了 SQL *Plus自身提交了另外一个 P L / S Q L块,由该块调用 D B M S _ A P P L I C AT I O N _ I N F O。由于该包被覆盖,所以其源代码无法显示。但是,我们可以在跟踪信息 中看到伪指令。(并不是所有版本的 SQL *Plus都可以提供该调用,在这种情况下,跟踪文件没 有此项内容。) 第3章 跟踪和调试计计101下载其他伪指令的含义在表 3 - 2中说明。即使不知道每个伪指令的确切含义,然而,我们也可以 从该级别的跟踪信息中了解 P L / S Q L程序的处理过程。 表3-2 PL/SQL伪指令 伪 指 令 功 能 描 述 B R * 以B R开始的(如B R N E)伪指令是分枝指令 C A L L , X C A L , S C A L , I C A L L 调用当前块或块外部的过程。根据所调用的过程的位置,使用不同的伪指令 E N T E R 栈帧入口(如匿名块或过程的入口) G B VA R,S B VA R,G B C R 处理P L / S Q L连接变量 M O V * 以M O V开始的伪指令表示数据从一个位置移动到另一个位置,类似赋值语句 R E T 从栈帧中返回 4. SQL跟踪 通过在会话中使用下面的语句设置 S Q L _ T R A C E为真值(T R U E): ALTER SESSION SET SQL_TRACE = TRUE; 所有S Q L语句的信息和被送往服务器的 P L / S Q L的块都被转储到跟踪文件中。(接着,实用程 序t k p r o f可以用来把这些信息转换为更可读的格式)该类信息还可以通过设置 event 10046来实现, 其事件字符串如下: 10046 trace name context forever,level level_num 可为该事件设置的跟踪级别如下所示: 跟 踪 级 别 说 明 1 与S Q L _ T R A C E相同。 4 1级+连接变量信息。 8 1级+等待信息(可用于观察锁存等待,也可用于检测全表扫描。 1 2 1级+连接变量+等待信息。 与我们在前面作为环形缓冲区跟踪的一部分介绍的连接变量跟踪不同,这类跟踪将显示送 往服务器的所有连接变量的信息,而非只是显示 P L / S Q L块中的信息。除此之外,这种跟踪还可 以显示变量自身的值。 例如,假设我们从SQL *Plus中发布下列P L / S Q L块: 节选自在线代码SQLTrace.sql SQL> -- First set up the variables SQL> VARIABLE v_String1 VARCHAR2(20); SQL> VARIABLE v_String2 VARCHAR2(20); SQL> SQL> BEGIN 2 :v_String1 := 'Hello'; 3 :v_String2 := ' World!'; 4 END; 5 / PL/SQL procedure successfully completed. 102计计第一部分 PL/SQL介绍及开发环境 下载SQL> -- Turn on SQL tracing (including bind variable information) SQL> ALTER SESSION SET EVENTS '10046 trace name context forever, level 4'; Session altered. SQL> BEGIN 2 DBMS_OUTPUT.PUT_LINE(:v_String1 || :v_String2); 3 END; 4 / Hello World! PL/SQL procedure successfully completed. 该块将生成下列跟踪信息。 PARSING IN CURSOR #1 len=61 dep=0 uid=28 oct=47 lid=28 tim=0 hv=2809072883 ad='801d40b4' BEGIN DBMS_OUTPUT.PUT_LINE(:v_String1 || :v_String2); END; END OF STMT PARSE #1:c=0,e=0,p=0,cr=0,cu=0,mis=0,r=0,dep=0,og=4,tim=0 BINDS #1: bind 0: dty=1 mxl=32(20) mal=00 scl=00 pre=00 oacflg=03 oacfl2=10 size=64 offset=0 bfp=010bf728 bln=32 avl=05 flg=05 value="Hello" bind 1: dty=1 mxl=32(20) mal=00 scl=00 pre=00 oacflg=03 oacfl2=10 size=0 offset=32 bfp=010bf748 bln=32 avl=07 flg=01 value=" World!" EXEC #1:c=0,e=0,p=0,cr=0,cu=0,mis=0,r=1,dep=0,og=4,tim=0 ===================== PARSING IN CURSOR #2 len=52 dep=0 uid=28 oct=47 lid=28 tim=0 hv=4201917273 ad='8010fdac' begin dbms_output.get_lines(:lines, :numlines); end; END OF STMT PARSE #2:c=0,e=0,p=0,cr=0,cu=0,mis=0,r=0,dep=0,og=4,tim=0 BINDS #2: bind 0: dty=1 mxl=2000(255) mal=25 scl=00 pre=00 oacflg=43 oacfl2=10 size=2000 offset=0 bfp=010c63a0 bln=255 avl=00 flg=05 bind 1: dty=2 mxl=22(02) mal=00 scl=00 pre=00 oacflg=01 oacfl2=0 size=24 offset=0 bfp=010bf750 bln=22 avl=02 flg=05 value=25 EXEC #2:c=0,e=0,p=0,cr=0,cu=0,mis=0,r=1,dep=0,og=4,tim=0 ===================== PARSING IN CURSOR #1 len=53 dep=0 uid=28 oct=47 lid=28 tim=0 hv=583813323 ad='80355540' begin DBMS_APPLICATION_INFO.SET_MODULE(:1,NULL); end; END OF STMT PARSE #1:c=0,e=0,p=0,cr=0,cu=0,mis=0,r=0,dep=0,og=4,tim=0 BINDS #1: bind 0: dty=1 mxl=128(08) mal=00 scl=00 pre=00 oacflg=21 oacfl2=0 size=128 第3章 跟踪和调试计计103下载offset=0 bfp=010bf6e8 bln=128 avl=08 flg=05 value="SQL*Plus" APPNAME mod='SQL*Plus' mh=3669949024 act='' ah=4029777240 EXEC #1:c=0,e=0,p=0,cr=0,cu=0,mis=0,r=1,dep=0,og=4,tim=0 除了由脚本发布的匿名块外,该跟踪还显示由 SQL *Plus自身提交的两个额外的块(一个用 来检索和显示D B M S _ O U T P U T信息,另一个来检索D B M S _ A P P L I C AT I O N _ I N F O的信息)。这三 个块中都包括连接变量。该类跟踪中显示的连接变量的值如表 3 - 3所示。这里所显示的大多数的 字段是供内部使用的,其中, d t y、m x l和v a l u e字段是最有用的。 表3-3 连接变量跟踪中的字段说明 字 段 说 明 b i n d 只界标位置(从0开始) d t y 只数据类型,该类型对应在头文 件o c i d f n . h中发现的O C I数据类型 m x l 只连接变量的最大长度。需要注意的是,该值可能会比字符串所需的长度要大一些,以 便在共享缓冲池中共享游标数据。所需长度要在圆括号中 m a l 只数据连接的数组长度 s c l 只比例 p r e 只精度 o a c f l g , o a c f l 2 只内部标志 s i z e 只成组连接缓冲区 o ff s e t 只成组连接缓冲区内部偏移量的总长度 b f p 只连接缓冲区的地址 b l n 只连接缓冲区的长度 a v l 只实际变量长度 f l g 只内部标志 v a l u e 只可见的变量值 5. 组合跟踪事件 我们在前几节介绍的所有基于事件的跟踪都在同一个跟踪文件中生成输出信息。因此,如 果设置了一个以上的事件,则每个事件的输出将会交错出现在跟踪文件中。例如,如下所示, 我们可以把调用跟踪和伪指令跟踪结合使用: 节选自在线代码CombinedTracing.sql SQL> -- Set both the pseudo-code and call tracing events. SQL> ALTER SESSION SET EVENTS '10928 trace name context level 1'; Session altered. SQL> ALTER SESSION SET EVENTS '10938 trace name context level 1'; Session altered. SQL> -- Make some random calls. SQL> BEGIN 2 RandomCalls(3); 3 END; 4 / PL/SQL procedure successfully completed. 104计计第一部分 PL/SQL介绍及开发环境 下载下面是跟踪文件中的一部分输出信息。该文件显示了两类跟踪信息。 ------------ PL/SQL TRACE INFORMATION ----------- Levels set : 1 Entry #1 00001: ENTER 44, 0, 1, 1 Trace: ANONYMOUS BLOCK: Stack depth = 1 00009: INFR DS[0]+36 Frame Desc Version = 1, Size = 19 # of locals = 1 TC_SSCALAR: FP+8, d=FP+16, n=FP+40 00014: CVTIN HS+0 =3=, FP+8 00019: INSTB 2, STPROC 00023: MOVA FP+8, FP+4 00028: XCAL 2, 1 Entry #1 EXAMPLE.RANDOMCALLS: 00001: ENTER 312, 0, 1, 1 Trace: PROCEDURE EXAMPLE.RANDOMCALLS: Call to entry at line 4 Stack depth = 2 ... Trace: PACKAGE BODY EXAMPLE.RANDOM: Call to entry at line 3 Stack depth = 3 EXAMPLE.RANDOM: 00009: INFR DS[0]+92 ... [Line 6] IF v_Case = 1 THEN EXAMPLE.RANDOMCALLS: 00072: CVTIN HS+0 =1=, FP+52 EXAMPLE.RANDOMCALLS: 00077: CMP3N FP+12, FP+52, PC+31 =00108:= EXAMPLE.RANDOMCALLS: 00087: BRNE PC+21 =00108:= [Line 7] CallMe1; EXAMPLE.RANDOMCALLS: 00093: INSTB 3, STPROC EXAMPLE.RANDOMCALLS: 00097: XCAL 3, 1 Entry #1 EXAMPLE.CALLME1: 00001: ENTER 4, 0, 1, 1 Trace: PROCEDURE EXAMPLE.CALLME1: Call to entry at line 3 Stack depth = 3 [Line 3] NULL; EXAMPLE.CALLME1: 00009: RET 3.4.2 基于P L / S Q L的跟踪 事件1 0 9 3 8中的调用和异常跟踪可以使用 Oracle8 PL/SQL中的包D B M S _ T R A C E实 现。 该类跟踪功能的进一步完善也将通过该包,而不是通过事件实现。 O r a c l e 8 i第 一版(8 . 1 . 5)中的包D B M S _ T R A C E提供了与基于事件跟踪同类的跟踪功能,而 O r a c l e 8 i第二版 (8 . 1 . 6)则显著地增强了跟踪功能。 1. Oracle8i第1版(8 . 1 . 5)的D B M S _ T R A C E O r a c l e 8 i第1版(8 . 1 . 5)中的D B M S _ T R A C E提供了三个过程,它们是 S E T _ P L S Q L _ T R A C E、 C L E A R _ P L S Q L _ T R A C E和P L S Q L _ T R A C E _ V E R S I O N。下面分别介绍这三个过程: 过程S E T _ P L S Q L _ T R A C E该过程启动当前会话中的跟踪,并开始把转储信息直接地送到 第3章 跟踪和调试计计105下载跟踪文件中。该过程用下面的语句定义: PROCEDURE SET_PLSQL_TRACE(trace_level IN INTEGER); 其中t r a c e _ l e v e l说明要跟踪的对象。对事件跟踪级别的计算方法与我们在前面介绍的 event 10938 相同,也就是计算想要的跟踪功能值的和。下面列出了可以使用的跟踪功能(其中的值与 e v e n t 1 0 9 3 8的值相同)。 跟 踪 名 称 值 说 明 T R A C E _ A L L _ C A L L S 1 跟踪所有的子程序调用。 T R A C E _ E N A B L E _ C A L L S 2 只跟踪允许的调用(使用D E B U G编译的调用)。 T R A C E _ A L L _ E X C E P T I O N 4 跟踪所有的异常。 T R A C E _ E N A B L E D _ E X C E P T I O N S 8 仅跟踪在允许的子程序中引发的异常。 该包的头文件中定义了跟踪名称的常数值,因此上表中的跟踪名称可以直接用在对 S E T _ P L S Q L _ T R A C的调用中。例如,假设我们向数据库提交下面的语句: 节选自在线代码AllCallsExceptions815.sql SQL> -- Enable tracing of all calls and all exceptions. SQL> BEGIN 2 DBMS_TRACE.SET_PLSQL_TRACE( 3 DBMS_TRACE.TRACE_ALL_CALLS + 4 DBMS_TRACE.TRACE_ALL_EXCEPTIONS); 5 END; 6 / PL/SQL procedure successfully completed. SQL> -- Anonymous block which raises some exceptions. SQL> BEGIN 2 CallRaise(1001); 3 RaiseIt(-1); 4 END; 5 / BEGIN * ERROR at line 1: ORA-06510: PL/SQL: unhandled user-defined exception ORA-06512: at "EXAMPLE.RAISEIT", line 7 ORA-06512: at line 3 该块将在跟踪文件中生成下面的输出信息。这些信息与 event 10938跟踪所生成的输出完全 一致。 ------------ PL/SQL TRACE INFORMATION ----------- Levels set : 1 4 Trace: ANONYMOUS BLOCK: Stack depth = 1 Trace: PROCEDURE EXAMPLE.CALLRAISE: Call to entry at line 3 Stack depth = 2 Trace: PROCEDURE EXAMPLE.RAISEIT: Call to entry at line 4 Stack depth = 3 Trace: Pre-defined exception - OER 1001 at line 9 of PROCEDURE EXAMPLE.RAISEIT: 106计计第一部分 PL/SQL介绍及开发环境 下载第3章 跟踪和调试计计107下载 Trace: PROCEDURE EXAMPLE.RAISEIT: Call to entry at line 4 Stack depth = 2 Trace: User defined exception at line 7 of PROCEDURE EXAMPLE.RAISEIT: 过程C L E A R _ P L S Q L _ T R A C E 该过程用来关闭会话的跟踪。该调用后执行的语句将不再 被跟踪。该过程用下面的语句定义 ,不带参数: PROCEDURE CLEAR_PLSQL_TRACE; 过程P L S Q L _ T R A C E _ V E R S I O N该过程返回包D B M S _ T R A C E的主版本和子版本。该包的 头文件中也定义了主版本和子版本的常数。其格式如下: PROCEDURE PLSQL_TRACE_VERSION(major OUT BINARY_INTEGER, minor OUT BINARY_INTEGER); 其中m a j o r是主版本,m i n o r是子版本。该过程在O r a c l e 8 i版本8 . 1 . 5下运行时返回下列输出: 节选自在线代码traceVersion.sql SQL> DECLARE 2 v_MajorVersion BINARY_INTEGER; 3 v_MinorVersion BINARY_INTEGER; 4 BEGIN 5 DBMS_TRACE.PLSQL_TRACE_VERSION(v_MajorVersion, v_MinorVersion); 6 DBMS_OUTPUT.PUT_LINE( 7 'Trace major version: ' || v_MajorVersion); 8 DBMS_OUTPUT.PUT_LINE( 9 'Trace minor version: ' || v_MinorVersion); 10 END; 11 / Trace major version: 1 Trace minor version: 0 PL/SQL procedure successfully completed. 2. Oracle8i版本8 . 1 . 6的D B M S _ T R A C E 与O r a c l e 8 i版本 8 . 1 . 5的D B M S _ T R A C E和基于事件的跟踪不同, O r a c l e 8 i版本 8 . 1 . 6的 D B M S _ T R A C E生成的输出信息是存储在数据库表中的,而不是转储在文件中。这种记录方式提供 了更为可靠的存储方案。该包中提供的跟踪事件类型也要多一些,并且还有额外的子程序可使用。 跟踪事件 O r a c l e 8 i版本8 . 1 . 6提供的可跟踪事件如表 3 - 4所示。这些事件中包括了 O r a c l e 8 i版 本8 . 1 . 5中所有的事件。这些事件的使用与 S E T _ P L S Q L _ T R A C E中的使用方式完全一样。 表3-4 Oracle8i版本8 . 1 . 6的D B M S _ T R A C E提供的事件 跟 踪 事 件 值 说 明 T R A C E _ A L L _ C A L L S 1 跟踪所有的子程序调用 T R A C E _ E N A B L E D _ C A L L S 2 仅跟踪允许的调用(使用D E B U G编译的调用) T R A C E _ A L L _ E X C E P T I O N S 4 跟踪所有的异常 T R A C E _ E N A B L E D _ E X C E P T I O N S 8 仅跟踪在允许的子程序中引发的异常 T R A C E _ A L L _ S Q L 3 2 跟踪所有执行的 S Q L语句 T R A C E _ E N A B L E D _ S Q L 6 4 跟踪允许的子程序中执行的 S Q L语句 T R A C E _ A L L _ L I N E S 1 2 8 跟踪所有代码行(包括调用和从过程返回的代码) T R A C E _ E N A B L E D _ L I N E S 2 5 6 跟踪允许子程序中的代码行(续) 跟 踪 事 件 值 说 明 T R A C E _ S TO P 1 6 3 8 4 停止跟踪 T R A C E _ PA U S E 4 0 9 6 暂停跟踪 T R A C E _ R E S U M E 8 1 9 2 继续跟踪 T R A C E _ L I M I T 1 6 限制跟踪数量 暂停和继续跟踪 使用过程PA U S E _ P L S Q L _ T R A C E可以暂停跟踪数据的收集,如果在其后 调用过程R E S U M E _ P L S Q L _ T R A C E的话,可恢复跟踪功能。这两个命令都没有参数。 带有t r a c e _ l e v e l参数的S E T _ P L S Q L _ T R A C E命令等价于T R A C E _ PA U S E命令,也可以暂停跟 踪,类似地,也可以通过设置 t r a c e _ l e v e l来恢复跟踪,等价于T R A C E _ R E S U M E。 限制跟踪数据 类似基于事件跟踪使用的环形缓冲区,基于 P L / S Q L的跟踪也可以保留最近 的跟踪记录。实现这种功能的方法有两个。第一个是使用过程 L I M I T _ P L S Q L _ T R A C E,该过程 定义如下: PROCEDURE LIMIT_PLSQL_TRACE(limit IN BINARY_INTEGER:=8192); 由参数l i m i t指定的记录将被保留下来(最近的记录),在此以前的记录将被覆盖。需要指出的是, 系统保留的记录数是 l i m i t指定的记录数的近似值,这是因为系统并不对每个跟踪的发生进行核 对。但是,超出指定记录数量的记录最多不会超出 1 0 0 0个。 第二个方法是在S E T _ P L S Q L _ T R A C E中将T R A C E _ L I M I T作为t r a c e _ l e v e l使用,并设置e v e n t 1 0 9 4 0 (该命令用来限制基于事件跟踪使用的环行缓冲区的容量 )。在这种情况下,跟踪限制将被 设置为1 0 2 3×event 10940设置的事件级别。 跟踪注释 每个跟踪操作都可以带有一个与其关联的注释,该功能可通过过程 C O M M E N T _ P L S Q L _ T R A C E设置,其格式如下: PROCEDURE COMMENT_PLSQL_TRACE(comment IN VARCHAR2); 其中,c o m m e n t是指定的注释,其长度在2 0 4 7个字符内。 版本核对 如果数据库版本已经升级或由于没有载入包 D B M S _ T R A C E的相应版本而降级的 话,用户程序就可能出现版本不兼容的问题。数据库版本可以由函数 I N T E R N A L _ V E R S I A O N _ C H E C K来核对。该函数的定义如下: FUNCTION INTERNAL_VERSION_CHECK RETURN BINARY_INTEGER; 如果版本匹配的话,该函数的返回值为 0。如果不匹配,则该函数返回 1。这时就要重新载 入包D B M S _ T R A C E并重新运行程序。 操作号 每当开始一个跟踪操作时,系统就为该操作生成一个唯一的操作号。该操作号可由 函数G E T _ P L S Q L _ T R A C E _ R U N N U M B E R返回。其定义如下: FUNCTION GET_PLSQL_TRACE_RUNNUMBER RETURN BINARY_INTEGER; 跟踪表 保存跟踪数据的表有两个,它们是 p l s q l _ t r a c e _ r u n s和p l s q l _ t r a c e _ e v e n t s。这两个表 是由脚本t r a c e t a b . s q l生成的,在U n i x操作系统中,存储在$ O R A C L E _ H O M E / r d b m s / a d m i n中。这 108计计第一部分 PL/SQL介绍及开发环境 下载两个表都隶属于S Y S,因此对它们的访问必须经过授权。 有关每个跟踪操作的一般信息存储在表 p l s q l _ t r a c e _ r u n s中,该表结构如下所示: 列 数 据 类 型 说 明 r u n i d N U M B E R 唯一的操作I D。 r u n _ d a t a D AT E 操作开始的时间。 r u n _ o w n e r VA R C H A R 2 ( 3 1 ) 已开始运行的数量。 r u n _ c o m m e n t VA R C H A R 2 ( 2 0 4 7 ) 用户提供的注释。 r u n _ c o m m e n t l VA R C H A R 2 ( 2 0 4 7 ) 附加的注释。注意, C O M M E N T _ P L S Q L _ T R A C E将只修改run_ c o m m e n t ,因此,该列必须由手工修改。 r u n _ e n d D AT E 操作结束的时间。 r u n _ f l a g s VA R C H A R 2 ( 2 0 4 7 ) 操作使用的标志。 r e l a t e d _ r u n N U M B E R 用于客户或服务器端相关操作。 r u n _ s y s t e m _ i n f o VA R C H A R 2 ( 2 0 4 7 ) 没有启用。 s p a r e l VA R C H A R 2 ( 2 5 6 ) 没有启用。 表p l s q l _ t r a c e _ e v e n t s记录了详细的跟踪数据,该表的每一行对应一个跟踪事件。其结构如 下: 列 数 据 类 型 说 明 r u n i d N U M B E R 运行I D。 e v e n t _ s e q N U M B E R 事件I D。 e v e n t _ t i m e D AT E 该事件的时间。 r e l a t e d _ e v e n t N U M B E R 相关事件的I D。 e v e n t _ k i n d VA R C H A R 2 ( 3 1 ) 事件类型。 e v e n t _ u n i t _ d b l i n k VA R C H A R 2 ( 3 1 ) 当前库单元的数据库连接。 e v e n t _ u n i t _ o w n e r VA R C H A R 2 ( 3 1 ) 当前库单元的拥有者。 e v e n t _ u n i t VA R C H A R 2 ( 3 1 ) 当前库单元的名称。 e v e n t _ u n i t _ k i n d VA R C H A R 2 ( 3 1 ) 当前库单元的类型。 e v e n t _ l i n e N U M B E R 当前行。 e v e n t _ p r o c _ n a m e VA R C H A R 2 ( 3 1 ) 当前存在的过程名。 s t a c k _ d e p t h N U M B E R 当前栈深度。 p r o c _ n a m e VA R C H A R 2 ( 3 1 ) 被调用过程的名称。 p r o c _ d b l i n k VA R C H A R 2 ( 3 1 ) 被调用过程的数据库连接。 p r o c _ o w n e r VA R C H A R 2 ( 3 1 ) 被调用过程的拥有者。 p r o c _ u n i t VA R C H A R 2 ( 3 1 ) 调用的库单元。 p r o c _ u n i t _ k i n d VA R C H A R 2 ( 3 1 ) 调用过程的类型。 p r o c _ l i n e N U M B E R 调用过程的行。 p r o c _ p a r a m s VA R C H A R 2(2 0 4 7) 过程参数。 i c d _ i n d e x N U M B E R 调用P L / S Q L内部程序的I C D索引。 u s e r _ e x c p N U M B E R 用户定义的异常号。 e x c p N U M B E R 预定义异常号。 e v e n t _ c o m m e n t VA R C H A R 2(2 0 4 7) 事件的注释。 跟踪事件的种类在列 e v e n t _ k i n d给出,该列的值的含义在下面的表中说明。该表中的符号名 称是定义在包D B M S _ T R A C E头文件中的常数。 第3章 跟踪和调试计计109下载符 号 名 称 值 说 明 P L S Q L _ T R A C E _ S TA RT 3 8 开始跟踪。 P L S Q L _ T R A C E _ S TO P 3 9 结束跟踪。 P L S Q L _ T R A C E _ S E T _ F L A G S 4 0 跟踪选项变更。 P L S Q L _ T R A C E _ PA U S E 4 1 跟踪暂停。 P L S Q L _ T R A C E _ R E S U M E 4 2 恢复跟踪。 P L S Q L _ T R A C E _ E N T E R _ V M 4 3 运行引擎入口。 P L S Q L _ T R A C E _ E X I T _ V M 4 4 从运行引擎退出。 P L S Q L _ T R A C E _ B E G I N _ C A L L 4 5 调用独立过程。 P L S Q L _ T R A C E _ E L A B _ S P E C 4 6 调用包说明。 P L S Q L _ T R A C E _ E L A B _ B O D Y 4 7 调用包体。 P L S Q L _ T R A C E _ I C D 4 8 调用内部P L / S Q L程序。 P L S Q L _ T R A C E _ P R C 4 9 调用远程过程。 P L S Q L _ T R A C E _ E N D _ C A L L 5 0 结束调用,返回调用块。 P L S Q L _ T R A C E _ N E W _ L I N E 5 1 P L / S Q L代码新行。 P L S Q L _ T R A C E _ E X C P _ R A I S E D 5 2 引发异常。 P L S Q L _ T R A C E _ E X C P _ H A N D L E D 5 3 异常处理。 P L S Q L _ T R A C E _ S Q L 5 4 运行S Q L语句。 P L S Q L _ T R A C E _ B I N D 5 5 处理连接变量。 P L S Q L _ T R A C E _ U S E R 5 6 用户定义的跟踪事件。 P L S Q L _ T R A C E _ N O D E B U G 5 7 模块未用D E B U G编译,跳过该事件。 案例 有关D B M S _ T R A C E的案例,请访问专为本书设置的 We b站点w w w. o s b o r n e . c o m。 3.4.3 基于P L / S Q L的配置 连同我们在上一节讨论的基于 P L / S Q L的跟踪一起,O r a c l e 8 i第二 版(8 . 1 . 6)通过包 D B M S _ P R O F I L E R提供了剖析程序(P r o f i l e r)。跟踪功能,如我们在上面所看到的, 提供了P L / S Q L程序运行期间所发生事件的有关信息,如对过程的调用,或所引发的异常等信息。 而剖析程序则是用于记录程序运行时间的工具。借助于该工具,我们可以收集有关 P L / S Q L代码 的每一行运行所用的最大,最小,和全部时间。这类时间信息还可以用于累计库单元一级或应 用级的运行所用时间。 注意 我们在本章前面介绍的几种开发工具,如 Rapid SQL、SQL Navigator以及S Q L P r o g r a m m e r,都提供了剖析程序的图形界面。有关详细资料,请看在线文档。 1. DBMS_PROFILER子程序 D B M S _ P R O F I L E R中提供的子程序如表 3 - 5所示。其有关功能在下面几节将详细说明。每个 可用的子程序即可以作为函数,也可以作为过程使用。作为函数使用时,返回的 B I N A RY _ I N T E G E R的值用来说明成功或失败,而过程将在失败时引发异常。这些子程序的返回值在该包 的头文件中定义。下面是部分返回码的说明。 返 回 码 值 说 明 S U C C E S S 0 成功返回。 E R R O R _ PA R A M 1 调用参数不对。 110计计第一部分 PL/SQL介绍及开发环境 下载E R R O R _ I O 2 写入P r o f i l e r表有错。 E R R O R _ V E R S I O N -1 包版本与数据库版本不符。 表3-5 DBMS_PROFILER子程序 子 程 序 说 明 S TA RT _ P R O F I L E R 启动P r o f i l e r运行 S TO P _ P R O F I L E R 停止P r o f i l e r运行并把数据写入表 PA U S E _ P R O F I L E R 暂停收集数据 R E S U M E _ P R O F I L E R 暂停后继续 F L U S H _ D ATA 把收集的数据写入表中 G E T _ V E R S I O N 返回包P r o f i l e r的版本 I N T E R N A L _ V E R S I O N _ C H E C K 核对软件与数据库版本是否匹配 R O L L U P _ U N I T 为指定的库单元累计数据 R O L L U P _ R U N 为全部运行的程序累计数据 类似于O r a c l e 8 i的8 . 1 . 6版的D B M S _ T R A C E,剖析程序数据被写入数据库表中存放。我们将 在下面“D B M S _ P R O F I L E R表”中介绍这些表的结构。 S TA RT _ P R O F I L E R 该程序运行后开始收集 P r o f i l i n g数据并返回当前运行号。该程序的定 义如下: FUNCTION START_PROFILER( run_commentIN VARCHAR2 := SYSDATE, run_comment1IN VARCHAR2 := ", run_number OUT BINARY_INTEGER) RETURN BINARY_INTEGER; PROCEDURE START_PROFILER( run_commentIN VARCHAR2 := SYSDATE, run_comment1IN VARCHAR2 := ", run_number OUT BINARY_INTEGER); FUNCTION START_PROFILER( run_commentIN VARCHAR2 := SYSDATE, run_comment1IN VARCHAR2 := ") RETURN BINARY_INTEGER; PROCEDURE START_PROFILER( run_commentIN VARCHAR2 := SYSDATE, run_comment1IN VARCHAR2 := "); 其中的参数 r u n _ c o m m e n t和r u n _ c o m m e n t 1可以用来说明该剖析程序的运行并被存储在该 P r o f i l e r表中。当前运行号将在 r u n _ n u m b e r中返回。需要注意的是,如果你使用了不返回运行号 的版本,则确认运行号的唯一方法是去查询该表。 S TO P _ P R O F I L E R 该程序将停止收集P r o f i l e r数据,并把已收集的数据写入表中。该程序定 义如下: FUNCTION STOP_PROFILER RETURN BINARY_INTEGER; PROCEDURE STOP_PROFILER 该程序没有参数。 P A U S E _ P R O F I L E R该程序将临时停止收集 P r o f i l e r数据。这时,它不清除缓冲区。定义 如下: FUNCTION PAUSE_PROFILER RETURN BINARY_INTEGER; 第3章 跟踪和调试计计111下载PROCEDURE PAUSE_PROFILER; R E S U M E _ P R O F I L E R 该程序将在暂停后重新开始收集 P r o f i l e r数据。定义如下: FUNCTION RESUME_PROFILER RETURN BINARY_INTEGER; PROCEDURE RESUME_PROFILER; F L U S H _ D ATA 该程序将把收集到的数据写入 P r o f i l e r表中。除非使用 F L U S H _ D ATA或 S TO P _ P R O F I L E R存储数据,否则数据不予保存。该程序定义如下: FUNCTION FLUSH_DATA RETURN BINARY_INTEGER; PROCEDURE FLUSH_DATA; G E T _ V E R S I O N 该过程将返回包P r o f i l e r的主版本和子版本。主版本和子版本的定义在该 包的头文件中。该过程定义如下: PROCEDURE GET_VERSION(major OUT BINARY_INTEGER, minor OUT BINARY_INTEGER); 主版本将在参数m a j o r中返回,子版本在m i n o r中返回。 I N T E R N A L _ V E R S I O N _ C H E C K 类似于包D B M S _ T R A C E ,如果数据库版本已经升级,或 因没有载入相应版本的包 D B M S _ P R O F I L E R而降级的话,程序运行时将会引发错误。版本匹配 工作可由该函数实现,其定义如下: FUNCTION INTERNAL_VERSION_CHECK RETURN BINARY_INTEGER; 如果版本匹配的话,则其返回值为 0 ,否则为1。如果该函数返回 1 ,用户应再次载入 D B M S _ P R O F I L E R并重新运行程序。 R O L L U P _ U N I T 该过程将计算运行某个程序单元的总运行时间。该过程的定义如下: PROCEDURE ROLLUP_UNIT(run IN NUMBER,unit IN NUMBER); 其中,参数r u n是运行号,u n i t是单元号。每个程序单元都被赋予一个唯一的运行号,该运 行号也可以通过查询P r o f i l e r表获得。 R O L L U P _ R U N 该过程将计算给定运行的总执行时间。其定义如下: PROCEDURE ROLLUP_RUN(run IN NUMBER); 其中,参数r u n是运行号。 2. DBMS_PROFILER 表 P r o f i l e r数据存储在三个数据库表中,这些表可以用文件 p r o f t a b . s q l创建。在U n i x系统下,该 文件存储在$ O R A C L E _ H O M E / r d b m s / a d m i n中。下面将描述这些表。 表P L S Q L _ P R O F I L E R _ R U N S 该表存储每个P r o f i l e r操作的信息。其结构如下: 列 数 据 类 型 说 明 r u n i d N U M B E R 运行的唯一I D。 r e l a t e d _ r u n N U M B E R 相关运行的I D。该I D用于客户和服务器的相互关联的运行。 r u n _ o w n e r VA R C H A R 2 ( 3 2 ) 启动该运行的用户。 r u n _ d a t e D AT E 运行的开始时间。 r u n _ c o m m e n t VA R C H A R 2 ( 2 0 4 7 ) 用户为该运行指定注释,该注释传递到 S TA RT _ P R O F I L E R中。 r u n _ t o t a l _ t i m e N U M B E R 该运行的总执行时间。 112计计第一部分 PL/SQL介绍及开发环境 下载r u n _ s y s t e m _ i n f o VA R C H A R 2 ( 2 0 4 7 ) 未启用。 r u n _ c o m m e n t 1 VA R C H A R 2 ( 2 0 4 7 ) 附加的注释,该注释也传递到 S TA RT _ P R O F I L E R中。 s p a r e 1 VA R C H A R 2 ( 2 5 6 ) 未启用。 表P L S Q L _ P R O F I L E R _ U N I T S 该表存储了运行期间每个程序单元的有关信息 ,该表的结构 如下: 列 数 据 类 型 说 明 r u n i d N U M B E R 运行的唯一I D。 u n i t _ n u m b e r N U M B E R 程序单元的唯一I D。 u n i t _ t y p e VA R C H A R 2 ( 3 2 ) 程序单元的类型。 u n i t _ o w n e r VA R C H A R 2 ( 3 2 ) 程序单元的所有者。 u n i t _ n a m e VA R C H A R 2 ( 3 2 ) 程序单元的名称。 u n i t _ t i m e s t a m p D AT E 程序单元的时间戳,可用来检测变更。 t o t a l _ t i m e N U M B E R 程序单元的运行所用的总时间。 s p a r e 1 N U M B E R 未启用。 s p a r e 2 N U M B E R 未启用。 表P L S Q L _ P R O F I L E R _ D ATA 该表提供了最低级别的P r o f i l i n g信息,也就是说,只提供每行 P L / S Q L代码的有关信息。该表的结构如下: 列 数 据 类 型 说 明 r u n i d N U M B E R 唯一的运行I D。 u n i t _ n u m b e r N U M B E R 唯一的单元I D。 l i n e # N U M B E R 单元中的行号。 t o t a l _ o c c u r N U M B E R 运行中该行运行的次数。 t o t a l _ t i m e N U M B E R 该行所用的总运行时间。 m i n _ t i m e N U M B E R 该行运行所需的最小时间。 m a x _ t i m e N U M B E R 该行运行所需的最大时间。 s p a r e 1 N U M B E R 未用。 s p a r e 2 N U M B E R 未用。 s p a r e 3 N U M B E R 未用。 s p a r e 4 N U M B E R 未用。 案例 有关D B M S _ P R O F I L E R的案例,请访问为本书专门提供的 We b站点w w w. o s b o r n e . c o m . 3.5 小结 我们在本章分析了调试 P L / S Q L代码的不同技术,其涉及范围从基于字符的调试技术如 D B M S _ O U TO U T到插入调试表的全图形 G U I调试器。使用那种调试方法取决于程序所在的环境 和要求。我们在本章除了逐个介绍了每种调试方法外,还讨论了七个常见的 P L / S Q L错误以及避 免这些错误的方法。我们在本章的最后几节讨论了 P L / S Q L不同版本提供的跟踪和配置工具。 第3章 跟踪和调试计计113下载下载 第4章 创建子程序和包 P L / S Q L块主要有两种类型,即命名块和匿名块。匿名块(以 D E C L A R E或B E G I N开始)每 次使用时都要进行编译,除此之外,该类块不在数据库中存储并且不能直接从其他的 P L / S Q L块 中调用。我们在本章以及下面两章中介绍的块结构,如过程,函数,包和触发器都属于命名块, 这类构造没有匿名块的限制,它们可以存储在数据库中并在适当的时候运行。我们将在本章探 讨创建过程,函数,以及包的语法。在第 5章,我们将介绍如何使用这些块结构和这些构造的实 现,而在第6章集中介绍数据库触发器的功能。 4.1 过程和函数 P L / S Q L的过程和函数的运行方式非常类似于其他 3 G L (第3代程序设计语言)使用的过程和函 数。它们之间具有许多共同的特征属性。总起来说,过程和函数统称为子程序。下面的代码就 是一个在数据库中创建一个过程的例子: 节选自在线代码AddNewStudent.sql CREATE OR REPLACE PROCEDURE AddNewStudent ( p_FirstName students.first_name%TYPE, p_LastName students.last_name%TYPE, p_Major students.major%TYPE) AS BEGIN -- Insert a new row in the students table. Use -- student_sequence to generate the new student ID, and -- 0 for current_credits. INSERT INTO students (ID, first_name, last_name, major, current_credits) VALUES (student_sequence.nextval, p_FirstName, p_LastName, p_Major, 0); END AddNewStudent; 注意 表s t u d e n t s以及我们在第 1章中描述的其他关系表,都可以用在线文档中的脚本 r e l Ta b l e s . s q l来创建。 一旦创建了该过程,我们就可以从其他的 P L / S Q L块中对其进行调用: 节选自在线代码AddNewStudent.sql BEGIN AddNewStudent('Zelda', 'Zudnik', 'Computer Science'); 第二部分 非对象功能END; 从该例中,我们可以总结如下要点: • 过程A d d N e w S t u d e n t首先是用语句C R E ATE OR REPLACE PROCEDURE创建的。当该过 程创建后,首先对其进行编译,接着将其按编译后的格式存储在数据库中。这种编译后生 成的代码可以从另外一个 P L / S Q L块中运行。(该过程的源码也可以存储。有关过程源码的 详细介绍,请参阅5 . 1 . 1节。) • 当调用该过程时,可以向该过程传递参数。在上面的例子中,新生的名和姓,以及专业都 在运行时作为参数传递给该过程。在该过程内部,参数 p _ F i r s t N a m e将具有值‘ Z e l d a’, p _ L a s t N a m e的值是‘Z u d n i k’,而p _ M a j o r的值为‘Computer Science’,这些字符串都是在 调用时传递给该过程的。 • 过程调用本身也是一个 P L / S Q L语句。过程不能作为表达式的一部分进行调用。当过程被 调用时,系统就把控制交给该过程的第一个可执行语句。当过程结束时,控制就将返回到 调用语句的下一个语句。在这点上,P L / S Q L过程非常类似于其他3 G L语言的过程调用方式。 函数可以作为表达式的一部分进行调用,我们将在本节的稍后部分介绍函数的特点。 • 过程也是P L / S Q L块,它由声明部分,可执行代码部分和异常处理部分组成。对于匿名块, 只需要可执行部分。上面的过程 A d d N e w S t u d e n t只有可执行部分。 4.1.1 创建子程序 类似于数据字典中的其他类型的对象,子程序是使用 C R E AT E语句创建的。如过程是用语句 C R E ATE PROCEDURE创建的,函数的创建语句则是 C R E ATE FUNCTION。下面我们分别介绍 这些创建语句。 1. 创建过程 创建过程语句的语法如下所示: CREATE [OR REPLACE] PROCEDURE procedure_name [ ( argument[{IN | OUT | IN OUT}] type, ... argument[{IN | OUT | IN OUT}] type) ] {IS | AS} procedure_body 其中p r o c e d u r e _ n a m e是要创建的过程名,a rg u m e n t是过程的参数名,t y p e是关联参数的类型, p r o e d u r e _ b o d y是构成该过程代码的 P L / S Q L块。有关过程和函数的参数和关键字 I N , O U T,和I N O U T的含义,请参见4 . 1 . 3节的内容。 O r a c l e 8 i给每个过程参数都增加了一个附加选择项关键字 N O C O P Y。有关该关键字 的讨论请参见4 . 1 . 3节中的“按引用或按值传递参数”。 为了修改过程的代码,首先必须将该过程撤消,然后再重建。由于这种操作已经是开发过 程的标准方式,所以关键字 OR REPLACE 允许将撤消和重建这两步操作合并为一个操作。如果 过程存在,首先撤消该过程,而不给出任何警告提示。(可以使用命令DROP PROCEDURE来撤 消一个过程,该命令在本章 4 . 1 . 2节中介绍。)如果该过程已经不存在,就可以直接创建它。如果 116计计第二部分 非对象功能 下载该过程已存在而没有关键字 OR REPLACE,则C R E AT E语句将返回一条 O r a c l e错误信息“O R A - 9 5 5 :该名称已被当前对象使用”。 和其他的C R E AT E语句一样,创建过程是一种 D D L操作,因此,在过程创建前和创建后,都 要执行一条隐式的C O M M I T命令。这种操作可以通过使用关键字 IS 或A S来实现,这两个关键字 是等价的。 过程体 过程体是一种带有声明部分,可执行语句部分和异常部分的 P L / S Q L块。该声明部 分是位于关键字IS 或AS 和关键字B E G I N之间的语句。可执行部分(该部分是必须要有的)是位 于关键字B E G I N和E X C E P T I O N之间的语句。最后,异常部分位于关键字 E X C E P T I O N和关键字 E N D之间的语句。 提示 在过程和函数中没有使用关键字 D E C L A R E。取而代之的是关键字I S或A S。这种 语法风格是P L / S Q L从A d a语言中继承下来的。 综上所述,过程的结构应具有下面所示的特征: CREATE OR REPLACE PROCEDURE procedure_name [ parameter_list] AS /* Declarative section is here */ BEGIN /* Executable section is here */ EXCEPTION /* Exception section is here */ END [ procedure_name]; 过程名可以写在过程声明中最后一个 E N D语句之后。如果在该E N D语句之后有标识符的话, 该标识符一定要与该过程名匹配。 提示 在过程的最后一个E N D语句的后面写上过程名是一种良好的编程风格,这样做的 好处是强调了E N D语句和C R E AT E语句的匹配,同时,也使 P L / S Q L编译程序能够尽早 地提示B E G I N G - E N D不匹配错误。 2. 创建函数 函数类似于过程。两者都带有参数,而参数具有模式(参数和模式在4 . 1 . 3节介绍),两者都 不同于带有声明、可执行以及异常处理部分的 P L / S Q L块。两者都可以存储在数据库中或在块中 声明。(不能存储在数据库中的过程和函数在5 . 1节讨论。)两者不同的是,过程调用本身是一个 P L / S Q L语句,而函数调用是作为表达式的一部分执行的。例如,下面的函数在指定的班级有百 分之8 0以上满员时返回真值T R U E,否则返回假值FA L S E: 节选自在线代码AlmostFull.sql CREATE OR REPLACE FUNCTION AlmostFull ( p_Department classes.department%TYPE, p_Course classes.course%TYPE) RETURN BOOLEAN IS v_CurrentStudents NUMBER; v_MaxStudents NUMBER; v_ReturnValue BOOLEAN; 第4章 创建子程序和包计计117下载v_FullPercent CONSTANT NUMBER := 80; BEGIN -- Get the current and maximum students for the requested -- course. SELECT current_students, max_students INTO v_CurrentStudents, v_MaxStudents FROM classes WHERE department = p_Department AND course = p_Course; -- If the class is more full than the percentage given by -- v_FullPercent, return TRUE. Otherwise, return FALSE. IF (v_CurrentStudents / v_MaxStudents * 100) >= v_FullPercent THEN v_ReturnValue := TRUE; ELSE v_ReturnValue := FALSE; END IF; RETURN v_ReturnValue; END AlmostFull; 函数A l m o s t F u l l返回的是逻辑值。该函数可以从下面的 P L / S Q L块中调用。值得注意的是, 该调用不是一个独立的语句,而只是循环中作为 I F语句表达式的一项。 节选自在线代码c a l l F u n c t i o n . s q l SQL> DECLARE 2 CURSOR c_Classes IS 3 SELECT department, course 4 FROM classes; 5 BEGIN 6 FOR v_ClassRecord IN c_Classes LOOP 7 -- Output all the classes which don't have very much room 8 IF AlmostFull(v_ClassRecord.department, 9 v_ClassRecord.course) THEN 10 DBMS_OUTPUT.PUT_LINE( 11 v_ClassRecord.department || ' ' || 12 v_ClassRecord.course || ' is almost full!'); 13 END IF; 14 END LOOP; 15 END; 16 / MUS 410 is almost full! PL/SQL procedure successfully completed. 函数的语法 创建存储函数的语法非常类似于过程的语法。其定义如下: CREATE [OR REPLACE] FUNCTION function_name [( argument[{IN | OUT | IN OUT}] type, ... argument[{IN | OUT | IN OUT}] type)] 118计计第二部分 非对象功能 下载RETURN return_type{IS | AS} function_body 其中f u n c t i o n _ n a m e是函数的名称,参数a rg u m e n t和t y p e的含义与过程相同, r e t u r n _ t y p e是函 数返回值的类型,f u n c t i o n _ b o d y是包括函数体的P L / S Q L块。 与过程的参数类似,函数的参数表是可选的,并且函数声明部分和函数调用都没有使用括 弧。然而,由于函数调用是表达式的一部分,所以函数返回类型是必须要有的。函数的类型可 以用来确定包含函数调用的表达式的类型。 像过程一样, O r a c l e 8 i为函数的参数提供了关键字 N O C O P Y。本章“按引用和按值 传递参数”一节将介绍该关键字。 返回语句 在函数体内,返回语句用来把控制返回到到调用环境中。该语句的通用语法如 下: RETURN expression; 其中e x p r e s s i o n是返回值。当该语句执行时,如果表达式的类型与定义不符,该表达式将被 转换为函数定义子句R E T U R N中指定的类型。同时,控制将立即返回到调用环境。 尽管函数每次只有一个返回语句被执行,但是,函数中可以有一个以上的返回语句。如果 函数结束时还没有遇到返回语句,就会发生错误。下面的例子介绍了一个函数中有多个返回语 句的情况。尽管该函数中有五个不同的返回语句,但只有一个被执行,而执行哪个返回语句则 取决于变量p _ D e p a r t m e n t和p _ C o u r s e的取值情况。 节选自在线代码ClassInfo.sql CREATE OR REPLACE FUNCTION ClassInfo( /* Returns 'Full' if the class is completely full, 'Some Room' if the class is over 80% full, 'More Room' if the class is over 60% full, 'Lots of Room' if the class is less than 60% full, and 'Empty' if there are no students registered. */ p_Department classes.department%TYPE, p_Course classes.course%TYPE) RETURN VARCHAR2 IS v_CurrentStudents NUMBER; v_MaxStudents NUMBER; v_PercentFull NUMBER; BEGIN -- Get the current and maximum students for the requested -- course. SELECT current_students, max_students INTO v_CurrentStudents, v_MaxStudents FROM classes WHERE department = p_Department AND course = p_Course; -- Calculate the current percentage. 第4章 创建子程序和包计计119下载v_PercentFull := v_CurrentStudents / v_MaxStudents * 100; IF v_PercentFull = 100 THEN RETURN 'Full'; ELSIF v_PercentFull > 80 THEN RETURN 'Some Room'; ELSIF v_PercentFull > 60 THEN RETURN 'More Room'; ELSIF v_PercentFull > 0 THEN RETURN 'Lots of Room'; ELSE RETURN 'Empty'; END IF; END ClassInfo; 当在函数中使用返回语句时,返回语句必须带有表达式。同样,返回语句也可以用在过程 中。但在过程中使用的返回语句没有参数,它只是立即把控制返回到调用环境中。这时,声明 为O U T或IN OUT的形式参数的当前值将被传递回对应的实参,程序从调用过程语句的下一行继 续执行(本章4 . 1 . 3节将介绍参数的详细内容。) 4.1.2 过程和函数的撤消 与表的撤消相类似,过程和函数也可以撤消。撤消操作是将过程或函数从数据字典中删除。 撤消过程的语法如下: DROP PROCEDURE procedure_name; 撤消函数的语法是: DROP FUNCTION function_name; 其中p r o c e d u r e _ n a m e是现行的过程名, f u n c t i o n _ n a m e则是现行函数名。例如,下面的语句 将撤消过程A d d N e w S t u d e n t : DROP PROCEDURE AddNewStudent; 如果要撤消的对象是函数的话,就必须使用语句 DROP FUNCITO N ,如果是过程,就使用 DROP PROCEDURE。象语句C R E AT E一样,D R O P语句也是D D L命令,因此在该语句执行前后 都要隐式地执行 C O M M I T命令。如果指定的子程序不存在的话,则 D R O P语句将引发错误 “ORA-4043: 对象不存在.”。 4.1.3 子程序参数 与其他类型的3 G L语言一样,我们可以创建带参数的过程和函数。这些参数可以是不同的模 式,并可以按值或按引用传递。下面我们来介绍这类参数的特性。 1. 参数模式 以上面使用的过程A d d N e w S t u d e n t为例,我们可以从下面的P L / S Q L匿名块中调用该过程: 节选自在线代码callANS.sql 120计计第二部分 非对象功能 下载DECLARE -- Variables describing the new student v_NewFirstName students.first_name%TYPE := 'Cynthia'; v_NewLastName students.last_name%TYPE := 'Camino'; v_NewMajor students.major%TYPE := 'History'; BEGIN -- Add Cynthia Camino to the database. AddNewStudent(v_NewFirstName, v_NewLastName, v_NewMajor); END; 该块声明的变量( v _ N e w F i r s t N a m e , v _ N e w L a s t N a m e , v _ N e w M a j o r)作为参数传递给过程 A d d N e w S t u d e n t。在这种上下文中,我们把这些参数称为实参,而在过程声明部分中的参数 (p _ F i r s t N a m e,p _ L a s t N a m e , p _ M a j o r)则称为形参。实参包含了过程被调用时传递过来的值, 并且实参还接收过程返回时的结果(与返回模式有关)。实参的值是过程中将要使用的值。当调 用过程时,形参被赋予实参的值。对于过程内部而言,实参是由形参引用的。当过程结束时, 实参被赋予形参的值。上述的赋值操作(包括类型转换)必要时将遵循 P L / S Q L的一般赋值规 则。 形参可以有三种模式,I N,O U T或IN OUT。(O r a c l e 8 i增加了N O C O P Y限定符,该参数在本 章“使用N O C O P Y”一节介绍。)如果没有为形参指定模式,其默认模式为 I N。表4 - 1说明了模 式间的区别,下面的例子也使用了不同的参数模式: 表4-1 参数模式 模 式 说 明 I N 当当过程被调用时,实参的值将传入该过程。在该过程内部,形参类似 P L / S Q L使用的常 数,即该值具有只读属性不能对其修改。当该过程结束时,控制将返回到调用环境,这 时,对应的实参没有改变。 O U T 当当过程被调用时,实参具有的任何值将被忽略不计。在该过程内部,形参的作用类似没 有初始化的P L / S Q L变量,其值为空( N U L L)。该变量具有读写属性。当该过程结束时, 控制将返回调用环境,形参的内容将赋予对应的实参。(在 O r a c l e 8 i中,该操作可由 N O C O P Y变更。有关N O C O P Y的详细内容,请看本章“按引用和按值传递参数”一节。) IN OUT 当该模式是模式IN 和O U T的组合。当调用过程时,实参的值将被传递到该过程中。在该 过程内部,形参相当于初始化的变量,并具有读写属性。当该过程结束时,控制将返回 到调用环境中,形参的内容将赋予实参(在 O r a c l e 8 i中与参数N O C O P Y有关)。 注意 例子M o d e Te s t演示了合法与不合法的 P L / S Q L赋值操作。如果将注释为非法语句 的注释符删除,该程序将出现编译错误。 节选自在线代码ModeTest.sql CREATE OR REPLACE PROCEDURE ModeTest ( p_InParameter IN NUMBER, p_OutParameter OUT NUMBER, p_InOutParameter IN OUT NUMBER) IS v_LocalVariable NUMBER := 0; BEGIN 第4章 创建子程序和包计计121下载DBMS_OUTPUT.PUT_LINE('Inside ModeTest:'); IF (p_InParameter IS NULL) THEN DBMS_OUTPUT.PUT('p_InParameter is NULL'); ELSE DBMS_OUTPUT.PUT('p_InParameter = ' || p_InParameter); END IF; IF (p_OutParameter IS NULL) THEN DBMS_OUTPUT.PUT(' p_OutParameter is NULL'); ELSE DBMS_OUTPUT.PUT(' p_OutParameter = ' || p_OutParameter); END IF; IF (p_InOutParameter IS NULL) THEN DBMS_OUTPUT.PUT_LINE(' p_InOutParameter is NULL'); ELSE DBMS_OUTPUT.PUT_LINE(' p_InOutParameter = ' || p_InOutParameter); END IF; /* Assign p_InParameter to v_LocalVariable. This is legal, since we are reading from an IN parameter and not writing to it. */ v_LocalVariable := p_InParameter; -- Legal /* Assign 7 to p_InParameter. This is ILLEGAL, since we are writing to an IN parameter. */ -- p_InParameter := 7; -- Illegal /* Assign 7 to p_OutParameter. This is legal, since we are writing to an OUT parameter. */ p_OutParameter := 7; -- Legal /* Assign p_OutParameter to v_LocalVariable. In Oracle7 version 7.3.4, and Oracle8 version 8.0.4 or higher (including 8i), this is legal. Prior to 7.3.4, it is illegal to read from an OUT parameter. */ v_LocalVariable := p_OutParameter; -- Possibly illegal /* Assign p_InOutParameter to v_LocalVariable. This is legal, since we are reading from an IN OUT parameter. */ v_LocalVariable := p_InOutParameter; -- Legal /* Assign 8 to p_InOutParameter. This is legal, since we are writing to an IN OUT parameter. */ p_InOutParameter := 8; -- Legal DBMS_OUTPUT.PUT_LINE('At end of ModeTest:'); IF (p_InParameter IS NULL) THEN DBMS_OUTPUT.PUT('p_InParameter is NULL'); ELSE 122计计第二部分 非对象功能 下载DBMS_OUTPUT.PUT('p_InParameter = ' || p_InParameter); END IF; IF (p_OutParameter IS NULL) THEN DBMS_OUTPUT.PUT(' p_OutParameter is NULL'); ELSE DBMS_OUTPUT.PUT(' p_OutParameter = ' || p_OutParameter); END IF; IF (p_InOutParameter IS NULL) THEN DBMS_OUTPUT.PUT_LINE(' p_InOutParameter is NULL'); ELSE DBMS_OUTPUT.PUT_LINE(' p_InOutParameter = ' || p_InOutParameter); END IF; END ModeTest; 注意 在O r a c l e 7 . 3 . 4版之前,以及 8 . 0 . 3版中,从参数 O U T读取是非法操作,但在 O r a c l e 8的8 . 0 . 4版及更高版本中,该操作是合法的。详细介绍请看下文“从参数 O U T读 取”。) 2. 在形参和实参之间传递值 我们使用下面的块来调用过程 M o d e Te s t: 节选自在线代码callMT.sql DECLARE v_In NUMBER := 1; v_Out NUMBER := 2; v_InOut NUMBER := 3; BEGIN DBMS_OUTPUT.PUT_LINE('Before calling ModeTest:'); DBMS_OUTPUT.PUT_LINE('v_In = ' || v_In || ' v_Out = ' || v_Out || ' v_InOut = ' || v_InOut); ModeTest(v_In, v_Out, v_InOut); DBMS_OUTPUT.PUT_LINE('After calling ModeTest:'); DBMS_OUTPUT.PUT_LINE(' v_In = ' || v_In || ' v_Out = ' || v_Out || ' v_InOut = ' || v_InOut); END; 该调用生成的输出信息如下: Before calling ModeTest: v_In = 1 v_Out = 2 v_InOut = 3 Inside ModeTest: p_InParameter = 1 p_OutParameter is NULL p_InOutParameter = 3 At end of ModeTest: p_InParameter = 1 p_OutParameter = 7 p_InOutParameter = 8 第4章 创建子程序和包计计123下载After calling ModeTest: v_In = 1 v_Out = 7 v_InOut = 8 该输出信息显示了在该过程内部 O U T参数已经被初始化为N U L L。同样,当该过程结束运行 时,在该过程结尾处的形参 I N和IN OUT的值也被复制给了对应的实参。 注意 如果该过程引发了异常,则形参 IN OUT和O U T的值不会被复制到对应的实参中 (在O r a c l e 8 i中,该功能与N O C O P Y参数有关)。请读者看下文“子程序内部引发的异常” 内容。 直接量或常数作为实参 因为复制功能的使用,对应于参数 IN OUT或O U T的实参必须是变 量,而不能是常数或表达式。也就是说,程序必须提供返回的变量的存储位置。例如,我们可 以在调用过程M o d e Te s t时用直接量来取代变量v _ I n . : 节选自在线代码callMT.sql DECLARE v_Out NUMBER := 2; v_InOut NUMBER := 3; BEGIN ModeTest(1, v_Out, v_InOut); END; 但是如果用直接量来取代变量 v _ O u t,就会发生下列错误: 节选自在线代码callMT.sql SQL> DECLARE 2 v_InOut NUMBER := 3; 3 BEGIN 4 ModeTest(1, 2, v_InOut); 5 END; 6 / DECLARE * ERROR at line 1: ORA-06550: line 4, column 15: PLS-00363: expression '2' cannot be used as an assignment target ORA-06550: line 4, column 3: PL/SQL: Statement ignored 编译检查 P L / S Q L编译器在创建过程时将对合法的赋值进行检查。例如,如果我们把对 p _ I n P a r a m e t e r的赋值语句的注释去掉的话,编译器将报告过程 M o d e Te s t有下列错误: PLS-363: expression 'P_INPARAMETER' cannot be used as an assignment target 从参数O U T读取 在O r a c l e 7 . 3 . 4以前的版本以及8 . 0 . 3版下,对过程中O U T参数进行读操作是 非法的。如果我们在8 . 0 . 3版数据库下编译过程M o d e Te s t的话,编译器将报告下列错误: PLS-00365: 'P_OUTPARAMETER' is an OUT parameter and cannot be read 解决该问题的方法是声明 O U T参数为IN OUT模式。表4 - 2列出了允许对O U T参数进行读操 作的O r a c l e版本。 124计计第二部分 非对象功能 下载表4-2 允许读操作的版本 Oracle 版本 读O U T参数 7 . 3 . 4之前的版本 不能 7 . 3 . 4版 可以 8 . 0 . 3 不能 8 . 0 . 4及更高版本 可以 3. 对形参的限制 调用过程时,实参的值将被传入该过程,这些实参在该过程内部以引用的方式使用形参。 同时,作为参数传递机制一部分,对变量的限制也传递给该过程。在过程的声明中,强制指定 参数C H A R和VA R C H A R 2的长度,以及指定 N U M B E R参数的精度或小数点后位数都是非法的, 这是因为这些限制可以从实参中获得。例如,下面的过程声明就是非法的并将引发编译错误: 节选自在线代码ParameterLenght.sql CREATE OR REPLACE PROCEDURE ParameterLength ( p_Parameter1 IN OUT VARCHAR2(10), p_Parameter2 IN OUT NUMBER(3,1)) AS BEGIN p_Parameter1 := 'abcdefghijklm'; p_Parameter2 := 12.3; END ParameterLength; 正确的声明应该是下面的代码: 节选自在线代码ParameterLenght.sql CREATE OR REPLACE PROCEDURE ParameterLength ( p_Parameter1 IN OUT VARCHAR2, p_Parameter2 IN OUT NUMBER) AS BEGIN p_Parameter1 := 'abcdefghijklmno'; p_Parameter2 := 12.3; END ParameterLength; 因此,是谁对形参p _ P a r a m e t e r 1和p _ P a r a m e t e r 2有限制呢?从上面的例子中,我们可以看出正 是实参对形参施加了限制。如果我们使用下面的代码调用过程 P a r a m e t e r L e n g t h的话: 节选自在线代码P a r a m e t e r L e n g t h . s q l DECLARE v_Variable1 VARCHAR2(40); v_Variable2 NUMBER(7,3); BEGIN ParameterLength(v_Variable1, v_Variable2); END; 则p _ P a r a m e t e r 1的最大长度为4 0 (该长度从实参v _ Va r i a b l e 1继承而来),而p _ P a r a m e t e r 2的精 度为7, 小数点后位数为3 (从实参v _ Va r i b l e继承而来)。在进行参数声明时,一定要注意上述特点。 现在,请考虑下面的程序块,该块也调用 P a r a m e t e r L e n g h t : 第4章 创建子程序和包计计125下载节选自在线代码ParameterLenght.sql DECLARE v_Variable1 VARCHAR2(10); v_Variable2 NUMBER(7,3); BEGIN ParameterLength(v_Variable1, v_Variable2); END; 上述两个块的唯一不同之处是第一块的参数 v _ Va r i a b l e 1 ,也就是p _ P a r a m e t e r 1的长度为1 0 ,而 不是4 0。由于过程P a r a m e t e r L e n g h t要把长度为1 5的字符串赋给p _ P a r a m e t e r 1 (即v _ Va r i a b l e 1 ) ,而该 参数没有足够的长度。当调用该过程时,将导致下面的错误: ORA-06502: PL/SQL: numeric or value error ORA-06512: at "EXAMPLE.PARAMETERLENGTH", line 5 ORA-06512: at line 5 该错误的根源不在该过程中,而是与调用该过程的代码有关。除此之外,错误 O R A - 6 5 0 2是 运行错误,而不是编译错误。因此,该块已经通过了编译,该错误是在该过程返回时发生的, 错误的原因是P L / S Q L引擎企图把字符串‘a d c d e f g h i j k l m n o’复制到形参中。 提示 为了避免如O R A - 6 5 0 2之类的错误,应在创建过程时,在文档中记录实参的任何 限制要求。该文档应由存储在过程中的注释组成,并要标明该过程的功能,以及所有参 数的定义。 %类型和过程参数 尽管对形参不能进行强制声明,但我们可以使用 %类型来说明形参。如 果一个形参是用%类型声明的,并且说明%类型的变量也是强制类型的话,则该强制说明将作用 在形参上而不是实参上。如果我们在过程 P a r a m e t e r L e n g t h中使用了下面的说明: 节选自在线代码ParameterLength.sql CREATE OR REPLACE PROCEDURE ParameterLength ( p_Parameter1 IN OUT VARCHAR2, p_Parameter2 IN OUT students.current_credits%TYPE) AS BEGIN p_Parameter2 := 12345; END ParameterLength; 由于列c u r r e n t _ c r e d i t s的精度是3 ,所以,上面程序中的 P _ P a r a m e t e r 2的精度也被限制为 3位。 即使我们用具有足够精度的实参来调用该过程,其形参的精度也是 3位。因此下列的过程将生成 O R A - 6 5 0 2错误: 节选自在线代码ParameterLenght.sql SQL> DECLARE 2 v_Variable1 VARCHAR2(1); 3 -- Declare v_Variable2 with no constraints 4 v_Variable2 NUMBER; 5 BEGIN 6 -- Even though the actual parameter has room for 12345, the 7 -- constraint on the formal parameter is taken and we get 8 -- ORA-6502 on this procedure call. 126计计第二部分 非对象功能 下载9 ParameterLength(v_Variable1, v_Variable2); 10 END; 11 / DECLARE * ERROR at line 1: ORA-06502: PL/SQL: numeric or value error: number precision too large ORA-06512: at "EXAMPLE.PARAMETERLENGTH", line 5 ORA-06512: at line 9 4. 子程序内部引发的异常 如果错误发生在子程序的内部,就会引发异常。该异常即可以是由用户定义的,也可以是 程序中予定义的。如果引发异常的过程中没有处理该错误的异常处理程序(或该异常发生在该 异常处理程序的内部。),根据异常的传播规则(请看 P L / S Q L用户指南的有关章节),控制将立 即转出该过程返回其调用环境。然而,在本例的情况下,形参 O U T和IN OUT的值并没有返回到 实参。这些实参仍将被设置为调用前的值。例如,假设我们创建下面的过程: 节选自在线代码RaiseError.sql /* Illustrates the behavior of unhandled exceptions and * OUT variables. If p_Raise is TRUE, then an unhandled * error is raised. If p_Raise is FALSE, the procedure * completes successfully. */ CREATE OR REPLACE PROCEDURE RaiseError ( p_Raise IN BOOLEAN, p_ParameterA OUT NUMBER) AS BEGIN p_ParameterA := 7; IF p_Raise THEN /* Even though we have assigned 7 to p_ParameterA, this * unhandled exception causes control to return immediately * without returning 7 to the actual parameter associated * with p_ParameterA. */ RAISE DUP_VAL_ON_INDEX; ELSE -- Simply return with no error. This will return 7 to the -- actual parameter. RETURN; END IF; END RaiseError; 现在,如果我们用下面的块调用 R a i s e E r r o r的话: 节选自在线代码RaiseError.sql DECLARE v_TempVar NUMBER := 1; BEGIN 第4章 创建子程序和包计计127下载DBMS_OUTPUT.PUT_LINE('Initial value: ' || v_TempVar); RaiseError(FALSE, v_TempVar); DBMS_OUTPUT.PUT_LINE('Value after successful call: ' || v_TempVar); v_TempVar := 2; DBMS_OUTPUT.PUT_LINE('Value before 2nd call: ' || v_TempVar); RaiseError(TRUE, v_TempVar); EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Value after unsuccessful call: ' || v_TempVar); END; 将得到下面所示的输出: Initial value: 1 Value after successful call: 7 Value before 2nd call: 2 Value after unsuccessful call: 2 在第一次调用 R a i s e E r r o r之前,变量v _ Te m p Va r的值为1 .在第一次调用成功结束后,该变量 的值为7。接着,在第二次调用 R a i s e E r r o r前,该块将变量v _ Te m p Va r的值修改为2。由于第二次 调用没有成功,变量v _ Te m p Va r的值仍然是2 (而不是被再次赋予值7 )。 当声明参数O U T或IN OUT时使用了N O C O P Y选项时,异常处理的语义将发生变化。 有关该选项的使用方法,请看下文“使用 N O C O P Y时的异常语义”内容。 5. 按引用和按值传递参数 子程序参数可以按下列两种方式传递,即按引用,或按值传递。当参数是按引用传递时, 一个指向实参的指针将被传递到对应的形参。当参数是按值传递时,实参的值将被赋予对应的 形参。按引用传递的效率要比按值传递的效率要高,这是因为按引用传递不涉及复制操作。按 引用传递特别适合于处理集合类型参数,如表,数组等。 P L / S Q L的默认方式是对参数 I N进行按 引用传递,而对参数O U T,IN OUT执行按值传递。采用这种参数传递规则是为了与我们在前一 节讨论的异常语义保持一致,以便可以对实参的强制说明进行验证。 O r a c l e 8 i以前的版本不支持 对引用方式的修改。 6. 使用N O C O P Y参数 O r a c l e 8 i提供了一个叫做N O C O P Y的编译选项。使用该项声明参数的语法如下: parameter_name [mode] NOCOPY datatype 其中p a r a m e t e r _ n a m e是参数名,m o d e是参数的模式(IN ,OU ,IN OUT),d a t a t y p e是参数 的数据类型。如果使用了 N O C O P Y,则P L / S Q L编译器将按引用传递参数,而不按值传递。由于 N O C O P Y是一个编译选项,而非指令,所以该选项不会大量使用。下面的例子介绍了 N O C O P Y 的使用方法: 节选自在线代码NoCopyTest.sql 128计计第二部分 非对象功能 下载CREATE OR REPLACE PROCEDURE NoCopyTest ( p_InParameter IN NUMBER, p_OutParameter OUT NOCOPY VARCHAR2, p_InOutParameter IN OUT NOCOPY CHAR) IS BEGIN NULL; END NoCopyTest; 对参数I N使用N O C O P Y将会产生编译错误,这是因为参数 I N总是按引用传递, N O C O P Y不 能更改其引用方式。 使用N O C O P Y时的异常语义 当参数按引用传递时,任何对实参的修改也将引起对其对应 形参的修改,这是因为该实参和形参同时位于相同的存储单元的缘故。换句话说,如果过程退 出时没有处理异常而形参已被修改的话,则该形参对应的实参的原始值也将被修改。假设我们 在过程R a i s e E r r o r中使用N O C O P Y选项,该代码如下: 节选自在线代码NoCopyTest.sql CREATE OR REPLACE PROCEDURE RaiseError ( p_Raise IN BOOLEAN, p_ParameterA OUT NOCOPY NUMBER) AS BEGIN p_ParameterA := 7; IF p_Raise THEN RAISE DUP_VAL_ON_INDEX; ELSE RETURN; END IF; END RaiseError; 对该过程的唯一修改是指定参数 p _ P a r a m e t e r A按引用传递。假设我们用下面的代码调用该 R a i s e E r r o r过程: 节选自在线代码NoCopyTest.sql DECLARE v_TempVar NUMBER := 1; BEGIN DBMS_OUTPUT.PUT_LINE('Initial value: ' || v_TempVar); RaiseError(FALSE, v_TempVar); DBMS_OUTPUT.PUT_LINE('Value after successful call: ' || v_TempVar); v_TempVar := 2; DBMS_OUTPUT.PUT_LINE('Value before 2nd call: ' || v_TempVar); RaiseError(TRUE, v_TempVar); EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Value after unsuccessful call: ' || v_TempVar); END; 第4章 创建子程序和包计计129下载(上面的代码块与我们在4 . 1 . 3节中使用的代码一样。) 如下所示,现在该块的输出与前面使用的块的输出有所不同: Initial value: 1 Value after successful call: 7 Value before 2nd call: 2 Value after unsuccessful call: 7 可以看出,即使该块中引发了异常,其实参也被修改了两次。 使用N O C O P Y的限制 在某些情况下,N O C O P Y将被编译器忽略,这时的参数仍将按值传 递。这时,编译器不会报告编译错误。由于 N O C O P Y是一个提示项( H i n t),编译器可以决定是 否执行该项。在下列情况下,编译器将忽略 N O C O P Y项: • 实参是索引表(index-by table)的成员时。如果该实参是全表,则该限制不起作用。 • 实参被强制指定精度,比例或 NOT NULL时。该限制将不适用按最大长度强制的字符串参 数。 • 实参和形参都是记录类型,二者是以隐含方式或使用了 % R O W T Y P E类型声明时,作用在 对应字段的强制说明不一致。 • 传递实参需要隐式类型转换时。 • 子程序涉及到远程过程调用( P R C)。远程过程调用就是跨越数据库对远程服务器的过程 调用。 提示 对于最后一条来说,如果子程序是 P R C的一部分,则N O C O P Y将被忽略。如果对 现存的应用进行修改使其具有远程调用命令的话,则异常处理的语义将随之改变。 使用 N O C O P Y的优点 N O C O P Y的主要优点是可以提高程序的效率。当我们传递大型 P L / S Q L表时,其优越性特别显著。请看下面的例子: 节选自在线代码CopyFast.sql CREATE OR REPLACE PACKAGE CopyFast AS -- PL/SQL table of students. TYPE StudentArray IS TABLE OF students%ROWTYPE; -- Three procedures which take a parameter of StudentArray, in -- different ways. They each do nothing. PROCEDURE PassStudents1(p_Parameter IN StudentArray); PROCEDURE PassStudents2(p_Parameter IN OUT StudentArray); PROCEDURE PassStudents3(p_Parameter IN OUT NOCOPY StudentArray); -- Test procedure. PROCEDURE Go; END CopyFast; CREATE OR REPLACE PACKAGE BODY CopyFast AS PROCEDURE PassStudents1(p_Parameter IN StudentArray) IS BEGIN 130计计第二部分 非对象功能 下载NULL; END PassStudents1; PROCEDURE PassStudents2(p_Parameter IN OUT StudentArray) IS BEGIN NULL; END PassStudents2; PROCEDURE PassStudents3(p_Parameter IN OUT NOCOPY StudentArray) IS BEGIN NULL; END PassStudents3; PROCEDURE Go IS v_StudentArray StudentArray := StudentArray(NULL); v_StudentRec students%ROWTYPE; v_Time1 NUMBER; v_Time2 NUMBER; v_Time3 NUMBER; v_Time4 NUMBER; BEGIN -- Fill up the array with 50,001 copies of David Dinsmore's -- record. SELECT * INTO v_StudentArray(1) FROM students WHERE ID = 10007; v_StudentArray.EXTEND(50000, 1); -- Call each version of PassStudents, and time them. -- DBMS_UTILITY.GET_TIME will return the current time, in -- hundredths of a second. v_Time1 := DBMS_UTILITY.GET_TIME; PassStudents1(v_StudentArray); v_Time2 := DBMS_UTILITY.GET_TIME; PassStudents2(v_StudentArray); v_Time3 := DBMS_UTILITY.GET_TIME; PassStudents3(v_StudentArray); v_Time4 := DBMS_UTILITY.GET_TIME; -- Output the results. DBMS_OUTPUT.PUT_LINE('Time to pass IN: ' || TO_CHAR((v_Time2 - v_Time1) / 100)); DBMS_OUTPUT.PUT_LINE('Time to pass IN OUT: ' || TO_CHAR((v_Time3 - v_Time2) / 100)); DBMS_OUTPUT.PUT_LINE('Time to pass IN OUT NOCOPY: ' || TO_CHAR((v_Time4 - v_Time3) / 100)); END Go; END CopyFast; 第4章 创建子程序和包计计131下载注意 该例使用了一个包来把相关的过程编为一组。本书的第 1 4章介绍了集合以及方法 E X T E N D的用途。 过程组P a s s S t u d e n t s中的每个过程除了接收P L / S Q L表s t u d e n t的一个参数外没有任何其他的功 能。该参数有 50 001个记录,是一个相当大的表。该过程组各过程之间的不同之处是 P a s s S t u d e n t s 1是以I N模式接收其参数,而 P a s s S t u d e n t s 2以IN OUT模式接收参数,P a s s S t u d e n t s 3 则以IN OUT、N O C O P Y的模式接收参数。因此, P a s s S t u d e n t s 2是按值传递参数,而其他两个过 程则执行按引用传递参数。我们可以从下面调用 C o p y F a s t . G o的结果看到这种区别: Time to pass IN: 0 Time to pass IN OUT: 4.28 Time to pass IN OUT NOCOPY: 0 尽管在不同的操作系统平台上,实际结果可能有所不同,但我们可以看出,按值传递 I N OUT 模式的参数所使用的时间远远大于按引用传递 IN 和IN OUT NOCOPY参数所使用的时间。 7. 不带参数的子程序 如果过程没有参数的话,就不需要在该过程调用声明中或在其过程调用中使用括弧。函数 的也具有类似的情况。下面的过程代码演示了使用不带参数的过程。 节选自在线代码noparams.sql CREATE OR REPLACE PROCEDURE NoParamsP AS BEGIN DBMS_OUTPUT.PUT_LINE('No Parameters!'); END NoParamsP; CREATE OR REPLACE FUNCTION NoParamsF RETURN DATE AS BEGIN RETURN SYSDATE; END NoParamsF; BEGIN NoParamsP; DBMS_OUTPUT.PUT_LINE('Calling NoParamsF on ' || TO_CHAR(NoParamsF, 'DD-MON-YYYY')); END; 在Oracle8i 的C A L L调用语法下,括弧是选择项。 8. 按位置对应法和按名称对应法 到目前为止本章所举案例中,实参都与其位置对应的形参相关联。假设有下面的过程声明 代码: 节选自在线代码CallMe.sql CREATE OR REPLACE PROCEDURE CallMe( p_ParameterA VARCHAR2, p_ParameterB NUMBER, p_ParameterC BOOLEAN, 132计计第二部分 非对象功能 下载p_ParameterD DATE) AS BEGIN NULL; END CallMe; 以及如下所示的过程调用: 节选自在线代码CallMe.sql DECLARE v_Variable1 VARCHAR2(10); v_Variable2 NUMBER(7,6); v_Variable3 BOOLEAN; v_Variable4 DATE; BEGIN CallMe(v_Variable1, v_Variable2, v_Variable3, v_Variable4); END; 实参是按位置与形参相关联的。也就是说, v _ Va r i a b l e 1是与 P _ P a r a m e t e r A相关联的, v _ Va r i a b l e 2是与p _ P a r a m e t e r B相关联的,依此类推,参数间的这种对应法称为按位置对应法 (Positional Notation).这种定位关系在3 G L语言,以及C语言中都是常用的表达方式。 除此之外,我们还可以使用如下所示的按名称对应法( Named Notation)来调用过程: 节选自在线代码CallMe.sql DECLARE v_Variable1 VARCHAR2(10); v_Variable2 NUMBER(7,6); v_Variable3 BOOLEAN; v_Variable4 DATE; BEGIN CallMe(p_ParameterA => v_Variable1, p_ParameterB => v_Variable2, p_ParameterC => v_Variable3, p_ParameterD => v_Variable4); END; 在按名称对应法下,形参和实参同时出现在参数的位置上。这种方法允许我们按程序的要 求重新安排参数的顺序。例如,下面的块使用同样的参数调用过程 C a l l M e : 节选自在线代码CallMe.sql DECLARE v_Variable1 VARCHAR2(10); v_Variable2 NUMBER(7,6); v_Variable3 BOOLEAN; v_Variable4 DATE; BEGIN CallMe(p_ParameterB => v_Variable2, p_ParameterC => v_Variable3, p_ParameterD => v_Variable4, p_ParameterA => v_Variable1); END; 如果有必要的话,按位置对应法和按名称对应法可以同时用在一个调用中。但是,该类调 第4章 创建子程序和包计计133下载用的第一个参数必须是按位置匹配的,其余的参数可以按名称指定。下面的程序中使用了这种 混合调用方法: 节选自在线代码CallMe.sql DECLARE v_Variable1 VARCHAR2(10); v_Variable2 NUMBER(7,6); v_Variable3 BOOLEAN; v_Variable4 DATE; BEGIN -- First 2 parameters passed by position, the second 2 are -- passed by name. CallMe(v_Variable1, v_Variable2, p_ParameterC => v_Variable3, p_ParameterD => v_Variable4); END; 按名称对应法也是P L / S Q L从Ada 语言继承的一个特性。至于什么时候应该使用按名称对应, 什么时候应使用按名称对应,从效率上讲,二者没有明显区别。唯一的区别是它们的风格不一 样。表4 - 3介绍了这两种方式的不同风格。 就作者来说,我倾向于使用按位置对应法。因为我喜欢编制风格简明的代码。我认为给实 参命名有意义的名称是非常重要的。但从另一方面来说,如果过程带有大量的参数(超过 1 0 个),那么就应该考虑使用按名称对应法,因为这样做可以较好的匹配实参和形参。实际上,带 有超过1 0个参数的过程是非常少的。 表4-3 按位置对应法与按名称对应法的对比 按位置对应法 按名称对应法 实参使用有意义的名称来说明每个 清楚地指明了实参与形参的对应关系 参数的用途 用于实参和形参的参数名可以相互 该方式的维护工作比较多,因为如果某个形参 独立;当任意一个参数的名称修改时, 的名称改变的话,则所有对该过程的调用中实参 不会影响程序的使用 使用的名称都要做相应的修改 如果形参的顺序发生变化时,所有 形参和实参的使用顺序是独立的;参数出现的 对该过程的调用的位置符号必须也 位置可以随意修改 做相应的调整,因此维护工作量大 程序的风格比命名方式更简洁 由于形参和实参都要写在调用中,所以代码的 编制工作量要比按位置对应方式大一些 使用默认值的参数必须出现在参数 允许形参使用默认值,与参数的本身的默认值无关 表的最后一个 提示 过程带有的参数越多,调用该过程并确认其所有参数都可用的难度就越大。如果 过程带有大量的参数要传递或要接收的话,可以考虑定义记录类型将参数作为记录中的 字段使用。这样一来,就可以使用一个记录类型的参数进行调用。(注意,如果调用环 134计计第二部分 非对象功能 下载境不是在P L / S Q L下,可能无法对记录类型赋值。)P L / S Q L对参数没有限制。 9. 参数默认值 类似于变量的声明,过程或函数的形参可以具有默认值。如果一个参数有默认值的话,该 参数就可以不从调用环境中传递。如果传递了参数,则实参的值将取代默认值。参数使用默认 值的语法如下: parameter_name [mode] parameter_type{:= |DEFAULT} initial_value 其 中 , p a r a m e t e r _ n a m e是 形 参 的 名 称 , m o d e是 参 数 使 用 的 模 式 ( IN , O U T, I N O U T), p a r a m e t e r _ t y p e是参数类型(予定义的或用户定义的),i n i t i a l _ v a l u e是赋予形参的默认值。 关键字: =和D E FA U LT可以任选使用。例如,除非由显式说明来覆盖默认值外,我们可以重编过 程A d d N e w S t u d e n t来把默认的经济专业值赋给所有的新生: 节选自在线代码default.sql CREATE OR REPLACE PROCEDURE AddNewStudent ( p_FirstName students.first_name%TYPE, p_LastName students.last_name%TYPE, p_Major students.major%TYPE DEFAULT 'Economics') AS BEGIN -- Insert a new row in the students table. Use -- student_sequence to generate the new student ID, and -- 0 for current_credits. INSERT INTO students VALUES (student_sequence.nextval, p_FirstName, p_LastName, p_Major, 0); END AddNewStudent; 如上所示,如果形参p _ M a j o r在过程调用中没有实参与其关联的话,该参数就使用其默认值。 我们也可以使用按位置对应法来实现上述功能: 节选自在线代码default.sql BEGIN AddNewStudent('Simon', 'Salovitz'); END; 或使用如下按名称对应法: 节选自在线代码default.sql BEGIN AddNewStudent(p_FirstName => 'Veronica', p_LastName => 'Vassily'); END; 如果使用了按位置对应法,则所有带有默认值的没有与实参相关联的参数就必须位于该参 数表的尾端。请看下面的代码: 节选自在线代码DefaultTtest.sql CREATE OR REPLACE PROCEDURE DefaultTest ( p_ParameterA NUMBER DEFAULT 10, p_ParameterB VARCHAR2 DEFAULT 'abcdef', p_ParameterC DATE DEFAULT SYSDATE) AS 第4章 创建子程序和包计计135下载BEGIN DBMS_OUTPUT.PUT_LINE( 'A: ' || p_ParameterA || ' B: ' || p_ParameterB || ' C: ' || TO_CHAR(p_ParameterC, 'DD-MON-YYYY')); END DefaultTest; 上面代码中,过程 D e f a u l t Te s t 的三个参数都使用了默认值。如果我们只希望参数 p _ P a r a m e t e r B使用默认值,而参数p _ P a r a m e t e r A和p _ P a r a m e t e r C都使用指定的值,我们最好使用 按名称对应法,其代码如下: 节选自在线代码DefaultTtest.sql SQL> BEGIN 2 DefaultTest(p_ParameterA => 7, p_ParameterC => '30-DEC-95'); 3 END; 4 / A: 7 B: abcdef C: 30-DEC-1995 PL/SQL procedure successfully completed. 在使用按位置对应法的情况下,如果我们要 p _ P a r a m e t e r B使用默认值,我们就必须使 p _ P a r a m e t e r C也用默认值,没有与实参关联的所有的默认参数都必须位于参数表的最后,其代 码如下所示: 节选自在线代码DefaultTest.sql SQL> BEGIN 2 -- Uses the default value for both p_ParameterB and 3 -- p_ParameterC. 4 DefaultTest(7); 5 END; 6 / A: 7 B: abcdef C: 17-OCT-1999 PL/SQL procedure successfully completed. 提示 在使用默认值时,尽量把它们放置在参数表的最后位置。在这种方式下,其他参 数即可以使用按位置对应法也可以使用按名称对应法。 4.1.4 过程与函数的比较 过程和函数有许多相同的特点: • 通过设置O U T参数,过程和函数都可以返回一个以上的值。 • 过程和函数都可以具有声明部分,执行部分,以及异常处理部分。 • 过程和函数都可以接收默认值。 • 都可以使用位置或名称对应法调用过程和函数。 • 过程和函数都可以接收参数 N O C O P Y(仅O r a c l e 8 i支持)。 综上所述,对于什么时候使用函数更合适,什么时候使用过程更有效这一问题,一般来说, 过程和函数的使用与子程序将要返回的值的数量以及这些值的使用方法有关。通常,如果返回 136计计第二部分 非对象功能 下载值在一个以上时,用过程为好。如果只有一个返回值,使用函数就可以满足要求。尽管函数可 以合法地使用参数 O U T(因此可以返回一个以上的值),但这种做法通常不予考虑。除此之外, 函数还可以从S Q L语句中调用。(请看第5章的有关内容。) 4.2 包 集成在P L / S Q L语言中的另一个 A d a语言特性是包的概念。包是由存储在一起的相关对象组 成的P L / S Q L结构。包有两个独立的部分,即说明部分和包体,这两部分独立地存储在数据字典 中。与可以位于本地块或数据库中的过程和函数不同,包只能存储;并且不能在本地存储。除 了允许相关的对象结为组之外,包与依赖性较强的存储子程序相比,其所受的限制较少。除此 之外,包的效率比较高(我们将在第5章讨论包的效率问题。) 从本质上讲,包就是一个命名的声明部分。任何可以出现在块声明中的语句都可以在包中 使用,这些语句包括过程,函数,游标,类型以及变量。把上述内容放入包中的好处是我们可 以从其他P L / S Q L块中对其进行引用,因此包为 P L / S Q L提供了全程变量。 4.2.1 包的说明 包的说明(也叫做包头)包含了有关包内容的信息。然而,该部分中不包括包的代码部分。 下面是一个包的说明部分: 节选自在线代码ClassPackage.sql CREATE OR REPLACE PACKAGE ClassPackage AS -- Add a new student into the specified class. PROCEDURE AddStudent(p_StudentID IN students.id%TYPE, p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE); -- Removes the specified student from the specified class. PROCEDURE RemoveStudent(p_StudentID IN students.id%TYPE, p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE); -- Exception raised by RemoveStudent. e_StudentNotRegistered EXCEPTION; -- Table type used to hold student info. TYPE t_StudentIDTable IS TABLE OF students.id%TYPE INDEX BY BINARY_INTEGER; -- Returns a PL/SQL table containing the students currently -- in the specified class. PROCEDURE ClassList(p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE, p_IDs OUT t_StudentIDTable, p_NumStudents IN OUT BINARY_INTEGER); END ClassPackage; 上面的包说明C l a s s P a c k a g e包括三个过程,一个类型说明和一个异常说明。创建包头的语法 第4章 创建子程序和包计计137下载如下所示: CREATE [OR REPLACE] PACKAGE package_name{IS | AS} type_definition| procedure_specification| function_specification| variable_declaration| exception_declaration| cursor_declaration| pragma_declaration END [ package_name]; 其中,p a c k a g e _ n a m e是包的名称。该包内的各种元素的说明语法(即过程说明,函数说明, 变量说明等)与匿名块中的同类元素的的说明使用的语法完全相同。也就是说,除去过程和函 数的声明以外,我们在前面介绍的用于过程声明部分的语法也适用于包头的说明部分。下面是 这类语法的规则: • 包元素的位置可以任意安排,然而,在声明部分,对象必须在引用前进行声明。例如,如 果一个游标使用了作为其W H E R E子句一部分的变量,则该变量必须在声明游标之前声明。 • 包头可以不对任何类型的元素进行说明。例如,包可以只带有过程和函数说明语句,而不 声明任何异常和类型。 • 对过程和函数的任何声明都必须是前向说明。所谓前向说明就是只对子程序和其参数(如 果有的话)进行描述,但不带有任何代码的说明。本书的第 5章的‘前向声明’一节专门 介绍该类声明的具体使用方法。该声明的规则不同于块声明语法,在块声明中,过程或函 数的前向声明和代码同时出现在其声明部分,而实现包所说明的过程或函数的代码则只能 出现在包体中。 4.2.2 包体 包体是一个独立于包头的数据字典对象。包体只能在包头完成编译后才能进行编译。包体 中带有实现包头中描述的前向子程序的代码段。除此之外,包体还可以包括具有包体全局属性 的附加声明部分,但这些附加说明对于说明部分是不可见的。下面的例子演示了包 C l a s s P a c k a g e 的包体部分: 节选自在线代码ClassPackage.sql CREATE OR REPLACE PACKAGE BODY ClassPackage AS -- Add a new student for the specified class. PROCEDURE AddStudent(p_StudentID IN students.id%TYPE, p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE) IS BEGIN INSERT INTO registered_students (student_id, department, course) VALUES (p_StudentID, p_Department, p_Course); END AddStudent; -- Removes the specified student from the specified class. PROCEDURE RemoveStudent(p_StudentID IN students.id%TYPE, 138计计第二部分 非对象功能 下载p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE) IS BEGIN DELETE FROM registered_students WHERE student_id = p_StudentID AND department = p_Department AND course = p_Course; -- Check to see if the DELETE operation was successful. If -- it didn't match any rows, raise an error. IF SQL%NOTFOUND THEN RAISE e_StudentNotRegistered; END IF; END RemoveStudent; -- Returns a PL/SQL table containing the students currently -- in the specified class. PROCEDURE ClassList( p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE, p_IDs OUT t_StudentIDTable, p_NumStudents IN OUT BINARY_INTEGER) IS v_StudentID registered_students.student_id%TYPE; -- Local cursor to fetch the registered students. CURSOR c_RegisteredStudents IS SELECT student_id FROM registered_students WHERE department = p_Department AND course = p_Course; BEGIN /* p_NumStudents will be the table index. It will start at * 0, and be incremented each time through the fetch loop. * At the end of the loop, it will have the number of rows * fetched, and therefore the number of rows returned in * p_IDs. */ p_NumStudents := 0; OPEN c_RegisteredStudents; LOOP FETCH c_RegisteredStudents INTO v_StudentID; EXIT WHEN c_RegisteredStudents%NOTFOUND; p_NumStudents := p_NumStudents + 1; p_IDs(p_NumStudents) := v_StudentID; END LOOP; END ClassList; END ClassPackage; 该包体部分包括了实现包头中过程的前向说明的代码。在包头中没有进行前向说明的对象 第4章 创建子程序和包计计139下载(如异常e _ S t u d e n t N o t R e g i s t e r e d)可以在包体中直接引用。 包体是可选的。如果包头中没有说明任何过程或函数的话(只有变量声明,游标,类型等), 则该包体就不必存在。由于包中的所有对象在包外都是可见的,所以,这种说明方法可用来声 明全局变量。(有关包元素的作用域和可见性将在下一节讨论。) 包头中的任何前向说明不能出现在包体中。包头和包体中的过程和函数的说明必须一致, 其中包括子程序名和其参数名,以及参数的模式。例如,由于下面的包体对函数 F u n c t i o n A使用 了不同的参数表,因此其包头与其包体不匹配。 节选自在线代码packageError.sql CREATE OR REPLACE PACKAGE PackageA AS FUNCTION FunctionA(p_Parameter1 IN NUMBER, p_Parameter2 IN DATE) RETURN VARCHAR2; END PackageA; CREATE OR REPLACE PACKAGE BODY PackageA AS FUNCTION FunctionA(p_Parameter1 IN CHAR) RETURN VARCHAR2; END PackageA; 如果我们企图按上面的说明来创建包 P a c g a g e A的话,编译程序将给包体提出下列错误警告: PLS-00328: A subprogram body must be defined for the forward declaration of FUNCTIONA. PLS-00323: subprogram or cursor 'FUNCTIONA' is declared in a package specification and must be defined in the package body. 4.2.3 包和作用域 包头中声明的任何对象都是在其作用域中,并且可在其外部使用包名作为前缀对其进行引 用。例如,我们可以从下面的 P L / S Q L块中调用对象C l a s s P a c k a g e . R e m o v e S t u d e n t : BEGIN ClassPackage.RemoveStudent(10006, 'HIS', 101); END; 上面的过程调用的格式与调用独立过程的格式完全一致,其唯一不同的地方是在被调用的 过程名的前面使用了包名作为其前缀。打包的过程可以具有默认参数,并且这些参数可以通过 按位置或按名称对应的方式进行调用,就象独立过程的参数的调用方式一样。 上述调用方法还可以适用于包中用户定义的类型。例如,为了调用过程 C l a s s L i s t,我们需要 声明一个类型为 C l a s s P a c k a g e . t _ S t u d e n t I D Ta b l e的变量(请看本书的第 1 4章有关声明和使用 P L / S Q L集合类型的介绍): 节选自在线代码callCL.sql DECLARE v_HistoryStudents ClassPackage.t_StudentIDTable; v_NumStudents BINARY_INTEGER := 20; 140计计第二部分 非对象功能 下载BEGIN -- Fill the PL/SQL table with the first 20 History 101 -- students. ClassPackage.ClassList('HIS', 101, v_HistoryStudents, v_NumStudents); -- Insert these students into temp_table. FOR v_LoopCounter IN 1..v_NumStudents LOOP INSERT INTO temp_table (num_col, char_col) VALUES (v_HistoryStudents(v_LoopCounter), 'In History 101'); END LOOP; END; 在包体内,包头中的对象可以直接引用,可以不用包名为其前缀。例如,过程 R e m o v e S t u e n t 可以简单地使用 e _ S t u d e n t N o t R e g i s t e r e d来引用异常,而不是用 ClassPackage. e_StudentNot R e g i s t e r e d来引用。当然,如果需要的话,也可以使用全名进行引用。 包体中对象的作用域 按照目前的程序 ,过程C l a s s P a c k a g e . A d d S t u d e n t和C l a s s P a c k a g e . R e m o v e S t u d e n t只是简单地 对表registered_student 进行更新。实际上,该操作还不完整。这两个过程还要更新表 s t u d e n t s和 c l a s s e s以反映新增或删除的学生情况。如下所示,我们可以在包体中增加一个过程来实现上述 操作: 节选自在线代码ClassPackage2.sql CREATE OR REPLACE PACKAGE BODY ClassPackage AS -- Utility procedure that updates students and classes to reflect -- the change. If p_Add is TRUE, then the tables are updated for -- the addition of the student to the class. If it is FALSE, -- then they are updated for the removal of the student. PROCEDURE UpdateStudentsAndClasses( p_Add IN BOOLEAN, p_StudentID IN students.id%TYPE, p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE) IS -- Number of credits for the requested class v_NumCredits classes.num_credits%TYPE; BEGIN -- First determine NumCredits. SELECT num_credits INTO v_NumCredits FROM classes WHERE department = p_Department AND course = p_Course; IF (p_Add) THEN -- Add NumCredits to the student's course load 第4章 创建子程序和包计计141下载UPDATE STUDENTS SET current_credits = current_credits + v_NumCredits WHERE ID = p_StudentID; -- And increase current_students UPDATE classes SET current_students = current_students + 1 WHERE department = p_Department AND course = p_Course; ELSE -- Remove NumCredits from the students course load UPDATE STUDENTS SET current_credits = current_credits - v_NumCredits WHERE ID = p_StudentID; -- And decrease current_students UPDATE classes SET current_students = current_students - 1 WHERE department = p_Department AND course = p_Course; END IF; END UpdateStudentsAndClasses; -- Add a new student for the specified class. PROCEDURE AddStudent(p_StudentID IN students.id%TYPE, p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE) IS BEGIN INSERT INTO registered_students (student_id, department, course) VALUES (p_StudentID, p_Department, p_Course); UpdateStudentsAndClasses(TRUE, p_StudentID, p_Department, p_Course); END AddStudent; -- Removes the specified student from the specified class. PROCEDURE RemoveStudent(p_StudentID IN students.id%TYPE, p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE) IS BEGIN DELETE FROM registered_students WHERE student_id = p_StudentID AND department = p_Department AND course = p_Course; -- Check to see if the DELETE operation was successful. If -- it didn't match any rows, raise an error. IF SQL%NOTFOUND THEN RAISE e_StudentNotRegistered; END IF; 142计计第二部分 非对象功能 下载UpdateStudentsAndClasses(FALSE, p_StudentID, p_Department, p_Course); END RemoveStudent; ... END ClassPackage; 过程U p d a t e S t u d e n t A n d c l a s s e s声明为包体的全局量,其作用域是包体本身。该过程可以由该 包中的其他过程调用(如A d d S t u d e n t和R e m o v e S t u d e n t),但是该过程在包体外是不可见的。 4.2.4 重载打包子程序 在包的内部,过程和函数可以被重载( O v e r l o a d i n g)。也就是说,可以有一个以上的名称相 同,但参数不同的过程或函数。由于重载允许将相同的操作施加在不同类型的对象上,因此, 它是P L / S Q L语言的一个重要特点。例如,假设我们要使用学生 I D或该学生的姓和名来把一个学 生加入到班级中。我们可以对包 C l a s s P a c k a g e修改如下: 节选自在线代码overload.sql CREATE OR REPLACE PACKAGE ClassPackage AS -- Add a new student into the specified class. PROCEDURE AddStudent(p_StudentID IN students.id%TYPE, p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE); -- Also adds a new student, by specifying the first and last -- names, rather than ID number. PROCEDURE AddStudent(p_FirstName IN students.first_name%TYPE, p_LastName IN students.last_name%TYPE, p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE); ... END ClassPackage; CREATE OR REPLACE PACKAGE BODY ClassPackage AS -- Add a new student for the specified class. PROCEDURE AddStudent(p_StudentID IN students.id%TYPE, p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE) IS BEGIN INSERT INTO registered_students (student_id, department, course) VALUES (p_StudentID, p_Department, p_Course); END AddStudent; -- Add a new student by name, rather than ID. PROCEDURE AddStudent(p_FirstName IN students.first_name%TYPE, p_LastName IN students.last_name%TYPE, p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE) IS 第4章 创建子程序和包计计143下载v_StudentID students.ID%TYPE; BEGIN /* First we need to get the ID from the students table. */ SELECT ID INTO v_StudentID FROM students WHERE first_name = p_FirstName AND last_name = p_LastName; -- Now we can add the student by ID. INSERT INTO registered_students (student_id, department, course) VALUES (v_StudentID, p_Department, p_Course); END AddStudent; ... END ClassPackage; 现在,我们可以对M u s i c 4 1 0增加一名学生如下: BEGIN ClassPackage.AddStudent(10000, 'MUS', 410); END; 或 BEGIN ClassPackage.AddStudent('Rita', 'Razmataz', 'MUS', 410); END; 我们可以看到同样的操作可以通过不同类型的参数实现,这就说明重载是非常有用的技术。 虽然如此,但重载仍要受到下列限制: • 如果两个子程序的参数仅在名称和模式上不同的话,这两个过程不能重载。例如下面的两 个过程是不能重载的: PROCEDURE overloadMe(p_TheParameter IN NUMBER); PROCEDURE overloadMe(p_TheParameter OUT NUMBER); • 不能仅根据两个过程不同的返回类型对其进行重载。例如,下面的函数是不能进行重载的: FUNCTION overloadMeToo RETURN DATE; FUNCTION overloadMeToo RETURN NUMBER; • 最后,重载函数的参数的类族( type family)必须不同,也就是说,不能对同类族的过程 进行重载。例如,由于C H A R和VA R C H A R 2属于同一类族,所以不能重载下面的过程: PROCEDURE OverloadChar(p_TheParameter IN CHAR); PROCEDURE OverloadChar(p_TheParameter IN VARCHAR2); 注意 P L / S Q L编译器实际上允许程序员创建违反上述限制的带有子程序的包。然而, P L / S Q L运行时系统将无法解决引用问题并将引发“ PLS-307:too many declaration of ‘s u b p r o g r a m’ match this call”的运行错误。 对象类型和重载 144计计第二部分 非对象功能 下载根据用户定义的对象类型,打包子程序也可以重载。例如,假设我们要创建下面的 两个对象类型: 节选自在线代码objectOverload.sql CREATE OR REPLACE TYPE t1 AS OBJECT ( f NUMBER ); CREATE OR REPLACE TYPE t2 AS OBJECT ( f NUMBER ); 现在,我们可以创建一个包和一个带有根据其参数的对象类型重载的两个过程的包体: 节选自在线代码objectOverload.sql CREATE OR REPLACE PACKAGE Overload AS PROCEDURE Proc(p_Parameter1 IN t1); PROCEDURE Proc(p_Parameter1 IN t2); END Overload; CREATE OR REPLACE PACKAGE BODY Overload AS PROCEDURE Proc(p_Parameter1 IN t1) IS BEGIN DBMS_OUTPUT.PUT_LINE('Proc(t1): ' || p_Parameter1.f); END Proc; PROCEDURE Proc(p_Parameter1 IN t2) IS BEGIN DBMS_OUTPUT.PUT_LINE('Proc(t2): ' || p_Parameter1.f); END Proc; END Overload; 如下例所示,根据参数的类型对过程进行正确的调用: 节选自在线代码objectOverload.sql SQL> DECLARE 2 v_Obj1 t1 := t1(1); 3 v_OBj2 t2 := t2(2); 4 BEGIN 5 Overload.Proc(v_Obj1); 6 Overload.proc(v_Obj2); 7 END; 8 / Proc(t1): 1 Proc(t2): 2 PL/SQL procedure successfully completed. 有关对象类型的详细内容及使用方法,请看本书的第 12,13 两章。 4.2.5 包的初始化 当第一次调用打包子程序时,该包将进行初始化。也就是说将该包从硬盘中读入到内存并 第4章 创建子程序和包计计145下载启动调用的子程序的编译代码开始运行。这时,系统为该包中定义的所有变量分配内存单元。 每个会话都有其打包变量的副本,以确保执行同一包子程序的两个对话使用不同的内存单元。 在大多数情况下,初始化代码要在包第一次初始化时运行。为了实现这种功能,我们可以 在包体中所有对象之后加入一个初始化部分,其语法如下: CREATE OR REPLACE PACKAGE BODY package_name{IS | AS} ... BEGIN initialization_code; END [ package_name]; 其中,p a c k a g e _ n a m e是包的名称,i n i t i a l i z a t i o n _ c o d e是要运行的初始化代码。例如,下面的 包实现了一个随机数函数: 节选自在线代码Random.sql CREATE OR REPLACE PACKAGE Random AS -- Random number generator. Uses the same algorithm as the -- rand() function in C. -- Used to change the seed. From a given seed, the same -- sequence of random numbers will be generated. PROCEDURE ChangeSeed(p_NewSeed IN NUMBER); -- Returns a random integer between 1 and 32767. FUNCTION Rand RETURN NUMBER; -- Same as Rand, but with a procedural interface. PROCEDURE GetRand(p_RandomNumber OUT NUMBER); -- Returns a random integer between 1 and p_MaxVal. FUNCTION RandMax(p_MaxVal IN NUMBER) RETURN NUMBER; -- Same as RandMax, but with a procedural interface. PROCEDURE GetRandMax(p_RandomNumber OUT NUMBER, p_MaxVal IN NUMBER); END Random; CREATE OR REPLACE PACKAGE BODY Random AS /* Used for calculating the next number. */ v_Multiplier CONSTANT NUMBER := 22695477; v_Increment CONSTANT NUMBER := 1; /* Seed used to generate random sequence. */ v_Seed number := 1; PROCEDURE ChangeSeed(p_NewSeed IN NUMBER) IS BEGIN v_Seed := p_NewSeed; END ChangeSeed; FUNCTION Rand RETURN NUMBER IS 146计计第二部分 非对象功能 下载BEGIN v_Seed := MOD(v_Multiplier * v_Seed + v_Increment, (2 ** 32)); RETURN BITAND(v_Seed/(2 ** 16), 32767); END Rand; PROCEDURE GetRand(p_RandomNumber OUT NUMBER) IS BEGIN -- Simply call RandMax and return the value. p_RandomNumber := Rand; END GetRand; FUNCTION RandMax(p_MaxVal IN NUMBER) RETURN NUMBER IS BEGIN RETURN MOD(Rand, p_MaxVal) + 1; END RandMax; PROCEDURE GetRandMax(p_RandomNumber OUT NUMBER, p_MaxVal IN NUMBER) IS BEGIN -- Simply call RandMax and return the value. p_RandomNumber := RandMax(p_MaxVal); END GetRandMax; BEGIN /* Package initialization. Initialize the seed to the current time in seconds. */ ChangeSeed(TO_NUMBER(TO_CHAR(SYSDATE, 'SSSSS'))); END Random; 为了检索随机数,我们可以直接调用函数R a n d o m . R a n d。随机数序列是由其初始种子控制的, 对于给定的种子可以生成相应的随机数序列。因此,为了提供更多的随机数值,我们要在每次 实例化该包时,把随机数种子初始化为不同的值。为了实现上述功能,我们从包的初始部分调 用过程C h a n g e S e e d。 O r a c l e 8中提供了内置包 D B M S _ R A N D O M ,该包可以用于提供随机数。请看本书 C D - R O M中附录A对内置包的介绍。 4.3 小结 我们在本章分析了三种类型的命名 P L / S Q L块:过程,函数,和包。我们还讨论了创建这些 块的语法,特别是着重分析了各种参数的类型及传递方式。在下一章,我们将更多地使用过程, 函数和包。第5章的重点是子程序的类型,它们在数据字典中的存储方式,以及从 S Q L语句调用 存储子程序的方法。第 5章的最后将介绍O r a c l e 8 i新增的功能。在本书的第 6章,我们将介绍命名 块的第四种类型,数据库触发器类型。 第4章 创建子程序和包计计147下载下载 第5章 使用子程序和包 在上一章中,我们讨论了创建过程,函数和包的细节。在本章中,我们介绍这些部件的功 能,存储子程序和本地子程序的区别,存储子程序与数据字典的交互方式及如何从 S Q L语句中 调用存储子程序。除此之外,我们还要介绍 O r a c l e 8 i存储子程序的新增特性。 5.1 子程序位置 我们已在前几章中演示了可以存储在数据字典中的子程序和包。子程序首次是用命令 C R E ATE OR REPLACE创建的,接着,我们可以从其他 P L / S Q L块中调用已创建的子程序。除此 之外,子程序可以在块的声明部分定义,以这种方式定义的子程序叫做本地子程序。包则必须 存储在数据字典中,而不能在本地定义存储。 5.1.1 存储子程序和数据字典 当使用命令C R E ATE OR REPLACE创建子程序时,该子程序就存储在数据字典中。除去子 程序中的源文本外,该子程序是以编译后的中间代码形式存储的,这种中间代码叫做 p - c o d e。中 间代码中带有子程序中经计算得到的所有引用参数,子程序的源代码也被转换为 P L / S Q L引擎易 读的格式。当调用子程序时,就将中间代码从磁盘读入并启动执行。一旦从磁盘读入 中间代码, 系统就将其存储在系统全局工作区( S G A)的共享缓冲区部分,以便由多个用户同时进行访问。 与缓冲区的所有内容一样,根据系统采用的最近最少使用的算法,过期的中间代码将被从共享 缓冲区中清除。 中间代码类似于由3 G L语言生成的对象代码,或者类似于可由 J a v a运行时使用的J a v a字节码。 由于中间代码带有经计算得到的子程序中的所有对象引用(属于前联编的属性),所以,执行中 间代码的效率非常高。 子程序的信息可以通过各种数据字典视图来访问。视图 u s e r _ o b j e c t s包括了当前用户拥有的 所有对象的信息。该信息包括了对象的创建以及最后修改的时间,对象类型(表,序列,函数 等)和对象的有效性。视图 u s e r _ s o u r c e包括了对象的源程序代码。而视图 u s e r _ e r r o r s则包括了编 译错误信息。 请看下面的简单过程: CREATE OR REPLACE PROCEDURE Simple AS v_Counter NUMBER; BEGIN v_Counter := 7; END Simple; 创建该过程后,视图 u s e r _ o b j e c t s显示该过程是合法的,视图 u s e r _ s o u r c e则包括了该过程的 源代码。由于该过程已经编译成功,所以视图 u s e r _ e r r o r s没有显示错误。图 5 - 1的窗口显示了上述信息。 如果我们修改了过程 S i m p l e的代码,就会出现编译错误(源程序中缺少一个分号),修改过 的该过程如下: CREATE OR REPLACE PROCEDURE Simple AS v_Counter NUMBER; BEGIN v_Counter := 7 END Simple; 图5-1 成功编译后的数据字典视图 分析图5 - 2所示的相同的三个数据字典,我们不难看出有几个不同之处。首先, u s e r _ s o u r c e仍然 显示了该过程的源代码。然而, u s e r _ o b j e c t s中的状态指示为非法,而不是前面例子的合法提示, u s e r _ e r r o r s中有一条编译错误信息P L S - 1 0 3。 提示 在SQL *Plus中,命令SHOW ERRORS将为用户查询u s e r _ e r r o r s并将输出数据格 式为用户可读的形式。该命令将返回最后创建的对象的错误信息。如果编译程序出现了 错误提示:‘Warning: Procedure created with compilation errors’时,就可以使用该命令 查询错误的详细信息。有关 SQL *Plus的详细介绍,请看本书第 2章中介绍P L / S Q L开发 工具的内容。 虽然非法的存储子程序仍然在数据字典中,但是该子程序只有在将编译错误修改后才能调用。 如果调用了非法的过程,就会引发 P L S - 9 0 5错误,下面的例子演示了这种非法调用产生的错误: 第5章 使用子程序和包计计149下载图5-2 编译出错后的数据字典 SQL> BEGIN Simple; END; 2 / BEGIN Simple; END; * ERROR at line 1: ORA-06550: line 1, column 7: PLS-00905: object EXAMPLE.SIMPLE is invalid ORA-06550: line 1, column 7: PL/SQL: Statement ignored 有关数据字典的内容将在本书的配套光盘中的附录 C给予详细的介绍。 5.1.2 本地子程序 下面的程序是一个在P L / S Q L块的声明部分声明的本地子程序的例子: 节选自在线代码localSub.sql DECLARE CURSOR c_AllStudents IS SELECT first_name, last_name FROM students; v_FormattedName VARCHAR2(50); 150计计第二部分 非对象功能 下载第5章 使用子程序和包计计151下载 /* Function that will return the first and last name concatenated together, separated by a space. */ FUNCTION FormatName(p_FirstName IN VARCHAR2, p_LastName IN VARCHAR2) RETURN VARCHAR2 IS BEGIN RETURN p_FirstName || ' ' || p_LastName; END FormatName; -- Begin main block. BEGIN FOR v_StudentRecord IN c_AllStudents LOOP v_FormattedName := FormatName(v_StudentRecord.first_name, v_StudentRecord.last_name); DBMS_OUTPUT.PUT_LINE(v_FormattedName); END LOOP; END; 函数F o r a m t N a m e是在块的声明部分声明的。该函数名是一个 P L / S Q L的标识符,因此该函数 名将遵循P L / S Q L语言中标识符的作用域和可见性规则,该函数只在其声明的块中可见,其作用 域从声明点开始到该块结束为止。其他块不能调用该函数,因为该函数对其他块来说是不可见 的。 1. 本地子程序作为存储子程序的一部分 请看下面的程序例子,本地子程序也可以声明为存储子程序声明部分的内容。在这种情况 下,由于该函数的作用域的限制,也只能从过程 S t o r e d P r o c中调用函数F o r m a t N a m e。 节选自在线代码localStored.sql CREATE OR REPLACE PROCEDURE StoredProc AS /* Local declarations, which include a cursor, variable, and a function. */ CURSOR c_AllStudents IS SELECT first_name, last_name FROM students; v_FormattedName VARCHAR2(50); /* Function that will return the first and last name concatenated together, separated by a space. */ FUNCTION FormatName(p_FirstName IN VARCHAR2, p_LastName IN VARCHAR2) RETURN VARCHAR2 IS BEGIN RETURN p_FirstName || ' ' || p_LastName; END FormatName; -- Begin main block. BEGIN FOR v_StudentRecord IN c_AllStudents LOOP152计计第二部分 非对象功能 下载 v_FormattedName := FormatName(v_StudentRecord.first_name, v_StudentRecord.last_name); DBMS_OUTPUT.PUT_LINE(v_FormattedName); END LOOP; END StoredProc; 2. 本地子程序的位置 任何本地子程序都必须在声明部分的结尾处声明。如果我们把函数 F o r m a t N a m e的声明移动 到c _ A l l S t u d e n t声明的上面的话,如下面的 SQL *Plus声明部分所示,我们将得到编译错误。 节选自在线代码localError.sql SQL> DECLARE 2 /* Declare FormatName first. This will generate a compile 3 error, since all other declarations have to be before 4 any local subprograms. */ 5 FUNCTION FormatName(p_FirstName IN VARCHAR2, 6 p_LastName IN VARCHAR2) 7 RETURN VARCHAR2 IS 8 BEGIN 9 RETURN p_FirstName || ' ' || p_LastName; 10 END FormatName; 11 11 CURSOR c_AllStudents IS 12 SELECT first_name, last_name 13 FROM students; 14 14 v_FormattedName VARCHAR2(50); 15 -- Begin main block 16 BEGIN 17 NULL; 18 END; 19 / CURSOR c_AllStudents IS * ERROR at line 11: ORA-06550: line 11, column 3: PLS-00103: Encountered the symbol "CURSOR" when expecting one of the following: begin function package pragma procedure form 3. 前向声明(Forward Declarations) 由于本地P L / S Q L子程序的名称是标识符,所以它们必须在引用前声明。一般来说,满足这 种要求不是一件困难的事情。然而,在具有相互引用的子程序中,实现上述要求就有一定的难 度。请看下面的例子: 节选自在线代码mutual.sql第5章 使用子程序和包计计153下载 DECLARE v_TempVal BINARY_INTEGER := 5; -- Local procedure A. Note that the code of A calls procedure B. PROCEDURE A(p_Counter IN OUT BINARY_INTEGER) IS BEGIN DBMS_OUTPUT.PUT_LINE('A(' || p_Counter || ')'); IF p_Counter > 0 THEN B(p_Counter); p_Counter := p_Counter - 1; END IF; END A; -- Local procedure B. Note that the code of B calls procedure A. PROCEDURE B(p_Counter IN OUT BINARY_INTEGER) IS BEGIN DBMS_OUTPUT.PUT_LINE('B(' || p_Counter || ')'); p_Counter := p_Counter - 1; A(p_Counter); END B; BEGIN B(v_TempVal); END; 该例子无法进行编译,其原因是过程 A调用了过程B,因此过程B必须要在过程A之前声明以 便可以确定对过程 B的引用。同时,由于过程 B要调用过程A,要求过程A也要在过程B之前声明 以便对确定对过程B的引用。在这种情况下,上述要求不能同时满足。为了协调这种需求,我们 可以使用前向声明来解决该问题。前向声明只需要提供过程名和其形参就可以实现相互引用的 过程的并存。除此之外,前向声明还可以用在包头中。下面就是一个使用前向声明的例子: 节选自在线代码forwardDeclaration.sql DECLARE v_TempVal BINARY_INTEGER := 5; -- Forward declaration of procedure B. PROCEDURE B(p_Counter IN OUT BINARY_INTEGER); PROCEDURE A(p_Counter IN OUT BINARY_INTEGER) IS BEGIN DBMS_OUTPUT.PUT_LINE('A(' || p_Counter || ')'); IF p_Counter > 0 THEN B(p_Counter); p_Counter := p_Counter - 1; END IF; END A; PROCEDURE B(p_Counter IN OUT BINARY_INTEGER) IS BEGIN DBMS_OUTPUT.PUT_LINE('B(' || p_Counter || ')');154计计第二部分 非对象功能 下载 p_Counter := p_Counter - 1; A(p_Counter); END B; BEGIN B(v_TempVal); END; 该块的输出如下: B(5) A(4) B(4) A(3) B(3) A(2) B(2) A(1) B(1) A(0) 4. 重载本地子程序 我们在第4章中曾经介绍过,包中声明的子程序可以被重载。该规则对于本地子程序的情况 也适用,下面就是一个对本地子程序进行重载的例子: 节选自在线代码overloadedlocal.sql DECLARE -- Two overloaded local procedures PROCEDURE LocalProc(p_Parameter1 IN NUMBER) IS BEGIN DBMS_OUTPUT.PUT_LINE('In version 1, p_Parameter1 = ' || p_Parameter1); END LocalProc; PROCEDURE LocalProc(p_Parameter1 IN VARCHAR2) IS BEGIN DBMS_OUTPUT.PUT_LINE('In version 2, p_Parameter1 = ' || p_Parameter1); END LocalProc; BEGIN -- Call version 1 LocalProc(12345); -- And version 2 LocalProc('abcdef'); END; 该块的输出如下: In version 1, p_Parameter1 = 12345 In version 2, p_Parameter1 = abcdef第5章 使用子程序和包计计155下载 5.1.3 存储子程序和本地子程序的比较 存储子程序和本地子程序的工作方式不同,它们具有不同的属性。它们在什么情况下使用 呢?作者个人倾向于使用存储子程序,通常把存储子程序放在包里使用。如果我们开发了一个 有用的子程序,通常,我们希望能够从一个以上的块中对其进行调用。为了实现这种功能,该 子程序必须放在数据库中使用。除此之外,存储子程序的长度和复杂性与本地子程序相比也具 有一定的优势。作者声明为本地子程序的过程和函数一般都是代码很少的程序段,并且也只是 从程序(包含该程序的块)特定的部分进行调用。使用这类的本地子程序的主要考虑是为了避 免单块中的代码重复问题。这种使用方法类似于 C语言中的宏功能。表 5 - 1总结了存储子程序和 本地子程序的区别。 表5-1 存储子程序与本地子程序的比较 存储子程序 本地子程序 该类子程序以编译后生成的中间代码 本地子程序被编译为该程序所在块的一部 形式p - c o d e存储在数据库中。当调用该 分。如果其所在块是匿名块并需要多次运行 类子程序时,不需进行编译即可运行 时,则该子程序就必须每次进行编译 存储子程序可以从由用户提交的具有 本地子程序只能从包含子程序的块内调用 子程序优先级 E X E C U T E属性的任何块 中调用 由于存储子程序与调用块的相互隔离, 本地子程序和调用块同处于一个块内,所以 调用块具有代码少,易于理解的特点。除 容易引起混淆。如果修改了调用块的话,则该 此之外,子程序和调用块还可以各自独 块调用的子程序作为所属块的一部分也要重新 立维护 编译 可以使用 D B M S _ S H A R E D _ P O O L . K E E P 本地子程序自身不能存储在共享缓冲区中 打包过程来把编译后p - c o d e代码存储在共享 缓冲区中*。这种方式可以改善程序性能 不能对独立存储子程序进行重载,但同一 同一块中的本地子程序可以重载 包内的打包子程序可以重载 * 包D B M S _ S H A R E D _ P O O L将在5 . 4 . 1节中介绍。 5.2 存储子程序和包的几个问题 作为数据字典对象的存储子程序和包具有自身独特的优势,例如,这类程序可以由多个数 据库用户共享等。然而,在使用中,我们必须还要注意到有关存储子程序和包的几种特性。其 中包括存储子程序间的相关性,包状态的处理方法,以及运行存储子程序和包所需的特权等。 5.2.1 子程序的相关性 当我们编译存储过程或函数时,该过程或函数引用的所有O r a c l e对象都将记录在数据字典中。该过程就依赖于这些存储的对象。在前面几节中,我们已经看到了在数据字典中显示了标志为 非法的有编译错误的子程序。同样,如果一个 D D L操作运行在其所相关的对象上时,存储子程 序也将是非法的。理解该问题的最好方式是通过例子进行说明。我们在第 4章中定义的函数 A l m o s t F u l l要查询表c l a s s e s。图5 - 3演示了函数A l m o s t F u l l的相关性。A l m o s t F u l l只与一个对象相 关,即表c l a s s e s。图5 - 3中的箭头标识了A l m o s t F u l l的相关对象。 现在,让我们假设创建了调用 A l m o s t F u l l的过程并把结果插入到表 t e m p _ t a b l e中。过程 R e c o r d F u l l C l a s s e s的代码如下: 图5-3 函数A l m o s t F u l l的相关图示 节选自在线代码RecordFullClasses.sql CREATE OR REPLACE PROCEDURE RecordFullClasses AS CURSOR c_Classes IS SELECT department, course FROM classes; BEGIN FOR v_ClassRecord IN c_Classes LOOP -- Record all classes that don't have very much room left -- in temp_table. IF AlmostFull(v_ClassRecord.department, v_ClassRecord.course) THEN INSERT INTO temp_table (char_col) VALUES (v_ClassRecord.department || ' ' || v_ClassRecord.course || ' is almost full!'); END IF; END LOOP; END RecordFullClasses; 过程R e c o r d F u l l C l a s s e s的相关性如图5 - 4所示。过程R e c o r d F u l l C l a s s e s与函数A l m o s t F u l l和表 t e m p _ t a b l e相关。由于过程R e c o r d F u l l C l a s s e s直接引用函数A l m o s t F u l l和表t e m p _ t a b l e,这种相关 就是直接相关。函数 A l m o s t F u l l又与表c l a s s e s相关,因此过程R e c o r d F u l l C l a s s e s就与表c l a s s e s间 接相关。 如果一个D L L执行了对表c a l s s e s的操作,则所有与表c l a s s e s相关的对象(直接或间接)都将 156计计第二部分 非对象功能 下载 表classes 函数AlmostFull 该函数直接与表classes 相关处于无效状态。如下所示,假设我们在例子中的表 c l a s s e s中增加一列使其发生变更: ALTER TABLE classes ADD ( student_rating NUMBER(2) -- Difficulty rating from 1 to 10 ); 该表的变更将导致与表 c l a s s e s相关的函数A l m o s t F u l l和过程R e c o r d F u l l C l a s s e s变为非法。图 5 - 5所示的SQL *Plus会话中窗口显示了非法错误信息。 1. 自动重编译 如果相关的对象处于非法状态的话, P L / S Q L引擎将在该对象再次被调用时对其重新进行编 译。由于函数A l m o s t F u l l和过程R e c o r d F l l C l a s s e s都没有引用表c a l s s e s中新增的列,所以重编译可 以顺利通过。继续图5 - 5会话的SQL *Plus窗口图5 - 6演示了重编译的结果。 图5-4 RecordFlillClasses 的相关图示 警告 自动重编也可能失败(特别是在修改了表说明的情况下)。这时,调用块将收到 一条编译错误信息。但该错误是在运行时生成,而不在编译后给出。 2. 包和相关性 如同上面例子所演示的,存储子程序在其相关的对象变更时将会处于非法状态。然而,包 的情况有所不同。请看图 5 - 7所示的包C l a s s P a c k a g e的相关图。该包体与表 r e g i s t e r e d _ s t u d e n t s和 包头有关。但是,该包头与包体无关,仅与表 r e g i s t e r e d _ s t u d e n t s有关。这就是包的一个优点, 既包体的变化不会导致修改包头。因此,与该包头有关的其他对象也不需要进行重编。如果该 包头有变化,则将自动地作废其包体,这是因为该包体与其包头有关。 注意 确实存在着在某种情况下包体的改变将导致包头做相应的修改。例如,如果某个 过程在其包体和说明部分的参数在其包体中发生变化,则该包头也必须做相应的修改以 第5章 使用子程序和包计计157下载 AlmostFull直接与表 classes相关 函数AlmostFull 过程RecoldFullClasses表temm-table 表classes 过程RecordFullclasses直接与 temp-table和AlmostFull相关, 与表classes间接相关适应这种变化。如果只是对实现过程的包体做了修改而没有影响其声明部分,则该包头 就可以不做修改。相类似,如果使用了特征相关模式(本章下文小节介绍),则仅对包 说明部分的对象特征修的改将会导致包体非法。同样,如果在包头中增加了一个对象 158计计第二部分 非对象功能 下载 图5-5 DDL操作导致的非法信息 图5-6 出现非法错误后的自动编译(如游标或变量),则包体将被作废。 图5-7 包C l a s s e s P a c k a g e的相关图 从下面的SQL *Plus会话中我们也可以看出上述情况: 节选自在线代码dependencies.sql SQL> -- First create a simple table. SQL> CREATE TABLE simple_table (f1 NUMBER); Table created. SQL> -- Now create a packaged procedure that references the table. SQL> CREATE OR REPLACE PACKAGE Dependee AS 2 PROCEDURE Example(p_Val IN NUMBER); 3 END Dependee; 4 / Package created. SQL> CREATE OR REPLACE PACKAGE BODY Dependee AS 2 PROCEDURE Example(p_Val IN NUMBER) IS 3 BEGIN 4 INSERT INTO simple_table VALUES (p_Val); 5 END Example; 6 END Dependee; 7 / Package body created. SQL> -- Now create a procedure that references Dependee. SQL> CREATE OR REPLACE PROCEDURE Depender(p_Val IN NUMBER) AS 2 BEGIN 3 Dependee.Example(p_Val + 1); 4 END Depender; 5 / Procedure created. SQL> -- Query user_objects to see that all objects are valid. SQL> SELECT object_name, object_type, status 第5章 使用子程序和包计计159下载 包头 包体 表registerd_studentclassPackage包体与包头有关,但 包头与其包体无关 classPackage的包体与表 registered-student有关,而包头与 该表无关2 FROM user_objects 3 WHERE object_name IN ('DEPENDER', 'DEPENDEE', 4 'SIMPLE_TABLE'); OBJECT_NAME OBJECT_TYPE STATUS ------------------------------ ------------- ------- SIMPLE_TABLE TABLE VALID DEPENDEE PACKAGE VALID DEPENDEE PACKAGE BODY VALID DEPENDER PROCEDURE VALID SQL> -- Change the package body only. Note that the header is SQL> -- unchanged. SQL> CREATE OR REPLACE PACKAGE BODY Dependee AS 2 PROCEDURE Example(p_Val IN NUMBER) IS 3 BEGIN 4 INSERT INTO simple_table VALUES (p_Val - 1); 5 END Example; 6 END Dependee; 7 / Package body created. SQL> -- Now user_objects shows that Depender is still valid. SQL> SELECT object_name, object_type, status 2 FROM user_objects 3 WHERE object_name IN ('DEPENDER', 'DEPENDEE', 4 'SIMPLE_TABLE'); OBJECT_NAME OBJECT_TYPE STATUS ------------------------------ ------------- ------- SIMPLE_TABLE TABLE VALID DEPENDEE PACKAGE VALID DEPENDEE PACKAGE BODY VALID DEPENDER PROCEDURE VALID SQL> -- Even if we drop the table, it only invalidates the SQL> -- package body. SQL> DROP TABLE simple_table; Table dropped. SQL> SELECT object_name, object_type, status 2 FROM user_objects 3 WHERE object_name IN ('DEPENDER', 'DEPENDEE', 4 'SIMPLE_TABLE'); OBJECT_NAME OBJECT_TYPE STATUS ------------------------------ ------------- ------- DEPENDEE PACKAGE VALID DEPENDEE PACKAGE BODY INVALID DEPENDER PROCEDURE VALID 160计计第二部分 非对象功能 下载第5章 使用子程序和包计计161下载 注意 数据字典视图u s e r _ d e p e n d e n c i e s , a l l _ d e p e n d e n c i e s ,和d b a _ d e p e n d e n c i e s直接列出了 模式对象间的关系。有关这些视图的介绍,请看本书 C D - R O M中的附录C部分。 图5 - 8演示了由脚本创建的对象的相关图。 3. 如何确认非法状态 当对象变更时,其相关的对象就会变成非法对象,我们在上面的例子中已经看到了这一点。 如果所有的对象都在同一个数据库中的话,则相关的对象将会在底层对象变更的同时进入非法 状态。由于数据字典在不断地跟踪对象间的相关,所以这种变化可以快速反应出来。假设我们 创建了过程P 1和P 2,如图5 - 9所示,P 1依赖于P 2,也就是说,对P 2进行重编译将会导致 P 1非法。 下面的SQL *Plus会话演示了这一过程: 图5-8 多个包的相关图示 图5-9 在同一数据库中的过程P 1和P 2 Depender直接与 包头相关 (包头) (包体) 包体与表simple_table与 包头相关节选自在线代码remoteDependencies.sql SQL> -- Create two procedures. P1 depends on P2. SQL> CREATE OR REPLACE PROCEDURE P2 AS 2 BEGIN 3 DBMS_OUTPUT.PUT_LINE('Inside P2!'); 4 END P2; 5 / Procedure created. SQL> CREATE OR REPLACE PROCEDURE P1 AS 2 BEGIN 3 DBMS_OUTPUT.PUT_LINE('Inside P1!'); 4 P2; 5 END P1; 6 / Procedure created. SQL> -- Verify that both procedures are valid. SQL> SELECT object_name, object_type, status 2 FROM user_objects 3 WHERE object_name IN ('P1', 'P2'); OBJECT_NAME OBJECT_TYPE STATUS ------------------------------ --------------- ------- P2 PROCEDURE VALID P1 PROCEDURE VALID SQL> -- Recompile P2, which invalidates P1 immediately. SQL> ALTER PROCEDURE P2 COMPILE; Procedure altered. SQL> -- Query again to see this. SQL> SELECT object_name, object_type, status 2 FROM user_objects 3 WHERE object_name IN ('P1', 'P2'); OBJECT_NAME OBJECT_TYPE STATUS ------------------------------ --------------- ------- P2 PROCEDURE VALID P1 PROCEDURE INVALID 假设,过程P 1和P 2位于不同的数据库,并且 P 1通过数据库连接来调用 P 2。图5 - 1 0所示的就 是上述调用方式,在这种情况下,如果重编译 P 2将不会立即影响P 1,下面的SQL *Plus演示了这 一过程: 节选自在线代码remoteDependencies.sql SQL> -- Create a database link that points back to the current SQL> -- instance. SQL> CREATE DATABASE LINK loopback 2 USING 'connect_string'; 162计计第二部分 非对象功能 下载第5章 使用子程序和包计计163下载 Database link created. SQL> -- Change P1 to call P2 over the link. SQL> CREATE OR REPLACE PROCEDURE P1 AS 2 BEGIN 3 DBMS_OUTPUT.PUT_LINE('Inside P1!'); 4 P2@loopback; 5 END P1; 6 / Procedure created. SQL> -- Verify that both are valid. SQL> SELECT object_name, object_type, status 2 FROM user_objects 3 WHERE object_name IN ('P1', 'P2'); OBJECT_NAME OBJECT_TYPE STATUS ------------------------------ --------------- ------- P2 PROCEDURE VALID P1 PROCEDURE VALID SQL> -- Now when we recompile P2, P1 is not invalidated immediately. SQL> ALTER PROCEDURE P2 COMPILE; Procedure altered. SQL> SELECT object_name, object_type, status 2 FROM user_objects 3 WHERE object_name IN ('P1', 'P2'); OBJECT_NAME OBJECT_TYPE STATUS ------------------------------ --------------- ------- P2 PROCEDURE VALID P1 PROCEDURE VALID 注意 在这个例子中,数据库连接实际上是一个回送环( l o o p b a c k),总是指向相同的 数据库。然而,我们所看到的就象 P 1和P 2各自处于不同的数据库一样。使用回送环可 以使我们在一个S E L E C T语句中查询P 1和P 2的状态。 图5-10 处于不同数据库中的过程 P 1和P 2164计计第二部分 非对象功能 下载 为什么在远程调用下的过程看起来有所不同呢?答案就在于数据字典并不跟踪远程相关对 象。实际上,由于远程对象可能位于不同的数据库中,因此要将所有相关远程对象作废实际上 是不可能的(如果远程对象处于无效期的话,数据字典可能无法对其进行访问)。 与上不同的是,远程对象的合法性要在运行时进行检查。当过程 P 1被调用时,就要访问远 程数据字典来确定过程 P 2的状态(如果远程数据库不能访问的话,就会引发异常)。这时,要对 P 1和P 2进行比较以决定是否对过程 P 1需要重新编译。比较过程的方法有两个,一种方法是时间 戳法(Ti m e s t a m p),另一种是标记法(S i g n a t u r e)。 注意 实际上没有必要使用数据库连接来进行运行合法检查。如果 P 1在客户端的 P L / S Q L引擎中(如在Oracle Forms中运行),而P 2在服务器端,那么情况就会类似,上 述两种比较方法都可以使用。有关 P L / S Q L运行环境的介绍,请看本书第 2章内容。 时间戳模型 在这种模型下,将对最后修改的两个对象的时间戳进行比较。表 u s e r _ o b j e c t s 的L A S T _ D D L _ T I M E字段包含有对象的时间戳。如果底层对象的时间戳要比其相关的对象的时 间戳新,则相关对象将进行重编译。然而,时间戳模型有下列问题需要注意: • 时间的比较并没有把两个对象所在的 P L / S Q L引擎的位置考虑在内。如果两个引擎位于不 同的时区的话,那么这种比较就没有意义。 • 即使上述两个引擎位于同一时区,时间戳法仍然会引起不必要的重编译。在上面的例子中, P 2实际上没有变更,但还是进行了重编译。这时, P 1没有必要重编译,但由于 P 1的时间戳 老一些,故还要对其重编译。 • 稍微严重的问题是,当P 1属于客户端P L / S Q L引擎时,如运行在Oracle Forms软件下。在这 种情况下,由于该过程的源代码可能不在 F o r m s软件的运行版本中存储,所以无法对其重 编译。 标记法模型 从P L / S Q L 2 . 3版开始,P L / S Q L提供了另一种叫做标记法的比较算法用 来克服时间戳法存在的问题。每当创建一个过程时,除去中间代码外,还把一个标 记存储在该过程的数据字典中。该标记将过程的类型和参数顺序进行编码。使用这种方法,过 程P 2的标记将只在其参数变更时改变。当过程 P 1第一次编译时,P 2的标记就被加入(不是记录 时间戳)。因此,过程P 1只有在过程P 2变更时才需要重编译。 为了使用标记法,要将系统参数 R E M O T E _ D E P E N D E N C I E S _ M O D E设置为S I G N AT U R E。 它是数据库初始化文件中的一个参数。(该初始化文件一般是 i n i t . o r a,其名称和位置与数据库系 统有关。)该参数也可交互设置。下面是设置该参数的三种方法: • 把命令行REMOTE_DEPENDENCIES_MODE= SIGNAT U R E加入到数据库初始文件中。当 数据库再次启动时,将为所有会话设置 S I G N AT U R E。 • 提交下面的命令 ALTER SYSTEM SET REMOTE_DEPENDENCIES_MODE=SIGNATURE; 该命令将从其提交开始对所有数据库会话生效。发布该命令要具有 A LTER SYSTEM的系统 特权。 • 提交下面的命令第5章 使用子程序和包计计165下载 ALTER SESSION SET REMOTE_DEPENDENCIES_MODE=SIGNATURE; 该命令只对会话有效。在该命令后当前会话中创建的对象将使用标记法。 在以上的所有选择项中,参数 T I M E S TA M P可以用来取代参数 S I G N AT U R E来适应 P L / S Q L 2 . 2版或更低版本的环境。参数 T I M E S TA M P是默认值。 下面是使用标记法的一些注意事项: • 如果形参的默认值变更的话,标记将不被修改。假设过程 P 2的一个参数有默认值,而过程 P 1正在使用该默认值。如果在 P 2的说明部分修改了该默认值,则根据默认规则, P 1将不重 编译。这样一来,除非人工重编过程 P 1,否则该默认参数的旧值仍将被使用。以上规则仅 适用具有I N属性的参数。 • 如果过程P 1正在调用过程P 2,而P 2的新的重载版本已经追加到远程包中,这时,标记不做 变动。过程P 1仍将使用旧的版本(不是新的重载版本)直到过程 P 1手工重编译为止。 • 要用手工对过程进行重编,请使用下面的命令: ALTER RPCEDURE procedure_name COMPILE; 其中,p r o c e d u r e _ n a m e是要编译的过程名。对于函数,请使用下面的命令: ALTER FUNCTION function_name COMPILE; 对于包,可以使用下面两个命令中的一个: ALTER PACKAGE package_name COMPILE SPECIFICATION; ALTER PACKAGE package_name COMPILE BODY; 有关标记方法的详细介绍,请参考 O r a c l e服务器应用开发指南7 . 3版以上的文档资料。 5.2.2 包运行时状态 当对包进行第一次实例时,将从磁盘读入该包的中间代码代码并将其放入系统全局工作区 S G A的共享缓冲区中。然而,包的运行状态,即打包的变量和游标,将存放在用户全局区 (U G A)的会话存储区中。这就保证了每个会话都将有其自己包运行状态的副本。正如我们在第 4章中看到的,包头中声明的变量的作用域为全局范围,这些变量对于具有 E X E C U T E特权的任 何P L / S Q L块都是可见的。由于包的运行状态是在 U G A中存放的,所以它们具有与数据库会话相 同的生存期。当包被实例时,其运行状态也得到初始化(包中的初始化代码将启动运行),并且 这些状态直到会话结束才被释放。即使包本身由于超时被从共享缓冲区中清除,但该包的状态 仍将持续。下面的例子演示了包运行状态: 节选自在线代码PersistPkg.sql CREATE OR REPLACE PACKAGE PersistPkg AS -- Type which holds an array of student ID's. TYPE t_StudentTable IS TABLE OF students.ID%TYPE INDEX BY BINARY_INTEGER; -- Maximum number of rows to return each time. v_MaxRows NUMBER := 5; -- Returns up to v_MaxRows student ID's.166计计第二部分 非对象功能 下载 PROCEDURE ReadStudents(p_StudTable OUT t_StudentTable, p_NumRows OUT NUMBER); END PersistPkg; CREATE OR REPLACE PACKAGE BODY PersistPkg AS -- Query against students. Since this is global to the package -- body, it will remain past a database call. CURSOR StudentCursor IS SELECT ID FROM students ORDER BY last_name; PROCEDURE ReadStudents(p_StudTable OUT t_StudentTable, p_NumRows OUT NUMBER) IS v_Done BOOLEAN := FALSE; v_NumRows NUMBER := 1; BEGIN IF NOT StudentCursor%ISOPEN THEN -- First open the cursor OPEN StudentCursor; END IF; -- Cursor is open, so we can fetch up to v_MaxRows WHILE NOT v_Done LOOP FETCH StudentCursor INTO p_StudTable(v_NumRows); IF StudentCursor%NOTFOUND THEN -- No more data, so we're finished. CLOSE StudentCursor; v_Done := TRUE; ELSE v_NumRows := v_NumRows + 1; IF v_NumRows > v_MaxRows THEN v_Done := TRUE; END IF; END IF; END LOOP; -- Return the actual number of rows fetched. p_NumRows := v_NumRows - 1; END ReadStudents; END PersistPkg; 过程P e r s i s t P k g . R e a d S t u d e n t s将从游标S t u d e n t C u r s o r中进行选择。由于该游标是在包一级声 明的(不是在过程R e a d S t u d e n t s中声明的),该游标仍将保持过去对 R e a d S t u d e n t s的调用。我们可 以使用下面的块来调用过程 P e r s i s t P k g . R e a d S t u d e n t s:节选自在线代码callRS.sql DECLARE v_StudentTable PersistPkg.t_StudentTable; v_NumRows NUMBER := PersistPkg.v_MaxRows; v_FirstName students.first_name%TYPE; v_LastName students.last_name%TYPE; BEGIN PersistPkg.ReadStudents(v_StudentTable, v_NumRows); DBMS_OUTPUT.PUT_LINE(' Fetched ' || v_NumRows || ' rows:'); FOR v_Count IN 1..v_NumRows LOOP SELECT first_name, last_name INTO v_FirstName, v_LastName FROM students WHERE ID = v_StudentTable(v_Count); DBMS_OUTPUT.PUT_LINE(v_FirstName || ' ' || v_LastName); END LOOP; END; 图5 - 11是执行该块三次的输出结果。对于每次调用,由于该游标一直在两次调用间保持打开 状态,所以每次都返回不同的数据。 图5 - 11 调用过程R e a d S t u d e n t s的结果 第5章 使用子程序和包计计167下载168计计第二部分 非对象功能 下载 1. 可连续重用包 P L / S Q L 2 . 3版及更高版本允许程序员对包做连续可重用标志。可连续重用包的运行 状态将保存在S G A而不是U G A中,并仅在每个数据库调用期间有效。可连续重用包 的语法是: PRAGMA SERIALLY_REUSABLE; 该语句应在包头中(如果有包体的话,也在包体中)中使用。如果我们修改 P e r s i s t P k g使其包括 这个P R A G M A,其输出也将发生变化。图 5 - 1 2是该包的输出信息。下面的程序是修改过的包 P e r s i s t P k g: 图5-12 调用可连续重用包R e a d S t u d e n t s 节选自在线代码PersistPkg2.sql CREATE OR REPLACE PACKAGE PersistPkg AS PRAGMA SERIALLY_REUSABLE; -- Type that holds an array of student IDs. TYPE t_StudentTable IS TABLE OF students.ID%TYPE INDEX BY BINARY_INTEGER;第5章 使用子程序和包计计169下载 -- Maximum number of rows to return each time. v_MaxRows NUMBER := 5; -- Returns up to v_MaxRows student IDs. PROCEDURE ReadStudents(p_StudTable OUT t_StudentTable, p_NumRows OUT NUMBER); END PersistPkg; CREATE OR REPLACE PACKAGE BODY PersistPkg AS PRAGMA SERIALLY_REUSABLE; -- Query against students. Even though this is global to the -- package body, it will be reset after each database call, -- because the package is now serially reusable. CURSOR StudentCursor IS SELECT ID FROM students ORDER BY last_name; ... END PersistPkg; 值得注意的是包 P e r s i s t P k g的两个版本之间的差别。非连续重用包版的程序将跨越数据库来 维持游标状态,而可连续重用包版程序每次都要复位其状态(以及输出)。这两种版本的区别在 表5 - 2中给出: 表5-2 包的两种版本的区别 可连续重用包 非连续重用包 运行状态保存在 S G A中,每次数据库调 运行状态保存在U G A中,其生存期与数据库会话相同 用后都将该运行状态释放 所用的最大内存与包的并发用户数量成正比 所用的最大内存与当前登录的用户数目成正比 注意 可连续重用包的语义在 M T S(多线程服务器)下仍保持不变。在 M T S环境下, U G A将存储在共享存储器中以便会话可以在数据库服务器进程间迁移。因为可连续重 用包减少了对内存的需求,所以它们在 M T S下具有优势。有关 M T S的介绍,请参阅 O r a c l e文档。 2. 包运行状态的相关 除了在存储对象之间的存在着相关外,包状态和匿名块之间也有相关特性。例如,请看下 面的包: 节选自在线代码anonymousDependencies.sql CREATE OR REPLACE PACKAGE SimplePkg AS v_GlobalVar NUMBER := 1; PROCEDURE UpdateVar; END SimplePkg;170计计第二部分 非对象功能 下载 CREATE OR REPLACE PACKAGE BODY SimplePkg AS PROCEDURE UpdateVar IS BEGIN v_GlobalVar := 7; END UpdateVar; END SimplePkg; 包S i m p l e P k g包含了一个包全局量v _ G l o b a l Va r.假设我们从一个数据库会话创建包 S i m p l e P k g . 接着,在第二个会话中,我们使用下面的块来调用 S i m p l e P k g . U p d a t e Va r : BEGIN SimplePkg.UpdateVar; END; 现在返回第一个会话,我们运行创建脚本再次创建包 S i m p l e P k g。最后,我们在第二个会话 中提交同样的匿名块。下面是得到的输出信息: BEGIN * ERROR at line 1: ORA-04068: existing state of packages has been discarded ORA-04061: existing state of package "EXAMPLE.SIMPLEPKG" has been invalidated ORA-04065: not executed, altered or dropped package "EXAMPLE.SIMPLEPKG" ORA-06508: PL/SQL: could not find program unit being called ORA-06512: at line 2 上面的程序发生了什么问题?图 5 - 1 3是上述情况的相关图示。匿名块依赖于包 S i m p l e P k g。 这种相关是编译时的依赖性,也就是在匿名块首次编译时就确定的相关关系。然而,除此之外, 由于每个会话都有其自己包变量的复本,所以运行时包变量之间也存在着依赖关系。因此,当 重编S i m p l e P k g时,运行时相关就紧随其后,引发了错误 O RA- 4 0 6 8并作废了该块。 运行时相关仅存在于包状态之上,它包括包中的变量和游标声明。如果包没有全局变量的 话,则匿名块的第二次运行将会成功。 图5-13 包的全局相关图示 匿名块 匿名块依赖于SimplePky,并包括 v_Globa Var的实例5.2.3 特权和存储子程序 存储子程序和包都是数据库字典中的对象,因而,它们属于特殊的数据库用户或模式。如 果用户被授予了正确的特权,则它们就可以访问这些对象。特权和角色在创建存储对象时也与 子程序内部可行的访问一起开始活动。 1. EXECUTE特权 为了能够对表进行访问,必须使用 S E L E C T,I N S E RT,U P D AT E和D E L E T E对象的特权。 语句G R A N T把这些特权赋予数据库用户或角色。对于存储子程序和包来说,相关的特权是 E X E C U T E。现在请看下面的过程R e c o r d F u l l C l a s s s e s,该程序是5 . 2 . 1节中的案例: 节选自在线代码execute.sql CREATE OR REPLACE PROCEDURE RecordFullClasses AS CURSOR c_Classes IS SELECT department, course FROM classes; BEGIN FOR v_ClassRecord IN c_Classes LOOP -- Record all classes that don't have very much room left -- in temp_table. IF AlmostFull(v_ClassRecord.department, v_ClassRecord.course) THEN INSERT INTO temp_table (char_col) VALUES (v_ClassRecord.department || ' ' || v_ClassRecord.course || ' is almost full!'); END IF; END LOOP; END RecordFullClasses; 注意 在线案例e x e c u t e . s q l将首先创建用户U s e r A和U s e r B,接着再创建本节案例需要的 对象。读者可以修改用于 D B A帐户的口令,以便使该案例可以运行在读者所在的系统 上。除此之外,我们还提供了演示该程序输出结果的程序 e x e c u t e . o u t。 假设过程R e c o r d F u l l C l a s s s e s相关的对象(即函数A l m o s t F u l l ,表c l a s s e s和t e m p _ t a b l e)都属于 数据库用户U s e r A。并且R e c o r d F u l l C l a s s s e s也属于U s e r A。如果我们把R e c o r d F u l l C l a s s s e s的特权 赋予其他的数据库用户,如 U s e r B ,如下面的命令所示: 节选自在线代码execute.sql GRANT EXECUTE ON RecordFllClassses TO UserB; 这样一来,U s e r B就可以用下面的块来运行 R e c o r d F u l l C l a s s s e s。下面语句中使用的点符号用 于指示模式: 节选自在线代码execute.sql BEGIN UserA.RecordFullClasses; END; 在这种情况下,所有的数据库对象都属于 U s e r A。图5 - 1 4演示了U s e r A的所属的对象。该图 第5章 使用子程序和包计计171下载172计计第二部分 非对象功能 中的虚线表示从U s e r A到U s e r B的G R A N T语句,而图中的实线则表示对象的相关。在运行了本节 前面的该块代码后,其结果将插入表 U s e r A . t e m p _ t a b l e中。 现在假设 U s e r B有另外一个叫做 t e m p _ t a b l e的表,如图 5 - 1 5所示。如果 U s e r B调用U s e r A . 表temp-table 函数AlmostFull 表classes 结果存储在表 UserA.temp_table中 表temp-table 表classes 结果将存储在 UserA.temp_table中 表UserB.temp_table将不被 修改 图5-14 UserA所属的数据库对象 图5-15 UserA和U s e r B所属的表t e m p _ t a b l e 下载第5章 使用子程序和包计计173下载 R e c o r d F u l l C l a s s s e s (通过运行前面介绍的匿名块 ),哪个表将被修改呢?答案是 U s e r A的表将被修 改。上述概念可以如下描述: 子程序在其拥有者的优先集下运行 即使U s e r B正在调用属于U s e r A的R e c o r d F u l l C l a s s s e s。但标识符t e m p _ t a b l e经过求值也将指 向属于U s e r A,而不是U s e r B的表。 O r a c l e 8 i提供一个叫做调用权的新功能,借助于调用权,可以指定一个过程是否在其 拥有者或其调用者的特权下运行。详细内容请参阅下面的“调用权与定义权的比较” 内容。 2. 存储子程序和角色 现在让我们对图 5 - 1 5 所示的情况做一点修改。假设 U s e r A 不拥有表 t e m p _ t a b l e 或 R e c o r d F u l l C l a s s e s,而U s e r B则拥有这两个对象。我们再进一步假设已经对 R e c o r d F u l l C l a s s e s做 了修改使其显式地引用U s e r A所属的对象。修改后的程序如下所示: 节选自在线代码execute.sql CREATE OR REPLACE PROCEDURE RecordFullClasses AS CURSOR c_Classes IS SELECT department, course FROM UserA.classes; BEGIN FOR v_ClassRecord IN c_Classes LOOP -- Record all classes that don't have very much room left -- in temp_table. IF UserA.AlmostFull(v_ClassRecord.department, v_ClassRecord.course) THEN INSERT INTO temp_table (char_col) VALUES (v_ClassRecord.department || ' ' || v_ClassRecord.course || ' is almost full!'); END IF; END LOOP; END RecordFullClasses; 为了能够正确地编译 R e c o r d F u l l C l a s s e s,U s e r A必须把表 c l a s s e s的 S E L E C T特权和 A l m o s t F u l l的E X E C U T E特权赋予U s e r B。图5 - 1 6中的虚线代表了这种授权。同时,该授权必须显 式地执行,而不能通过角色来实现。由 U s e r A执行的下列授权将保证 UserB. RecordFullClassesd 的编译成功: 节选自在线代码execute.sql GRANT SELECT ON classes TO UserB; GRANT EXECUTE ON AlmostFull TO UserB; 而下面通过中间角色实现的授权将不起作用。图 5 - 1 7显示了该角色的作用。 节选自在线代码execute.sql CREATE ROLE UserA_Role; GRANT SELECT ON classes TO UserA_Role; GRANT EXECUTE ON AlmostFull TO UserA_Role;GRANT UserA_Role to UserB; 图5-16 UserB所属的R e c o r d F u l l C l a s s e s 图5-17 通过角色实现授权 根据上面的例子,我们可以把上一节中的内容总结如下: 174计计第二部分 非对象功能 下载 表classes 表temp_table 由于授权是通过角色实现的,所以不对 UserB.RecordFullClasses编译 表classes 表temp_table 结果将存储在表 UserB.temp_table中子程序运行在其被显示地授权的所有者的特权下。 如果通过角色实现授权,则当我们试图编译 R e c o r d F u l l C l a s s e s时就会收到如下所示的 P L - 2 0 1 错误提示: PLS-201: identifier 'CLASSES' must be declared PLS-201: identifier 'ALMOSTFULL' must be declared 该规则也适用于存储在数据库中的触发器和包。特别对于存储过程内部的函数,包,或触 发器的对象(O r a c l e 7 . 3及更高版本)都是子程序拥有者所属的对象,或者是显式赋予拥有者的 对象。 为什么有这种限制呢?为了回答这个问题,需要我们来介绍有关联编的概念。 P L / S Q L使用 了前联编技术,也就是在编译子程序时而不是在运行时就对引用求值。语句 G R A N T和R E V O K E 都是D D L命令,该命令运行时立即生效,其新的特权将记录在数据字典中。所有数据库会话都 可以看到该新特权组。然而,对于角色来说,这种方法就没有必要。角色可以赋予一个用户, 用户可以使用SET ROLE命令来选择取消角色。其区别是命令 SET ROLE只能作用在一个数据库 会话上,而G R A N T和R E V O K E可以对所有会话有效。我们可以在一个会话中禁止一个角色,而 在另一个会话中启动该角色。 为了实现将通过角色授权的特权作用在子程序内部和触发器中,就必须在每次运行过程时 对该特权进行检查。编译程序在其联编中对特权进行检查。但前联编是在编译时检查特权,而 不是在运行时检查。为了保证前联编执行,存储过程和触发器内部的所有角色都将被禁止。 3. 调用权与定义权的比较 现在让我们来考虑本章 5 . 2 . 3节中介绍的例子,以及图 5 - 1 5演示的相关图示。在上述 情况下,U s e r A和U s e r B都有表t e m p _ t a b l e的副本,由于R e c o r d F u l l C l a s s e s属于U s e r A, 所以插入U s e r A . t m e p _ t a b l e . R e c o r d F u l l C l a s s e s就被叫做定义权过程,其原因就是其内部不合格的 外部引用是在其所有者或定义者的特权下实现的。 O r a c l e 8 i提供了不同的外部引用解决方案。在调用权子程序中,外部引用是通过调用者而不 是拥有者的特权组实现的。调用权程序是由 A U T H I D子句实现的,该语句只适用于独立子程序, 包说明和对象类型说明。在包内部或对象类型中的独立子程序必须都是调用权或都是定义权, 不能有混合类型。A U T H I D的语法如下: CREATE [OR REPLACE] FUNCTION function_name [ parameter_list] RETURN return_type [AUTHID {CURRENT_USER | DEFINER}] {IS | AS} function_body; CREATE [OR REPLACE] PROCEDURE procedure_name [ parameter_list] [AUTHID {CURRENT_USER | DEFINER}] {IS | AS} function_body; CREATE [OR REPLACE] PACKAGE package_spec_name [AUTHID {CURRENT_USER | DEFINER}] {IS | AS} 第5章 使用子程序和包计计175下载176计计第二部分 非对象功能 下载 package_spec; CREATE [OR REPLACE] TYPE type_name [AUTHID {CURRENT_USER | DEFINER}] {IS | AS} OBJECT type_spec; 如果在子句A U T H I D中说明了参数C U R R E N T _ U S E R,则该对象将具有调用权。如果说明了 D E F I N E R,则该对象就具有定义权。如果没有使用 A U T H I D子句的话,其默认值将是定义权。 例如,下面版本的R e c o r d F u l l C l a s s e s是调用权过程: 节选自在线代码invokers.sql CREATE OR REPLACE PROCEDURE RecordFullClasses AUTHID CURRENT_USER AS -- Note that we have to preface classes and AlmostFull with -- UserA, since both of these are owned by UserA only. CURSOR c_Classes IS SELECT department, course FROM UserA.classes; BEGIN FOR v_ClassRecord IN c_Classes LOOP -- Record all classes that don't have very much room left -- in temp_table. IF UserA.AlmostFull(v_ClassRecord.department, v_ClassRecord.course) THEN INSERT INTO temp_table (char_col) VALUES (v_ClassRecord.department || ' ' || v_ClassRecord.course || ' is almost full!'); END IF; END LOOP; END RecordFullClasses; 注意 在线案例invokers.sql 将首先创建用户U s e r A和U s e r B,接着再创建本节案例需要 的对象。读者可以修改用于 D B A帐户的口令,以便使该案例可以运行在读者所在的系 统上。除此之外,我们还提供了演示该程序输出结果的程序 i n v o k e r s . o u t。 如果U s e r B运行了R e c o r d F u l l C l a s s e s,插入操作将在表 U s e r B . t e m p _ t a b l e上执行。如果U s e r A 运行该过程,则插入操作将在表 U s e r A . t e m p _ t a b l e上执行。下面的SQL *Plus会话和图5 - 1 8演示 了上述执行过程: 节选自在线代码invokers.sql SQL> connect UserA/UserA Connected. SQL> -- Call as UserA. This will insert into UserA.temp_table. SQL> BEGIN 2 RecordFullClasses; 3 COMMIT; 4 END; 5 /第5章 使用子程序和包计计177下载 PL/SQL procedure successfully completed. SQL> -- Query temp_table. There should be 1 row. SQL> SELECT * FROM temp_table; NUM_COL CHAR_COL ---------- -------------------------------------------------------- MUS 410 is almost full! SQL> -- Connect as UserB. SQL> -- Now the call to RecordFullClasses will insert into SQL> -- UserB.temp_table. SQL> BEGIN 2 UserA.RecordFullClasses; 3 COMMIT; 4 END; 5 / PL/SQL procedure successfully completed. SQL> -- So we should have one row here as well. SQL> SELECT * FROM temp_table; NUM_COL CHAR_COL ---------- -------------------------------------------------------- MUS 410 is almost full! 图5-18 具有调用权的过程R e c o r d F u l l C l a s s e s 使用调用权的解决方案 在调用权子程序中,S Q L语句中的外部引用将通过调用者特权组求 表temp_table 调用权 表classes 表temp_table 如果UserB调用了RecordFull_ Classes,则结果将存储在该表中 如果UserA调用了RecordFull_ Classes,则结果将存储在这里178计计第二部分 非对象功能 下载 值。然而,在P L / S Q L语句中的引用(如赋值语句和过程调用语句)还是在其拥有者的特权组下 求值。这是为什么呢?图 5 - 1 8中,G R A N T语句仅作用在过程 R e c o r d F u l l C l a s s e s和表c l a s s e s上。 由于对A l m o s t F u l l的调用是一个P L / S Q L语句,所以该调用总是在U s r e A特权组下实现,因此,不 必对U s e r B使用G R A N T语句。 然而,假设对表 c l a s s s e s的G R A N T语句没有实现。这时,由于所有的 S Q L对象都可以在 U s e r A的特权组下访问,所以 U s e r A可以成功地编译该过程。但是 U s e r B将会在调用过程 R e c o r d F u l l C l a s s e s时收到O R A - 9 4 2的错误信息。图5 - 1 9和下面的SQL *Plus会话演示了上述情况: 节选自在线代码invokers.sql SQL> connect UserB/UserB Connected. SQL> BEGIN 2 UserA.RecordFullClasses; 3 END; 4 / BEGIN * ERROR at line 1: ORA-00942: table or view does not exist ORA-06512: at "USERA.RECORDFULLCLASSES", line 7 ORA-06512: at "USERA.RECORDFULLCLASSES", line 10 ORA-06512: at line 2 图5-19 撤消对表c l a s s e s的S E L E C T操作 注意 这里收到的错误信息是 O R A - 9 4 2,而不是P L S - 2 0 1。该错误是数据库编译错误, 如果UserA调用RecordFull_ Classes,则结果将存储在该表中 如果UserB调用了RecordFull_ Classes,则引发错误ORA-942第5章 使用子程序和包计计179下载 但我们却在运行时接收了该信息。 角色和调用权 假设对表c l a s s e s的授权语句G R A N T是通过角色间接实现的。请回忆一下我 们在图5 - 1 7中演示的定义权过程必须进行显式授权的规则。对于调用权程序来说,该规则不适 用。由于对调用权程序的外部引用是在运行时实现的,所以当前的特权组是可用的。这就说明 通过角色赋予调用者的特权将可以访问。图 5 - 2 0和下面的SQL *Plus会话演示了上述过程: 图5-20 角色和调用权图示 节选自在线代码invokers.sql SQL> connect UserA/UserA Connected. CREATE ROLE UserA_Role; Role created. SQL> GRANT SELECT ON classes TO UserA_Role; Grant succeeded. SQL> GRANT UserA_Role TO UserB; Grant succeeded. SQL> -- Connect as UserB and call. SQL> connect UserB/UserB Connected. SQL> -- Now the call to RecordFullClasses will succeed. SQL> BEGIN 2 UserA.RecordFullClasses; 3 COMMIT; 如果UserA调用了RecordFull_ Classes,则结果将存储在该表 如果UserB调用了RecordFull_ Classes,则结果将存储在该表中4 END; 5 / PL/SQL procedure successfully completed. 注意 在过程编译时求值的引用也必须直接授权。只有在运行时求值的引用可以通过角 色间接授权。该规则也说明命令 SET ROLE(如果在动态S Q L下执行)可以与运行时引 用一同使用。 外部例程和调用权 按默认值,使用J a v a语言编制的外部例程(也叫做 J a v a存储过程)将继 承调用权,这一点与 P L / S Q L例程的默认值不同。该规则与 J a v a方法调用模式保持一致。如果希 望J a v a存储过程带有定义权运行的话,可以使用子句 AUTHID DEFINER来说明调用。本书的第 1 0章将对这一规则做进一步介绍。 触发器、视图和调用权 数据库触发器总是带有定义权,并运行在其拥有触发器表模式的特 权下。该规则也适用于从视图中调用的 P L / S Q L函数。这时,该函数将运行在其视图的拥有者的 特权下。 5.3 在S Q L语句中使用存储函数 总的来说,由于子程序调用是程序性命令,所以不能从 S Q L语句中调用过程。然而,从 P L / S Q L 2 . 1版开始对存储函数取消了该限制。如果独立的或打包的函数满足某些条件的话,就可 以从S Q L语句的运行中对其进行调用。该功能只能用于 P L / S Q L 2 . 1版(O r a c l e 7的7 . 1版)及更高 版本。 O r a c l e 8 i则对该功能进行了进一步加强。 用户定义函数的调用方法与内置函数如 TO _ C H A R , U P P E R ,或A D D _ M O N T H S等使用方法相 同。根据用户定义函数的使用位置以及程序使用的 O r a c l e数据库版本,这种调用要满足不同的要 求。调用限制按纯层定义。 5.3.1 纯层 函数有四个不同的纯层( purity levels),纯层定义了函数所读或修改的数据结构。表 5 - 3列 出了可用的纯层。根据函数所处的纯层,调用将受到如下限制: • 从S Q L语句中执行的任何函数调用不能修改任何数据库表( W N D S)。(在O r a c l e 8 i中,从 非S E L E C T语句中执行的函数调用则可以修改数据库表。请看 5 . 3 . 3节。) • 为了实现远程运行(通过数据库连接)或并行运行,函数不能读或写包变量的值( R N P S 和W N P S)。 • 从S E L E C T,VA L U E S,或S E T子句执行的函数调用可以写包变量。在所有其他子句中的 函数都必须是W N P S纯层。 • 函数要与它所调用的子程序具有同样的纯层。例如,如果函数调用了执行 U P D AT E功能的 存储过程的话,该函数就不具有 W N D S纯层,因此,该调用不能使用在选择语句的内部。 • 不管存储P L / S Q L函数具有何种纯层,都不能从 C R E ATE TA B L E或A LTER TA B L E命令的 C H E C K限制子句中调用该存储P L / S Q L函数。也不能使用该存储 P L / S Q L函数来说明列的默 认值,其原因是这几种操作都要求不变定义。 180计计第二部分 非对象功能 下载表5-3 函数的纯层 纯 层 含 义 说 明 W N D S 不能写数据库状态 函数不能修改任何数据库表(使用 D M L语句) R N D S 不能读数据库状态 函数不能读数据库表(使用 S E L E C T语句) W N P S 不能写包状态 函数不能修改任何包变量(包变量不能出现在赋值语句的左边以及 F E T C H语句中) R N P S 不能读包状态 函数不能使用任何包变量(包变量不能出现在赋值语句的右边或作为过 程的一部分,以及S Q L表方式中) 除了上述的限制外,用户定义的函数还必须满足下面的要求才能实现从 S Q L语句中的调用。 除了用户定义的函数外,所有内置函数也必须遵循下列要求。 • 函数必须要存储在数据库中,其形式可以是独立的函数,或作为包的一部分。除此之外, 函数之间不能具有本地的关系。 • 函数只能使用I N参数,不能使用IN OUT或O U T参数。 • 形参只能使用数据库类型,不能使用 P L / S Q L类型,如B O O L E A N,或R E C O R D类型。数 据库类型可以是N U M B E R,C H A R,VA R C H A R 2,R O W I D,L O N G,R AW,LONG RAW, 以及D AT E和O r a c l e 8 i引入的新数据类型。 • 函数的返回类型也必须是数据库类型。 • 函数不能使用 C O M M I T或R O L L B A C K结束当前的事务,或返回到函数运行前的断点 (S a v e p o i n t). • 函数也不能提交任何A LTER SESSION 或A LTER SYSTEM命令。 作为函数调用的例子,下面的函数 F u l l N a m e以学生I D号为输入并返回学生的全名。 节选自在线代码F u l l N a m e . s q l CREATE OR REPLACE FUNCTION FullName ( p_StudentID students.ID%TYPE) RETURN VARCHAR2 IS v_Result VARCHAR2(100); BEGIN SELECT first_name || ' ' || last_name INTO v_Result FROM students WHERE ID = p_StudentID; RETURN v_Result; END FullName; 函数F u l l N a m e满足了上述所有的要求,因此我们可以从 S Q L语句中调用该函数,下面是调 用该函数的SQL *Plus会话: 节选自在线代码FullName.sql SQL> SELECT ID, FullName(ID) "Full Name" 2 FROM students; 第5章 使用子程序和包计计181下载182计计第二部分 非对象功能 下载 ID Full Name --------- ------------------------------- 10000 Scott Smith 10001 Margaret Mason 10002 Joanne Junebug 10003 Manish Murgratroid 10004 Patrick Poll 10005 Timothy Taller 10006 Barbara Blues 10007 David Dinsmore 10008 Ester Elegant 10009 Rose Riznit 10010 Rita Razmataz 10011 Shay Shariatpanahy 12 rows selected. SQL> INSERT INTO temp_table (char_col) 2 VALUES (FullName(10010)); 1 row created. R E S T R I C T _ R E F E R E N C E S P L / S Q L引擎可以确认独立函数的纯层。当从 S Q L语句中调用函数时, P L / S Q L对函数的纯层 进行检查。如果该函数没有满足限制条件, P L / S Q L就返回一个错误。对于打包的函数,需要使 用编译标识R E S T R I C T _ R E F E R E N C E S(O r a c l e 8 i之前的版本)。该编译标识说明了给定函数的纯 层。其语法如下: PRAGMA RESTRICT_REFERENCES(subprogram_or_package_name, WNDS [,WNPS] [,RNDS] [,RNPS]); 其中,s u b p r o g r a m _ o r _ p a c k a g e _ n a m e是包的名称,或打包的子程序的名称。(O r a c l e 8以上的版本 可以使用关键字D E FA U LT。)由于W N D S是出现在S Q L语句中所有函数必须指定的参数,因此上 述编译标识中也必须使用该参数。(O r a c l e 8 i对该限制有所放松。)该语句中的纯层说明的顺序是 任意的。该编译标识与函数说明都应在包头中出现。例如,下面的包 S t u d e n t O p s两次使用了 R E S T R I C T _ R E F E R E N C E S: 节选自在线代码StudentOps.sql CREATE OR REPLACE PACKAGE StudentOps AS FUNCTION FullName(p_StudentID IN students.ID%TYPE) RETURN VARCHAR2; PRAGMA RESTRICT_REFERENCES(FullName, WNDS, WNPS, RNPS); /* Returns the number of History majors. */ FUNCTION NumHistoryMajors RETURN NUMBER; PRAGMA RESTRICT_REFERENCES(NumHistoryMajors, WNDS); END StudentOps; CREATE OR REPLACE PACKAGE BODY StudentOps AS -- Packaged variable to hold the number of history majors. v_NumHist NUMBER;第5章 使用子程序和包计计183下载 FUNCTION FullName(p_StudentID IN students.ID%TYPE) RETURN VARCHAR2 IS v_Result VARCHAR2(100); BEGIN SELECT first_name || ' ' || last_name INTO v_Result FROM students WHERE ID = p_StudentID; RETURN v_Result; END FullName; FUNCTION NumHistoryMajors RETURN NUMBER IS v_Result NUMBER; BEGIN IF v_NumHist IS NULL THEN /* Determine the answer. */ SELECT COUNT(*) INTO v_Result FROM students WHERE major = 'History'; /* And save it for future use. */ v_NumHist := v_Result; ELSE v_Result := v_NumHist; END IF; RETURN v_Result; END NumHistoryMajors; END StudentOps; 注意 在O r a c l e 8 i中,不再需要使用该编译标识。 P L / S Q L引擎可以在运行时根据需要校 验所有函数的纯层。有关详细信息,请参阅 5 . 3 . 3节内容。 使用R E S T R I C T _ R E F E R E N C E S的合理性 为什么打包的函数要使用这个编译标识参数, 而对独立的函数却没有这种限制呢?该问题的答案与包头和包体的关系有关。我们以前讲过调 用打包函数的P L / S Q L块只与该包头有关,而与包体无关。进一步说,当我们创建调用块时,其 包体甚至可以没有。其结果是, P L / S Q L编译器需要这个编译标识参数来确认打包函数的纯层, 以及验证该包体是否在其调用块中正确使用。此后,不管在什么时候包体被修改(或首次创建), 该函数的代码都要按编译标识进行检查。该标识只在在编译期间检查。 严格地讲,P L / S Q L引擎可以在运行时检验函数的纯层,就象 O r a l c e 8 i之前的版本对独立函 数进行运行时验证那样。然而,使用编译标识就可以通知 P L / S Q L引擎不在运行时进行纯层检查, 这样做的好处是可以提高运行效率。同时,这样做也确保了给定的子程序不会因为从 S Q L语句 中调用过程而失败。D E F A U L T关键字 如果没有使用编译标识R E S T R I C T _ R E F E R E N C E S与给定的打包 函数相关联的话,该函数就没有任何纯层可言。然而,如果使用 O r a c l e 8或更高的版 本,我们就可以更改包的默认纯层。关键字 D E FA U LT可用来代替编译标识中的子程序名: PRAGMA RESTRICT_REFERENCES(DEFAULT,WNDS [,WNPS] [,RNDS] [,RNPS]); 修改默认纯层后,包中所有后续的子程序都必须与说明的纯层一致。例如,请看下面的包 D e f a u l t P r a g m a : 节选自在线代码DefaultPragma .sql CREATE OR REPLACE PACKAGE DefaultPragma AS FUNCTION f1 RETURN NUMBER; PRAGMA RESTRICT_REFERENCES(f1, RNDS, RNPS); PRAGMA RESTRICT_REFERENCES(DEFAULT, WNDS, WNPS, RNDS, RNPS); FUNCTION f2 RETURN NUMBER; FUNCTION f3 RETURN NUMBER; END DefaultPragma; CREATE OR REPLACE PACKAGE BODY DefaultPragma AS FUNCTION f1 RETURN NUMBER IS BEGIN INSERT INTO temp_table (num_col, char_col) VALUES (1, 'f1!'); RETURN 1; END f1; FUNCTION f2 RETURN NUMBER IS BEGIN RETURN 2; END f2; -- This function violates the default pragma. FUNCTION f3 RETURN NUMBER IS BEGIN INSERT INTO temp_table (num_col, char_col) VALUES (1, 'f3!'); RETURN 3; END f3; END DefaultPragma; 该默认的编译标识(声明了所有的纯层 )将应用于函数 f 2和f 3。由于 f 3执行了插入表 t e m p _ t a b l e的操作,所以违反了编译标识声明,编译该包时将出现下列错误: PL/SQL: Compilation unit analysis terminated PLS-00452: Subprogram 'F3' violates its associated pragma 初始化部分 包初始化部分的代码也可以带有纯层。第一次调用包中的任何函数将导致包初 始化部分的启动运行。因此,打包函数就具有与包括该包的初始话部分相同的纯层。包的纯层 也是用R E S T R I C T _ R E F E R E N C E S实现的,但要用包名称代替函数名称。下面是包初始化的语 法: 184计计第二部分 非对象功能 下载第5章 使用子程序和包计计185下载 CREATE OR REPLACE PACKAGE StudentOps AS PRAGMA RESTRICT_REFERENCES (StudentOps, WNDS); ... END StudentOps; 重载函数 R E S T R I C T _ R E F E R E N C E S可以出现在包说明部分中函数说明后的任何位置。然 而,该说明只能对一个函数定义有效。对于重载函数,该编译标识可以对前一个编译标识后最 近的函数定义有效。在下面的例子中,每个编译标识都对其前一个编译标识后的 Te s t F u n c的版本 有效。 节选自在线代码Overload .sql CREATE OR REPLACE PACKAGE Overload AS FUNCTION TestFunc(p_Parameter1 IN NUMBER) RETURN VARCHAR2; PRAGMA RESTRICT_REFERENCES(TestFunc, WNDS, RNDS, WNPS, RNPS); FUNCTION TestFunc(p_ParameterA IN VARCHAR2, p_ParameterB IN DATE) RETURN VARCHAR2; PRAGMA RESTRICT_REFERENCES(TestFunc, WNDS, RNDS, WNPS, RNPS); END Overload; CREATE OR REPLACE PACKAGE BODY Overload AS FUNCTION TestFunc(p_Parameter1 IN NUMBER) RETURN VARCHAR2 IS BEGIN RETURN 'Version 1'; END TestFunc; FUNCTION TestFunc(p_ParameterA IN VARCHAR2, p_ParameterB IN DATE) RETURN VARCHAR2 IS BEGIN RETURN 'Version 2'; END TestFunc; END Overload; 下面的SQL *Plus会话演示了上面两个重载函数都可以从 S Q L中调用: 节选自在线代码Overload .sql SQL> SELECT Overload.TestFunc(1) FROM dual; OVERLOAD.TESTFUNC(1) -------------------------------------------------------- Version 1 SQL> SELECT Overload.TestFunc('abc', SYSDATE) FROM dual; OVERLOAD.TESTFUNC('ABC',SYSDATE) -------------------------------------------------------- Version 2186计计第二部分 非对象功能 下载 提示 作者个人倾向于在每个函数的后面都加上 R E S T R I C T _ R E F E R E N C E S,这样可以 清楚地表明该参数作用的函数。 内置包 P L / S Q L提供的内置包中的过程一般都不如 P L / S Q L 2 . 3版的过程纯。这些过程包括 D B M S _ O U T P U T, D B M S _ P I P E , D B M S _ A L E RT, D B M S _ S Q L ,和U T L _ F I L E。然而,稍后的版本中 在这些包中加入了必要的编译标识 P R A G M A。表5 - 3介绍了在常用包中加入编译标识 P R A G M A 的时间。由于编译标识没有必要从 O r a c l e 8 i开始使用,所以所有满足限制条件的内置包函数都可 以向O r a c l e 8 i那样用在S Q L语句中。如果在 O r a c l e 8 i中调用了内置包函数并且该函数没有满足限 制条件,则该调用将在运行时发生错误。 注意 编译标识已经通过R D B M S修补程序加入到上述某些包中,因此某些比表 5 - 4列出 的版本还要早的P L / S Q L也可以支持编译标识功能。可以通过检查包头的源代码来验证 某个 P L / S Q L版本的纯层。(通常,这些文件存放在根 $ O R A C L E _ H O M E 下的 r d b m s / a d m i n目录中)。 5.3.2 默认参数 从过程语句中调用一个函数时,如果该函数有形参的话,我们可以使用其默认值。然而, 如果我们从 S Q L语句调用函数时,则所有的参数都必须说明。除此之外,还要使用位置符号 (Positinal Notation),而不能使用命名符号( Name Notation)。下面的对函数F u l l N a m e的调用是 非法的: SELECT FullName(p_StudentID = > 10000) FROM dual; 表5-4 内置包的编译标识R E S T R I C T _ R E F E R E N C E S 包 加入编译标识的版本 D B M S _ A L E RT 不存在— R E G I S T E R包括一个C O M M I T命令 D B M S _ J O B 不存在—作业运行在分离的进程中,因此不能从 S Q L中调用 D B M S _ O U T P U T 7 . 3 . 3 D B M S _ P I P E 7 . 3 . 3 D B M S _ S Q L 不存在— E X E C U T E和PA R S E可以用来执行D D L语句,该语句将引发隐式地 C O M M I T命令 S TA N D A R D 7 . 3 . 3 (包括过程R A I S E _ A P P L I C AT I I O N _ E R R O R ) U T L _ F I L E 8 . 0 . 6 U T L _ H T T P 7 . 3 . 3 5.3.3 从O r a c l e 8 i的S Q L语句中调用函数 正如我们在前几节看到的,编译标识 R E S T R I C T _ R E F E R E N C E S强化了编译时的纯层 处理。对于O r a c l e 8 i之前的版本,打包函数需要设置编译标识来实现从 S Q L语句中的 函数调用。然而,从O r a c l e 8 i开始放宽了这种限制,如果没有设置编译标识的话,数据库将在运 行时验证纯层。 O r a c l e 8 i的这种新的规则对使用外部例程非常有利(使用 C或J a v a语言编制的外部例程)。在 这种情况下,由于P L / S Q L编译器并不对这些函数进行实际编译,所以 P L / S Q L编译器也就无法实施纯层检查。(请看本书第1 0章有关外部例程的介绍)因此对这类外部例程的纯层检查就只能在 运行时进行。 对外部函数的纯层检查只在 P L / S Q L运行时发现从S Q L语句中对该函数进行了调用时才实施。 并且,如果在该函数中有编译标识的话,纯层检查将不执行。其结果是,使用纯层检查可以节 省运行时间并且也可用来标识函数的状态。 例如,假设我们把下面函数 S t u d e n t O p s中的编译标识去掉: 节选自在线代码StudentOps2 .sql CREATE OR REPLACE PACKAGE StudentOps AS FUNCTION FullName(p_StudentID IN students.ID%TYPE) RETURN VARCHAR2; /* Returns the number of History majors. */ FUNCTION NumHistoryMajors RETURN NUMBER; END StudentOps; 重新编译该函数后,如下面所示的 SQL *Plus 会话,仍然可以从 S Q L语句中对函数进行调 用。 SQL> SELECT StudentOps.FullName(ID) 2 FROM students 3 WHERE major = 'History'; STUDENTOPS.FULLNAME(ID) ----------------------- Margaret Mason Patrick Poll Timothy Taller SQL> INSERT INTO temp_table (num_col) 2 VALUES (StudentOps.NumHistoryMajors); 1 row created. SQL> SELECT * FROM temp_table; NUM_COL CHAR_COL --------- -------- 3 如果企图从S Q L语句中调用非法函数的话, O r a c l e 8 i将发布一条‘ORA-14551: 不能在查询 中执行D M L操作’ 的错误信息。请看下面程序中的函数 I n s e r t Te m p : 节选自在线代码InsertTemp .sql CREATE OR REPLACE FUNCTION InsertTemp( p_Num IN temp_table.num_col%TYPE, p_Char IN temp_table.char_col%type) RETURN NUMBER AS BEGIN INSERT INTO temp_table (num_col, char_col) VALUES (p_Num, p_Char); RETURN 0; END InsertTemp; 第5章 使用子程序和包计计187下载如果我们从S E L E C T语句中调用该函数的话,下面是输出的调用结果: 节选自在线代码InsertTemp .sql SQL> SELECT InsertTemp(1, 'Hello') 2 FROM dual; SELECT InsertTemp(1, 'Hello') * ERROR at line 1: ORA-14551: cannot perform a DML operation inside a query ORA-06512: at "EXAMPLE.INSERTTEMP", line 6 ORA-06512: at line 1 1. TRUST关键字 尽管在O r a c l e 8 i中可以不再使用编译标识 R E S T R I C T _ R E F E R E N C E S(对外部函数已经不能 使用该标识)但在O r a c l e 8 i之前的编制的代码仍可以使用该参数。我们在上一节中提到过,使用 编译标识的好处是可以提高程序的运行效率。因此,也许存在着这种情况,即程序从一个没有 使用编译标识的函数中调用一个声明了纯层的函数。为了实现这种程序设计要求, O r a c l e 8 i提供 了一个可在编译标识中使用的新关键字,用来代替或辅助纯层参数,它就是 T R U S T。 如果使用了T R U S T的话,则在编译标识中列出的限制将失效。更确切的讲,编译程序视这 些限制为真。这就允许我们来编制不再使用编译标识 R E S T R I C T _ R E F E R E N C E S的新代码,并实 现从声明为纯的函数中调用这些新的函数。例如,请看下面的包: 节选自在线代码TrustPkg .sql CREATE OR REPLACE PACKAGE TrustPkg AS FUNCTION ToUpper (p_a VARCHAR2) RETURN VARCHAR2 IS LANGUAGE JAVA NAME 'Test.Uppercase(char[]) return char[]'; PRAGMA RESTRICT_REFERENCES(ToUpper, WNDS, TRUST); PROCEDURE Demo(p_in IN VARCHAR2, p_out OUT VARCHAR2); PRAGMA RESTRICT_REFERENCES(Demo, WNDS); END TrustPkg; CREATE OR REPLACE PACKAGE BODY TrustPkg AS PROCEDURE Demo(p_in IN VARCHAR2, p_out OUT VARCHAR2) IS BEGIN p_out := ToUpper(p_in); END Demo; END TrustPkg; 正在被该D M L语句修改的该包中Tr u s t P k g . To U p p e r是一个外部例程,它是一个用 J a v a编制的 函数体,其功能是将输入参数转换为大写返回。(我们将在第 1 0章讨论参数的转换方法)。由于 该函数体不在P L / S Q L中,关键字T R U S T就要与编译标识一起使用。这样一来,由于编译器承认 函数To U p p e r具有了W N D S纯层,所以我们可以从D e m o中调用该函数To Upper。 2. 从D M L语句中调用函数 在O r a c l e 8 i之前,从D M L语句中调用的函数不能更新数据库(也就是说,该函数必须声明为 188计计第二部分 非对象功能 下载第5章 使用子程序和包计计189下载 R N D S纯层)。然而,在O r a c l e 8 i下,该限制有所放宽。现在,从 D M L语句中调用的函数即不能 读正在被该D M L语句修改的数据库表也不能修改正在被该 D M L语句修改的数据库表,但该函数 可以更新其他的表。例如,请看下面的函数 U p d a t e Te m p : 节选自在线代码DMLUpdate .sql CREATE OR REPLACE FUNCTION UpdateTemp(p_ID IN students.ID%TYPE) RETURN students.ID%TYPE AS BEGIN INSERT INTO temp_table (num_col, char_col) VALUES(p_ID, 'Updated!'); RETURN p_ID; END UpdateTemp; 在O r a c l e 8 i之前,执行下面的更新语句将导致错误: 节选自在线代码DMLUpdate .sql UPDATE students SET major = 'Nutrition' WHERE UpdateTemp(ID) = ID; 而在O r a c l e 8 i下,上面的更新语句只对表 temp_table 进行了修改,而没有更新表 s t u d e n t s。 注意 从并行D M L语句调用的函数不能修改数据库以及当前处于修改状态的表。 5.4 包的辅助功能 我们将在本节讨论包的某些辅助功能,其中包括在共享缓冲区中锁定包以及讨论包体长度 的问题。对于O r a c l e 8 i ,我们将讨论优化选择项D E T E R M I N I S T I C和PA R A L L E L _ E N A B L E。 5.4.1 共享缓冲区锁定 共享缓冲区是存放已编译子程序中间代码的 S G A的一部分。当第一次调用存储子程序时, 该子程序的中间代码将从磁盘中转载到共享缓冲区中;一旦没有其他程序引用该子程序时,其 中间代码将被从共享缓冲区中清除;共享缓冲区中的对象的清除是按 L R U算法(最近使用最少 的对象先清除)执行的。 包D B M S _ S H A R E D _ P O O L允许程序员把一个对象锁定在共享缓冲区中。当该对象被锁定后, 除非由程序申请对其清除,否则不管共享缓冲区是否已满,或是否有程序访问该对象,该对象 将常驻在共享缓冲区中。这种处理方法有利于提高程序的运行效率,因为从系统的磁盘重新载 入对象要进行大量读写操作。锁定对象还有助于将共享缓冲区的碎片效应减至最小。包 D B M S _ S H A R E D _ P O O L有三个过程,它们分别是: D B M S _ S H R A E D _ P O O L . K E E P, D B M S _ S H A R E D _ P O O L . U N K E E P,以及D B M S _ S H A R E D _ P O O L . S I Z E S . 1. 过程K E E P 过程D B M S _ S H R A E D _ P O O L . K E E P用来在缓冲区中锁定对象。包、触发器( O r a c l e 7 . 3及更 高版本)、序列和 S Q L语句都可以被锁定。需要注意的是,独立过程和函数不能锁定。过程 K E E P的语法如下:190计计第二部分 非对象功能 下载 PROCEDURE KEEP(name VARCHAR2,flag CHAR DEFAULT'P'); 以上参数在下面的表 5 - 5中说明。一旦对象被锁定,除非在数据库关闭或使用了过程 D B M S _ S H A R E D _ P O O L . U N K E E P外,该对象将永不退出共享缓冲区。注意, D B M S _ S H R A E D _POOL.KEEP并不立即将包载入缓冲区中;而是对在其后载入的包实施锁定。 表5-5 过程K E E P的参数说明 参 数 类 型 说 明 n a m e VA R C H A R 2 对象的名称。该参数可以是包名或与 S Q L语句关联的标识符。S Q L标 识符是由视图$ s q l a r e a中的字段 h a s h _ v a l u e和a d d r e s s的连接组成(默认 情况下,只能通过S Y S选择)并由过程 S I Z E S返回 f l a g C H A R 决定对象的类型。如果该参数是‘ P’(默认值),则参数n a m e就必须 与包名匹配。如果该参数是’ C’(游标)的话,则 n a m e就要带有 S Q L 语句的文本。如果该参数是‘ S’,则n a m e就是序列,如果它是‘ R’, 则n a m e就是触发器 2. 过程U N K E E P 过程U N K E E P是从共享缓冲区中删除锁定对象的唯一方法。锁定的对象不会自动退出共享 缓冲区。U N K E E P的语法是: PROCEDURE UNKEEP(name VARCHAR2,flag CHAR DEFAULT ‘P’); 以上参数的含义与过程 K E E P的参数相同。如果说明的对象没有在共享缓冲区中的话,将会 引发错误。 3. 过程S I Z E S 该过程将把共享缓冲区的内容输出到屏幕。其语录法如下: PROCEDURE SIZES(minsize NUMBER); 执行该命令后,长度大于指定的 m i n s i z e的对象将显示在屏幕上。过程 S I Z E S使用包D B M S _ O U T P U T来返回数据,因此,在调用该过程前,一定要在 SQL *Plus或SQL 服务管理器中使用 SET SEVEROUPUT ON。 5.4.2 包体长度的限制 P L / S Q L编译器内部的限制条件可能会影响 P L / S Q L的长度。一般来说,包体是包的最大部分, 因此也就容易与P L / S Q L的内部限制冲突。当包体长度达到了编译器的内部默认长度限制时,编 译器将显示错误信息‘P L S - 1 2 3 :程序太大,无法编译’。编译器对包体长度的限制如下: • D i a n a树中的节点数。 P L / S Q L编译器构造一种叫做 D i a n a的内部树,该树反映了块的结构。 D i a n a树是在第一遍编译中生成的。在O r a c l e 8 i之前的版本中,D i a n a节点的最大数目是3 2 K, O r a c l e 8 i将该包和类型体的限制扩充到了 6 4兆字节的容量。该容量是大多数块首次达到的 极限。 • 编译器生成的临时中间变量。该类变量的最大数是 2 1 K字节。 • 入口点的数量。一个包体最多可以有 3 2 K个入口点,入口点可以是过程或函数。 • 字符串的数量。P L / S Q L对字符串的限制单位是23 2。第5章 使用子程序和包计计191下载 在O r a c l e 8 i之前的版本中,最常达到的限制是 D i a n a节点数,因此,后来的版本对该限制进 行了扩充。总的来说,节点数是与源程序行的数量成正比,因此,减少包体长度的最好方法是 减少代码的行数。通常,我们可以把某些子程序从包体中去掉,转而放置在独立的包中。 提示 为了易于阅读和载入方便,应尽量将包的长度减少。 5.4.3 优化参数 对O r a c l e 8 i来说,还有两个可以用在函数声明中的辅助关键字— DETERMINISTIC 和 PA R A L L E L _ E N A B L E。如果使用这两个关键字的话,P L / S Q L编译优化器将会对调用 P L / S Q L函数进行优化。该关键字要放在函数的返回类型后以及语句I S或A S之前。其语法如下: CREATE [OR REPLACE] FUNCTION function_name [ parameter_list] RETURN return_type [DETERMINISTIC] [PARALLEL_ENABLE] IS | AS function_body; 如果同时指定了这两个关键字,其出现顺序可以是任意的。关键字 D E T E R M I N I S T I C和 PA R A L L E L _ E N A B L E可用于独立的函数,打包函数,或对象类型方法(请看本书第 1 3章有关对 象类型的介绍)。如果函数是方法或打包函数的话,则该关键字就要在包头或类型头中使用,而 不能出现在包体或类型体中。 我们在下面两节中将会看到这些关键字的使用方法。 1. 关键字D E T E R M I N I S T I C 如果一个函数对于给定的相同输入值总是返回同一结果并不会引起任何副作用的话(如对 打包变量进行修改),则该函数就被称为是确定函数。由于确定函数在其参数保持不变时可以免 去多次调用,所以该类函数可广泛使用。例如,请看下面的函数 S t u d e n t S t a t u s: 节选自在线代码determ .sql CREATE OR REPLACE FUNCTION StudentStatus( p_NumCredits IN NUMBER) RETURN VARCHAR2 AS BEGIN IF p_NumCredits = 0 THEN RETURN 'Inactive'; ELSIF p_NumCredits <= 12 THEN RETURN 'Part Time'; ELSE RETURN 'Full Time'; END IF; END StudentStatus; 由于该函数对于给定的相同输入总是返回同一个结果并不修改任何包变量,所以该函数是 一个确定函数。正是该函数的这种特点,我们可以使用关键字 D E T E R M I N I S T I C通知编译器该函192计计第二部分 非对象功能 下载 数是确定函数: 节选自在线代码determ .sql CREATE OR REPLACE FUNCTION StudentStatus(p_NumCredits IN NUMBER) RETURN VARCHAR2 DETERMINISTIC AS BEGIN IF p_NumCredits = 0 THEN RETURN 'Inactive'; ELSIF p_NumCredits <= 12 THEN RETURN 'Part Time'; ELSE RETURN 'Full Time'; END IF; END StudentStatus; P L / S Q L编译器将不验证该函数是否是真的确定函数,它只是对该函数做确定函数的标记。 确定函数可用于下列场合: • 用在基于函数的索引上的任何函数必须是确定函数。非确定函数可用在 S Q L语句的 W H E R E子句中,但程序员不能基于该函数创建索引。 • 如果实际的视图( Materialized view)被标记为E N A B L E _ Q U E RY REWRITE,则该视图中 使用的任何函数都必须是确定函数。 • 声明为REFRESH FA S T的快照或实际视图中的函数也应该是确定函数。该声明的使用并不 是必须的(在确定函数使用之前参数 REFRESH FA S T就已经被使用),它是一种推荐。 • 在S Q L语句中的W H E R E,ORDER BY或GROUP BY子句中使用的函数也是确定函数。这也 适用于S Q L类型的O R D E R方法或M A P方法。总的来说,用于确定结果集内容的任何函数都 必须是确定的。在这里需要再次说明的是,O r a c l e语言只是推荐使用上述规则,并不是强制 标准。 基于函数索引的确定函数 在O r a c l e 8 i中,可以根据调用P L / S Q L存储函数的表达式来创建索 引,这类索引叫做基于函数的索引。这就使我们在从 S Q L语句中调用函数时可以使用索引功能。 例如,请看下面的例子: 节选自在线代码determ .sql SELECT id FROM students WHERE SUBSTR(StudentStatus(current_credits), 1, 20) = 'Part Time'; 如果看一下S E L E C T语句的执行情况,可以看到: Rows Row Source Operation ------- --------------------------------------------------- 12 TABLE ACCESS FULL STUDENTS Rows Execution Plan ------- ---------------------------------------------------0 SELECT STATEMENT GOAL: CHOOSE 12 TABLE ACCESS (FULL) OF 'STUDENTS' 在上面的程序中,我们正在进行全表扫描。为了使上面的查询操作效率更高,我们可以创 建使用函数值的索引。这样一来,该查询就可以使用索引了: 节选自在线代码determ .sql CREATE INDEX students_index ON students (SUBSTR(StudentStatus(current_credits), 1, 20)) COMPUTE STATISTICS; 查询的执行情况现在为: Rows Row Source Operation ------- --------------------------------------------------- 12 TABLE ACCESS BY INDEX ROWID STUDENTS 13 INDEX RANGE SCAN (object id 13271) Rows Execution Plan ------- --------------------------------------------------- 0 SELECT STATEMENT GOAL: FIRST_ROWS 12 TABLE ACCESS (FULL) OF 'STUDENTS' 注意 为了创建基于函数的索引,索引的拥有者必须具有 Q U E RY REWRITE的特权。 系统特权GLOBAL REWEITE为在其他用户模式下创建基于函数的索引提供了保证。除 此之外,在优化器使用索引之前,还必须满足其他一些要求。有关信息请看 O r a c l e文档 资料。 2. 关键字PA R A L L E L _ E N A B L E 在某些情况下,程序员可以使用 O r a c l e的并行处理功能来并行执行 S Q L语句。如果有多个 S Q L语句调用一个P L / S Q L函数,则该函数将被独立的进程所调用,每个进程都运行一个该函数 的副本来对表列的子集进行操作。 如果该函数引用了一个包变量的话,就将引发错误。该函数的副本将对其打包变量进行初 始化,就像该函数是刚注册的函数。因此,该函数的副本将看不到函数早期对这些包变量的修 改。其结果是,读或修改包变量的任何函数都不能同时运行。如果该语句是一个 D M L语句的话 (不是查询语句),则该函数就可以既不读也不修改数据库状态。 对于O r a c l e 8 i之前的版本,上述限制是唯一由编译标识 R E S T R I C T _ R E F E R E N C E S实施的。 然而,在O r a c l e 8 i下,程序员可以使用子句 PA R A L L E L _ E N A B L E标识来通知优化器该函数的并 行属性。这就使程序员可以更灵活地控制函数运行在并行状态。 3. 非P L / S Q L函数的优化 O r a c l e 8 i允许程序员创建外部例程,这些用 C语言或J a v a语言编制的外部例程是可以直接从 P L / S Q L调用的函数。D E T E R M I N I S T I C和(或)PA R A L L E L _ E A N B L E子句也可以用在P L / S Q L调用 外部例程的说明中。如果使用了这些关键字,则上面所述的强制规则就将实施。当然,由于 P L / S Q L 编译器 不能运行在外 部例程上,所 以应由程序员来 验证外部例程 是否满足 DETERMINISTIC和PARALLEL_EANBLE实现的要求。有关进一步信息,请看本书的第10章内容。 第5章 使用子程序和包计计193下载5.5 小结 我们在本章讨论了命名 P L / S Q L块的三种类型,过程,函数和包。我们讨论的内容包括本地 和存储子程序的区别以及存储子程序之间的相关工作原理。除此之外,我们还研究了如何从 S Q L语句中调用存储子程序。在本章的最后,我们介绍了包的几种辅助功能。在下一章中,我 们将学习命名P L / S Q L块的第四种类型— 数据库触发器。 194计计第二部分 非对象功能 下载下载 第6章 数据库触发器 命名P L / S Q L块的第四种类型是触发器。触发器类在某些方面类似于子程序,但它们之间也 有明显地区别。我们在本章将介绍如何创建不同类型的触发器以及讨论触发器的一些应用。 6.1 触发器的类型 触发器类似于函数和过程,它们都是具有声明部分、执行部分和异常处理部分的命名 P L / S Q L块。像包一样,触发器必须在数据库中以独立对象的身份存储,并且不能与包和块具有 本地关系。我们在前两章中已经讲过,过程是显式地通过过程调用从其他块中执行的,同时, 过程调用可以传递参数。与之相反 ,触发器是在事件发生时隐式地运行的,并且触发器不能接收 参数。运行触发器的方式叫做激发( f i r i n g)触发器,触发事件可以是对数据库表的 D M L (I N S E RT、U P D AT E或D E L E T E)操作或某种视图的操作 ( Vi e w )。O r a c l e 8 i把触发器功能扩展到 了可以激发系统事件,如数据库的启动和关闭,以及某种 D D L操作。我们将在本章的后几节讨 论触发事件的详细内容。 触发器可以用于下列情况: • 维护在表创建阶段通过声明限制无法实现的复杂完整性限制。 • 通过记录修改内容和修改者来审计表中的信息。 • 在表内容发生变更时,自动通知其他程序采取相应的处理。 • 在订阅发布环境下,发布有关各种事件的信息。 有三种主要的触发器类型: D M L、替代触发器和系统触发器。在下面几节中,我们将逐一 介绍这些触发器类型。在本章后面“创建触发器”一节中还将详细讨论这些触发器。 注意 O r a c l e 8 i允许使用P L / S Q L语言或可以作为外部例程调用的其他语言来编制触发 器。有关触发器的详细介绍,请参阅 6 . 2 . 4节和第1 0章的内容。 6.1.1 DML触发器 D M L触发器可以由D M L语句激发,并且由该语句的类型决定 D M L触发器的类型。可以定义 D M L触发器进行I N S E RT,U P D AT E,D E L E T E操作。这类触发器可以在上述操作之前或之后激 发,除此之外,它们也可以在行或语句操作上激发。 作为例子,让我们假设要跟踪不同专业的统计信息,其中包括已注册学生的数量和已得到 的总分。我们要把这些结果存储在表 m a j o r _ s t a t s中: 节选自在线代码relTables.sql CREATE TABLE major_stats ( major VARCHAR2(30), total_credits NUMBER,total_students NUMBER); 为了保持表m a j o r _ s t a t s中的数据处于更新状态,我们可以创建一个每次表 s t u d e n t s被修改时 自动更新表m a j o r _ s t a t s的触发器。下面所示的 U p d a t e M a j o r S t a t s就是实现上述功能的触发器。在 表s t u d e n t s上进行任何D M L操作之后,该触发器将启动运行。该触发器的代码要查询表 s t u d e n t s 并使用当前的统计信息更新表 m a j o r _ s t a t s。 节选自在线代码UpdateMajorStats.sql CREATE OR REPLACE TRIGGER UpdateMajorStats /* Keeps the major_stats table up-to-date with changes made to the students table. */ AFTER INSERT OR DELETE OR UPDATE ON students DECLARE CURSOR c_Statistics IS SELECT major, COUNT(*) total_students, SUM(current_credits) total_credits FROM students GROUP BY major; BEGIN /* First delete from major_stats. This will clear the statistics, and is necessary to account for the deletion of all students in a given major. */ DELETE FROM major_stats; /* Now loop through each major, and insert the appropriate row into major_stats. */ FOR v_StatsRecord in c_Statistics LOOP INSERT INTO major_stats (major, total_credits, total_students) VALUES (v_StatsRecord.major, v_StatsRecord.total_credits, v_StatsRecord.total_students); END LOOP; END UpdateMajorStats; 语句触发器可以激发多种类型的触发语句。例如, U p d a t e M a j o r S t a t s可以激发 I N S E RT, U P D AT E,D E L E T E语句。触发事件说明了一个或多个激发触发器的 D M L操作。 6.1.2 替代触发器 O r a c l e 8提供的这种替代触发器( Instead-of trigger)只能定义在视图上(可以是关系 或对象)。与D M L触发器不同,D M L触发器是在D M L操作之外运行的,而替代触发 器则代替激发它的 D M L语句运行。替代触发器是行一级的。例如,请看下面的视图 c l a s s e s _ r o o m s : 节选自在线代码insteadOf.sql CREATE OR REPLACE VIEW classes_rooms AS SELECT department, course, building, room_number FROM rooms, classes WHERE rooms.room_id = classes.room_id; 196计计第二部分 非对象功能 下载如下所示,直接执行对该视图的插入操作是非法的。这是因为该视图是两个表的联合 ,而插 入操作要求对两个现行表进行修改,下面的 SQL *Plus会话显示了插入操作过程: 节选自在线代码insteadOf.sql SQL> INSERT INTO classes_rooms (department, course, building, room_number) 2 VALUES ('MUS', 100, 'Music Building', 200); INSERT INTO classes_rooms (department, course, building, room_number) * ERROR at line 1: ORA-01732: data manipulation operation not legal on this view 然而,我们可以创建一个替代触发器来实现正确的插入操作,也就是来更新现行表: 节选自在线代码insteadOf.sql CREATE TRIGGER ClassesRoomsInsert INSTEAD OF INSERT ON classes_rooms DECLARE v_roomID rooms.room_id%TYPE; BEGIN -- First determine the room ID SELECT room_id INTO v_roomID FROM rooms WHERE building = :new.building AND room_number = :new.room_number; -- And now update the class UPDATE CLASSES SET room_id = v_roomID WHERE department = :new.department AND course = :new.course; END ClassesRoomsInsert; 有了触发器C l a s s e s R o o m s I n s e r t,I N S E RT语句就可以执行正确的更新操作。 注意 在上面的程序中,触发器 C l a s s e s R o o m s I n s e r t没有做任何错误检查。我们将在本 章后的程序中加入错误检查和处理代码。 6.1.3 系统触发器 O r a c l e 8 i提供了第三种触发器,这种系统触发器在发生如数据库启动或关闭等系统 事件时激发,而不是在执行 D M L语句时激发。系统触发器也可以在 D D L操作时,如 表的创建中激发。例如,假设我们要记录对象创建的时间,我们可以通过创建下面的表来实现 上述记录功能: 节选自在线代码LogCreations.sql CREATE TABLE ddl_creations ( user_id VARCHAR2(30), object_type VARCHAR2(20), 第6章 数据库触发器计计197下载object_name VARCHAR2(30), object_owner VARCHAR2(30), creation_date DATE); 一旦该表可以使用,我们就可以创建一个系统触发器来记录相关信息。在每次 C R E AT E语句 对当前模式进行操作之后,触发器 L o g C r e a t i o n s就记录在d d l _ c r e a t i o n s中创建的对象的有关信息。 节选自在线代码LogCreations .sql CREATE OR REPLACE TRIGGER LogCreations AFTER CREATE ON SCHEMA BEGIN INSERT INTO ddl_creations (user_id, object_type, object_name, object_owner, creation_date) VALUES (USER, SYS.DICTIONARY_OBJ_TYPE, SYS.DICTIONARY_OBJ_NAME, SYS.DICTIONARY_OBJ_OWNER, SYSDATE); END LogCreations; 6.2 创建触发器 所有触发器,不管其类型如何,都可以使用相同的语法创建。下面是创建触发器的通用语 法: CREATE [OR REPLACE] TRIGGER trigger_name {BEFORE | AFTER | INSTEAD OF} triggering_event referencing_clause [WHEN trigger_condition] [FOR EACH ROW] trigger_body; 其中,t r i g g e r _ n a m e是触发器的名称, t r i g g e r i n g _ e v e n t说明了激发触发器的事件(也可能包 括特殊的表或视图),t r i g g e r _ b o d y是触发器的代码。r e f e r e n c i n g _ c l a u s e用来引用正在处于修改状 态下的行中的数据,如果在 W H E N子句中指定t r i g g e r _ c o n d i t i o n的话,则首先对该条件求值。触 发器主体只有在该条件为真值时才运行。我们在下面几节中将演示不同类型的触发器案例。 注意 触发器主体不能超过3 2 K。如果触发器长度超过了该限制,就要把该体内的某些 代码放到单独编译的包或存储子程序中,并从触发器主体中调用这些代码。 6.2.1 创建D M L触发器 D M L触发器是由对数据库表进行 I N S E RT、U P D AT E、D E L E T E操作而激发的触发器。该类 触发器可以在上述操作之前或之后激发运行,也可以按每个变更行激发一次,或每个语句激发 一次进行。这些条件的组合形成了触发器的类型。总共有 1 2种可能的触发类型: 3种语句×2种 定时×2级。例如,下面所有的说明都是合法的 D M L触发器类型: • 更新语句之前 • 插入行之后 • 删除行之前 198计计第二部分 非对象功能 下载表6 - 1总结了D M L触发器的各种选择项。除此之外,触发器也可以由给定表中的一个以上的 D M L语句,如I N S E RT和U P D A E而激发。触发器中的任何代码将随触发语句一起作为同一事务 的一部分运行。 可以对一个表定义任意数量的触发器,其中可以包括一个以上的给定 D M L类型。例如,可 以定义两个删除语句之后的触发器。所有同类型的触发器将按顺序激发。(下一节讨论触发器的 激发顺序。) 注意 在P L / S Q L 2 . 1版(O r a c l e 7的7 . 1版)之前的版本下,每种类型的触发器只能在表 上定义一个。也就是说,最多有 1 2个触发器。因此,初始化参数 C O M PAT I B L E就必须 是7 . 1或更高以便复制一个表上的同类触发器。 D M L触发器的触发事件说明了激发触发器的表的名称(以及列)。在O r a c l e 8 i下,触发器可 以在嵌套表的列上激发。本书的第 1 4章提供了更多的触发器内容。 表6-1 DML触发器类型 类 别 值 说 明 语句 I N S E RT、D E L E T E、 定义何种D M L语句激发触发器 U P D AT E 定时 之前或之后 定义触发器是在语句运行前或运行后激发 级 行或语句 如果触发器是行级触发器,该触发器就对由触发语句变更的 每一行激发一次。如果触发器是语句级的触发器,则该触发 器就在语句之前或之后激发一次。行级触发器是按触发器定 义中的FOR EACH ROW子句表示的 1. DML 触发器激发顺序 触发器是在D M L语句运行时激发的。下面是执行 D M L语句的算法步骤: 1) 如果有语句之前级触发器的话,先运行该触发器。 2) 对于受语句影响每一行: a. 如果有行之前级触发器的话,运行该触发器。 b. 执行该语句本身。 c. 如果有行之后级触发器的话,运行该触发器。 3) 如果有语句之后级触发器的话,运行该触发器。 为了说明上面的算法,假设我们在表 c l a s s e s上创建了所有四种U P D AT E触发器,即之前,之 后,语句和行级。我们将创建三个行前触发器和两个语句后触发器,其代码如下: 节选自在线代码firingOrder .sql CREATE SEQUENCE trig_seq START WITH 1 INCREMENT BY 1; CREATE OR REPLACE PACKAGE TrigPackage AS -- Global counter for use in the triggers v_Counter NUMBER; END TrigPackage; 第6章 数据库触发器计计199下载CREATE OR REPLACE TRIGGER ClassesBStatement BEFORE UPDATE ON classes BEGIN -- Reset the counter first. TrigPackage.v_Counter := 0; INSERT INTO temp_table (num_col, char_col) VALUES (trig_seq.NEXTVAL, 'Before Statement: counter = ' || TrigPackage.v_Counter); -- And now increment it for the next trigger. TrigPackage.v_Counter := TrigPackage.v_Counter + 1; END ClassesBStatement; CREATE OR REPLACE TRIGGER ClassesAStatement1 AFTER UPDATE ON classes BEGIN INSERT INTO temp_table (num_col, char_col) VALUES (trig_seq.NEXTVAL, 'After Statement 1: counter = ' || TrigPackage.v_Counter); -- Increment for the next trigger. TrigPackage.v_Counter := TrigPackage.v_Counter + 1; END ClassesAStatement1; CREATE OR REPLACE TRIGGER ClassesAStatement2 AFTER UPDATE ON classes BEGIN INSERT INTO temp_table (num_col, char_col) VALUES (trig_seq.NEXTVAL, 'After Statement 2: counter = ' || TrigPackage.v_Counter); -- Increment for the next trigger. TrigPackage.v_Counter := TrigPackage.v_Counter + 1; END ClassesAStatement2; CREATE OR REPLACE TRIGGER ClassesBRow1 BEFORE UPDATE ON classes FOR EACH ROW BEGIN INSERT INTO temp_table (num_col, char_col) VALUES (trig_seq.NEXTVAL, 'Before Row 1: counter = ' || TrigPackage.v_Counter); -- Increment for the next trigger. TrigPackage.v_Counter := TrigPackage.v_Counter + 1; END ClassesBRow1; CREATE OR REPLACE TRIGGER ClassesBRow2 200计计第二部分 非对象功能 下载BEFORE UPDATE ON classes FOR EACH ROW BEGIN INSERT INTO temp_table (num_col, char_col) VALUES (trig_seq.NEXTVAL, 'Before Row 2: counter = ' || TrigPackage.v_Counter); -- Increment for the next trigger. TrigPackage.v_Counter := TrigPackage.v_Counter + 1; END ClassesBRow2; CREATE OR REPLACE TRIGGER ClassesBRow3 BEFORE UPDATE ON classes FOR EACH ROW BEGIN INSERT INTO temp_table (num_col, char_col) VALUES (trig_seq.NEXTVAL, 'Before Row 3: counter = ' || TrigPackage.v_Counter); -- Increment for the next trigger. TrigPackage.v_Counter := TrigPackage.v_Counter + 1; END ClassesBRow3; CREATE OR REPLACE TRIGGER ClassesARow AFTER UPDATE ON classes FOR EACH ROW BEGIN INSERT INTO temp_table (num_col, char_col) VALUES (trig_seq.NEXTVAL, 'After Row: counter = ' || TrigPackage.v_Counter); -- Increment for the next trigger. TrigPackage.v_Counter := TrigPackage.v_Counter + 1; END ClassesARow; 假设我们现在提交下列U P D AT E语句: UPDATE classes SET num_credits = 4 WHERE department IN ('HIS', 'CS'); 该语句对四行有影响。语句之前级和之后触发器将各自运行一次,而行之前级和之后触发 器则每个运行四次。如果在上述操作之后,我们从表 t e m p _ t a b l e进行选择的话,下面就是得到的 结果: 节选自在线代码firingOrder .sql SQL> SELECT * FROM temp_table 2 ORDER BY num_col; NUM_COL CHAR_COL --------- -------------------------------------- 1 Before Statement: counter = 0 第6章 数据库触发器计计201下载2 Before Row 3: counter = 1 3 Before Row 2: counter = 2 4 Before Row 1: counter = 3 5 After Row: counter = 4 6 Before Row 3: counter = 5 7 Before Row 2: counter = 6 8 Before Row 1: counter = 7 9 After Row: counter = 8 10 Before Row 3: counter = 9 11 Before Row 2: counter = 10 12 Before Row 1: counter = 11 13 After Row: counter = 12 14 Before Row 3: counter = 13 15 Before Row 2: counter = 14 16 Before Row 1: counter = 15 17 After Row: counter = 16 18 After Statement 2: counter = 17 19 After Statement 1: counter = 18 当每个触发器被激发时,该触发器将可以看到由其前期触发器实现的变更,以及到目前为 止由D M L语句对数据实现的变更。触发器的激发可以通过由每个触发器打印的记数器值来判断。 (请看第5章中有关使用包变量的说明。) 同类触发器的激发顺序没有明确的定义。如前面所示的例子中,每个触发器都将可以看到 其前面的触发器实施的变更。如果顺序非常重要的话,可以把所有的操作组合在一个触发器中。 注意 当你为表创建快照日志时, O r a c l e将自动地为该表创建一个AFTER ROW触发器, 该触发器在每个 D M L语句后更新日志文件。如果要创建额外的 AFTER ROW触发器的 话,你一定要避免与系统创建的触发器发生冲突。除此之外,数据库系统还对触发器和 快照有其他的限制。 2. 行级触发器的相关标识 行级触发器是按触发语句所处理的行激发的。在触发器内部,我们可以访问正在处理中的 行的数据。这种访问是通过两个相关的标识符: : o l d和: n e w实现的。相关标识符是一种特殊的 P L / S Q L连接变量(bind variable),在该标识符前面的冒号说明它们是使用在嵌套 P L / S Q L中的宿 主变量意义上的连接变量,而不是一般的 P L / S Q L变量。P L / S Q L编译器将把这种变量按记录类型 处理。 triggering_table%ROWTYPE 其中,t r i g g e r i n g _ t a b l e是定义触发器所在的表。因此,下面的引用: :new.field 将只有在该字段位于触发表中时才是合法的。表 6 - 2总结了标识符 : o l d和: n e w的含义。尽管 从语法上将,这两个标识符都按记录类型处理,但实际上不是这样的(该问题将在下文“伪记 录”中介绍。)。正是这个原因,标识符: o l d和: n e w也被称为伪记录。 202计计第二部分 非对象功能 下载表6-2 :old和: n e w相关标识符 触发语句 标识符: o l d 标识符: n e w I N S E RT 无定义-所有字段为空 N U L L 该语句结束时将插入的值 U P D A E 更新前行的原始值 该语句结束时将更新的值 D E L E T E 行删除前的原始值 无定义-所有字段为空 N U L L 注意 标识符 : o l d对I N S E RT语句无定义,而标识符 : n e w对D E L E T E语句无定义。 P L / S Q L编译器不会对在 I N S E RT语句中使用的: o l d和在D E L E T E语句中使用的: n e w标识 符报错,编译的结果将使该字段为空。 O r a c l e 8 i定义了另外一个相关标识符 - : p a r e n t。如果触发器定义在嵌套表中的话,标 识符: o l d和: n e w就引用嵌套表中的行,而 : p a r e n t则引用其父表的当前行。有关详细 信息,请参阅O r a c l e文档资料。 使用: o l d和: n e w相关标识符 下面所示的触发器G e n e r a t e S t u d e n t I D使用了标识符: n e w。该触 发器是一个I N S E RT之前触发器,其目的是使用从序列 s t u d e n g t _ s e u e n c e中生成的值来填写 I D字 段。 节选自在线代码GenerateStudentID .sql CREATE OR REPLACE TRIGGER GenerateStudentID BEFORE INSERT OR UPDATE ON students FOR EACH ROW BEGIN /* Fill in the ID field of students with the next value from student_sequence. Since ID is a column in students, :new.ID is a valid reference. */ SELECT student_sequence.NEXTVAL INTO :new.ID FROM dual; END GenerateStudentID; 触发器G e n e r a t e S t u d e n t I D实际上是修改: n e w. I D的值,这就是: n e w标识符的用途之一,即当 该语句实际运行时,在 : n e w中的任何值都将被使用。使用触发器 G e n e r a t e S t u d e n t I D,我们可以 发布如下所示的I N S E RT语句: 节选自在线代码GenerateStudentID .sql INSERT INTO students (first_name, last_name) VALUES ('Lolita', 'Lazarus'); 该语句可以正确执行。尽管我们没有为主键列 I D指定值(该值是语句需要的),但触发器可 以提供它。事实上,即使我们为该 I D指定了一个值,该值也将被忽略不记,因为触发器将改变 该值。如果我们执行下面的命令: 节选自在线代码GenerateStudentID .sql INSERT INTO students (ID, first_name, last_name) VALUES (-7, 'Zelda', 'Zoom'); 该I D列将被来自于s t u d e n t _ s e q u e n c e . N E X T VA L的值填充,而不是值- 7 . 第6章 数据库触发器计计203下载按上面的操作结果,我们不能在行级触发器之后改变 : n e w,其原因是该语句已被处理了。 总的来说,对: n e w的修改只能在行级触发器之前修改。:o l d具有只读属性,只能读入。 记录:n e w和: o l d只在行级触发器内部合法。如果企图引用在语句级触发器之内的: n e w 或: o l d的话,编译器将报错。由于语句级的触发器只运行一次,即使存在很多被语句处理过的行 的话,n e w和: o l d也没有定义。编译器不知道该引用那一行。 伪记录 尽管: n e w和: o l d从语法上讲按t r i g g r i n g _ t a b l e % R O W T Y P E的记录来处理,但实际处 理却不一样。这样一来,应该是合法的记录操作对于 : n e w和:o l d来说就变成了非法操作。例如, 这两个记录不能按全记录赋值。而只用在其内部的个别字段可以赋值。下面的程序可以说明上 述问题: 节选自在线代码pseudoRecords .sql CREATE OR REPLACE TRIGGER TempDelete BEFORE DELETE ON temp_table FOR EACH ROW DECLARE v_TempRec temp_table%ROWTYPE; BEGIN /* This is not a legal assignment, since :old is not truly a record. */ v_TempRec := :old; /* We can accomplish the same thing, however, by assigning the fields individually. */ v_TempRec.char_col := :old.char_col; v_TempRec.num_col := :old.num_col; END TempDelete; 除此之外,: o l d和:n e w记录不能传递到接收t r i g g r i n g _ t a b l e % R O W T Y E P的过程或函数中。 引用子句 我们可以根据需要使用子句 R E F E R E N C I N G来为: n e w和: o l d指定一个不同的名称。 该子句可以在触发事件之后, W H E N子句之前使用,其语法如下: REFERENCING [OLD AS :old_name] [NEW AS :new_name] 在触发器体内,我们可以使用 : o l d _ n a m e和: n e w _ n a m e来代替: o l d和: n e w。下面是触发器 G e n e r a t e S t u d e n t I D的另一种版本,该版本使用 R E F E R E C I N G来把: n e w作为: n e w _ s t u d e n t引用。 节选自在线代码GenerateStudentID .sql CREATE OR REPLACE TRIGGER GenerateStudentID BEFORE INSERT OR UPDATE ON students REFERENCING new AS new_student FOR EACH ROW BEGIN /* Fill in the ID field of students with the next value from student_sequence. Since ID is a column in students, :new_student.ID is a valid reference. */ SELECT student_sequence.nextval INTO :new_student.ID FROM dual; 204计计第二部分 非对象功能 下载END GenerateStudentID; 3. WHEN子句 W H E N子句只适用于行级触发器。如果使用该子句的话,触发器体将只对满足由 W H E N子 句说明的条件的行执行。W H E N子句的语法是: WHEN trigger_condition 其中,t r i g g e r _ c o n d i t i o n是逻辑表达式。该表达式将为每行求值。 : n e w和:o l d记录可以在 t r i g g e r _ c o n d i t i o n内部引用,但不需使用冒号。该冒号只在触发器体内有效。例如,触发器 C h e c k C r e d i t s的体只在当前的学生得到的学分超出 2 0时才运行: CREATE OR REPLACE TRIGGER CheckCredits BEFORE INSERT OR UPDATE OF current_credits ON students FOR EACH ROW WHEN (new.current_credits > 20) BEGIN /* Trigger body goes here. */ END; 触发器C h e c k C r e d i t s也可写为下列代码: CREATE OR REPLACE TRIGGER CheckCredits BEFORE INSERT OR UPDATE OF current_credits ON students FOR EACH ROW BEGIN IF :new.current_credits > 20 THEN /* Trigger body goes here. */ END IF; END; 4. 触发器谓语:I N S E RT I N G、U P D AT I N G和D E L E T I N G 6 . 1 . 1节中讨论的触发器 U p d a t e M a j o r S t a t s就是I N S E RT、U P D AT E和D E L E T E触发器。在这 种触发器的内部(为不同的 D M L语句激发的触发器)有三个可用来确认执行何种操作的逻辑表 达式。这些表达式的谓语是 I N S E RT I N G、U P D AT I N G、D E L E T I N G。下面说明了每个谓词的 含义。 表达式谓语 状 态 I N S E RT I N G 如果触发语句是 I N S E RT的话,则为真值(T R U E),否则为FA L S E U P D AT I N G 如果触发语句是 U P D AT E的话,则为真值(T R U E),否则为FA L S E D E L E T I N G 如果触发语句是 D E L E T E的话,则为真值(T R U E),否则为FA L S E 注意 O r a c l e 8 i定义了额外的可以从触发器体内调用的函数,这种函数类似于触发器表 达式谓语。有关详细内容,请看 6 . 2 . 3节中的“事件属性函数”。 触发器L o g R S C h a n g e s使用上述表达式的谓语来记录表 r e g i s t e r e d _ s t u d e n t s发生的所有变化。 除了记录这些信息外,它还记录对表进行变更的用户名。该触发器的记录存放在表 R S _ a u d i t中, 其结构如下: 节选自在线代码relTables .sql CREATE TABLE RS_audit ( 第6章 数据库触发器计计205下载change_type CHAR(1) NOT NULL, changed_by VARCHAR2(8) NOT NULL, timestamp DATE NOT NULL, old_student_id NUMBER(5), old_department CHAR(3), old_course NUMBER(3), old_grade CHAR(1), new_student_id NUMBER(5), new_department CHAR(3), new_course NUMBER(3), new_grade CHAR(1) ); 触发器L o g R S C h a n g e s的创建语句如下: 节选自在线代码LogRSChanges.sql CREATE OR REPLACE TRIGGER LogRSChanges BEFORE INSERT OR DELETE OR UPDATE ON registered_students FOR EACH ROW DECLARE v_ChangeType CHAR(1); BEGIN /* Use 'I' for an INSERT, 'D' for DELETE, and 'U' for UPDATE. */ IF INSERTING THEN v_ChangeType := 'I'; ELSIF UPDATING THEN v_ChangeType := 'U'; ELSE v_ChangeType := 'D'; END IF; /* Record all the changes made to registered_students in RS_audit. Use SYSDATE to generate the timestamp, and USER to return the userid of the current user. */ INSERT INTO RS_audit (change_type, changed_by, timestamp, old_student_id, old_department, old_course, old_grade, new_student_id, new_department, new_course, new_grade) VALUES (v_ChangeType, USER, SYSDATE, :old.student_id, :old.department, :old.course, :old.grade, :new.student_id, :new.department, :new.course, :new.grade); END LogRSChanges; 触发器常用于进行数据检查,就象触发器 L o g R S C h a n g e s的功能那样。然而,检查还只是数 据库的一部分用途,触发器还可用于更灵活和更用户化的记录。我们还可以对触发器 L o g R S C h a n g e s进行修改,例如,使用它来记录仅由某些人做的修改。我们还可以使用该触发器 来检查是否用户有权变更数据并在没有授权的情况下引发异常(使用 R A I S E _ A P P L I C AT I O N _ E R R O R)。 206计计第二部分 非对象功能 下载6.2.2 创建替代触发器 D M L触发器是除去执行I N S E RT,U P D AT E或D E L E T E操作外(在这些语句之前或之 后)还要被激活运行的触发器,而替代触发器则被激发来代替执行 D M L语句。除此 之外,替代触发器还可以定义在视图上,而 D M L触发器只能定义在表上。替代触发器的用途有 两类: • 允许对无法变更的视图进行修改。 • 修改视图中嵌套表列的列。 我们将在本节讨论第一种应用,其他信息请看本书第 1 4章。 注意 在O r a c l e 8 i的8 . 1 . 5版中,替代触发器功能只能在其企业版中使用。在将来的版本 中,有可能在其他的版本中也提供该功能。 1. 可变更的与不可变更的视图 可变更视图是可以发布 D M L命令的视图。一般来说,视图如果不包括下列命令参数的话就 是一个可变更视图: • Set operators(UNION,UNION ALL,MINUS) • Aggregate functions(SUM,AV G , e t c . ) • GROUP BY,CONNECT BY,或S TA RT WITH clauses • 操作数D I S T I N C T • 联合 然而,也确实有包括联合的视图是可以变更的。总的来说,如果对该视图的 D M L操作每次 只变更基表,并且D M L操作满足了表6 - 3的条件,那么,该联合视图也是可变更的。如果一个视 图是不可变更的,则我们可以在其上写一个替代触发器来执行正确的操作,从而使该视图可变 更。如果需要进行额外处理的话,替代触发器也可以写在可变更视图上。 表6 - 3引用了保留字表。如果一个表与另一个表联合后,其原始表中的关键字在联合后的表 中仍然是关键字的话,该表就是一个关键字保留表( k e y - p r e s e r v e d)。 表6-3 可变更的联合视图 D M L操作 可执行条件 I N S E RT 该语句没有显式或隐式地引用非保留字表的列 U P D AT E 更新的列映射到保留字表的列中 D E L E T E 在联合中仅有一个保留字表 2. 替代触发器案例 请考虑我们在6 . 1 . 2节中介绍过的的视图c l a s s e s _ r o o m s: 节选自在线代码insteadOf.sql CREATE OR REPLACE VIEW classes_rooms AS SELECT department, course, building, room_number FROM rooms, classes WHERE rooms.room_id = classes.room_id; 如上所见,对该视图的 I N S E RT操作是合法的,尽管可以合法地对该视图执行 U P D AT E或 第6章 数据库触发器计计207下载D E L E T E操作,但这些命令不能实现正确的操作。例如,从 c l a s s e s _ r o o m s发布的D E L E T E命令将 会删除表c l a s s e s中对应的行,什么是 c l a s s e s _ r o o m s的正确D M L操作呢?这与程序的逻辑要求有 关。假设这些命令有下列含义: 操 作 含 义 I N S E RT 把新近插入的班级赋予新插入的教室。该操作将导致对表 c l a s s e s的更新 U P D AT E 变更赋予班级的教室。该操作将导致 c l a s s e s或r o o m s的更新,取决于表 c l a s s e s _ r o o m s的哪一列有变更 D E L E T E 从删除的班级中清除教室的I D。该操作将导致表 c l a s s e s的更新,将I D置 为空N U L L 下面所示的触发器 C l a s s e s R o o m s I n s t e a d实施了上述规则并允许对表 c l a s s e s _ r o m m s执行正确 的D M L操作。 节选自在线代码ClassesRoomInstead.sql CREATE OR REPLACE TRIGGER ClassesRoomsInstead INSTEAD OF INSERT OR UPDATE OR DELETE ON classes_rooms FOR EACH ROW DECLARE v_roomID rooms.room_id%TYPE; v_UpdatingClasses BOOLEAN := FALSE; v_UpdatingRooms BOOLEAN := FALSE; -- Local function that returns the room ID, given a building -- and room number. This function will raise ORA-20000 if the -- building and room number are not found. FUNCTION GetRoomID(p_Building IN rooms.building%TYPE, p_Room IN rooms.room_number%TYPE) RETURN rooms.room_id%TYPE IS v_RoomID rooms.room_id%TYPE; BEGIN SELECT room_id INTO v_RoomID FROM rooms WHERE building = p_Building AND room_number = p_Room; RETURN v_RoomID; EXCEPTION WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20000, 'No matching room'); END getRoomID; -- Local procedure that checks whether the class identified by -- p_Department and p_Course exists. If not, it raises -- ORA-20001. 208计计第二部分 非对象功能 下载PROCEDURE VerifyClass(p_Department IN classes.department%TYPE, p_Course IN classes.course%TYPE) IS v_Dummy NUMBER; BEGIN SELECT 0 INTO v_Dummy FROM classes WHERE department = p_Department AND course = p_Course; EXCEPTION WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20001, p_Department || ' ' || p_Course || ' doesn''t exist'); END verifyClass; BEGIN IF INSERTING THEN -- This essentially assigns a class to a given room. The logic -- here is the same as the "updating rooms" case below: First, -- determine the room ID: v_RoomID := GetRoomID(:new.building, :new.room_number); -- And then update classes with the new ID. UPDATE CLASSES SET room_id = v_RoomID WHERE department = :new.department AND course = :new.course; ELSIF UPDATING THEN -- Determine if we are updating classes, or updating rooms. v_UpdatingClasses := (:new.department != :old.department) OR (:new.course != :old.course); v_UpdatingRooms := (:new.building != :old.building) OR (:new.room_number != :old.room_number); IF (v_UpdatingClasses) THEN -- In this case, we are changing the class assigned for a -- given room. First make sure the new class exists. VerifyClass(:new.department, :new.course); -- Get the room ID, v_RoomID := GetRoomID(:old.building, :old.room_number); -- Then clear the room for the old class, UPDATE classes SET room_ID = NULL WHERE department = :old.department AND course = :old.course; 第6章 数据库触发器计计209下载-- And finally assign the old room to the new class. UPDATE classes SET room_ID = v_RoomID WHERE department = :new.department AND course = :new.course; END IF; IF v_UpdatingRooms THEN -- Here, we are changing the room for a given class. This -- logic is the same as the "inserting" case above, except -- that classes is updated with :old instead of :new. -- First, determine the new room ID. v_RoomID := GetRoomID(:new.building, :new.room_number); -- And then update classes with the new ID. UPDATE CLASSES SET room_id = v_RoomID WHERE department = :old.department AND course = :old.course; END IF; ELSE -- Here, we want to clear the class assigned to the room, -- without actually removing rows from the underlying tables. UPDATE classes SET room_ID = NULL WHERE department = :old.department AND course = :old.course; END IF; END ClassesRoomsInstead; 注意 子句FOR EACH ROW是替代触发器的选择项。不管该子句是否存在,所有的替 代触发器都是行级的。 触发器C l a s s e s R o o m s I n s t e a d使用触发器判定来决定将要执行的 D M L操作,并且采取相应的 动作。图6 - 1演示了表c l a s s e s、r o o m s和c l a s s e s _ r o o m s的原始内容。假设我们发布了下列 I N S E RT 命令: 节选自在线代码ClassesRoomInstead.sql INSERT INTO classes_rooms VALUES ('MUS', 100, 'Music Building', 200); 该触发器对表c l a s s e s进行更新以便反映新的教室。新的 c l a s s e s表如图6 - 2所示。现在我们假 设发布了下列U P D AT E命令: 节选自在线代码ClassesRoomInstead.sql UPDATE classes_rooms 210计计第二部分 非对象功能 下载SET department = 'NUT', course = 307 WHERE building = 'Building 7' AND room_number = 201; 我们看到表c l a s s e s又一次被更新,更新后的表反映了新的变更。从该表中我们可以看到,历 史系1 0 1课程没有分配教室,营养系 3 0 7课程分配到了原历史系 1 0 1课程的教室。这种变化反映在 图6 - 3所示的表中。最后,假设我们发布如下的 D E L E T E命令: 节选自在线代码ClassesRoomInstead.sql DELETE FROM classes_rooms WHERE building = 'Building 6'; 图6-1 表c l a s s e s r o o m s和c l a s s e s _ r o o m s的原始内容 图6-2 执行插入操作后的各表的内容 对表c l a s s e s的更新操作把原在 6楼教室的r o o m _ I D设置为空。更新后的表如图 6 - 4所示。请注 第6章 数据库触发器计计211下载意,经过前面所有的D M L语句,表r o o m s保持没有变更,只有表c l a s s s e s进行了更新。 图6-3 更新后的表的内容 图6-4 删除操作后表的内容 6.2.3 创建系统触发器 正如我们在前几节所看到的, D M L和替代触发器都在(或代替) D M L事件,即 I N S E RT、U P D AT E、D E L E T E语句上激活。而系统触发器可以在两种不同的事件即 D D L或数据库上激活。D D L事件包括C R E AT E、A LT E R或D R O P语句,而数据库事件包括服务器 的启动或关闭,用户的登录或退出,以及服务器错误。创建系统触发器的语法如下: CREATE [OR REPLACE] TRIGGER [ schema.] trigger_name {BEFORE | AFTER} { ddl_event_list| database_event_list} ON {DATABASE | [ schema.]SCHEMA} [ when_clause] 212计计第二部分 非对象功能 下载trigger_body; 其中,d d l _ e v e n t _ l i s t是一个或多个D D L事件(事件之间用 O R分隔),d a t a b a s e _ e v e n t _ l i s t是 一个或多个数据库事件(事件之间用‘ O R’分隔)。 表6 - 4说明了D D L和数据库事件的种类以及这些事件出现的时机(之前或以后)。系统不支 持替代系统触发器,T R U N C AT E没有对应的数据库事件。 注意 创建系统触发器必须具有系统权限 ADMINISTER DATABASE TRIGGER。本章 6 . 2 . 4节中的“触发器权限”提供了详细信息。 表6-4 系统D L L和数据库事件 事 件 允 许 时 机 说 明 启动 之后 实例启动时激活 关闭 之前 实例关闭时激活。如果数据库非正常关闭(如关闭故障), 则该事件不激活 服务器错误 之后 只要有该类错误就激活 登录 之后 在用户成功连接数据库后激活 注销 之前 在用户注销开始时激活 创建 之前,之后 在创建模式对象之前或之后激活 撤消 之前,之后 在创建模式对象撤消之前或之后激活 变更 之前,之后 在创建模式对象变更之前或之后激活 1. 数据库与模式触发器的比较 系统触发器可以在数据库级或模式级定义。数据库级的触发器不管触发事件何时发生都将 激活,而模式级触发器只有在指定的模式的触发事件发生时才会激活。关键字 D ATA B A S E和 S C H E M A决定了给定系统触发器的等级。如果没有用关键字 S C H E M A来说明模式,则以触发器 所属的模式为默认模式。例如,假设我们在作为数据库的联机用户 U s e r A时,创建了下列的触发 器: 节选自在线代码DatabaseSchema.sql CREATE OR REPLACE TRIGGER LogUserAConnects AFTER LOGON ON SCHEMA BEGIN INSERT INTO example.temp_table VALUES (1, 'LogUserAConnects fired!'); END LogUserAConnects; 字符串L o g U s e r A C o n n e c t s将在U s e r A与数据库建立连接时记录在表 t e m p _ t a b l e中。我们可以 通过创建下面的触发器在用户 U s e r B与数据库建立连接时为用户 U s e r B做相同的记录: 节选自在线代码DatabaseSchema.sql CREATE OR REPLACE TRIGGER LogUserBConnects AFTER LOGON ON SCHEMA BEGIN INSERT INTO example.temp_table VALUES (2, 'LogUserBConnects fired!'); END LogUserBConnects; 第6章 数据库触发器计计213下载最后,我们可以在以 e x a m p l e的身份与数据库建立连接时创建下面的触发器。由于 L o g A l l C o n n e c t s触发器是数据库级触发器,所以它可以把所有的数据库连接都记录在数据库中。 节选自在线代码DatabaseSchema.sql CREATE OR REPLACE TRIGGER LogAllConnects AFTER LOGON ON DATABASE BEGIN INSERT INTO example.temp_table VALUES (3, 'LogAllConnects fired!'); END LogAllConnects; 注意 我们必须首先创建U s e r A和U s e r B ,并在运行上面的例子前把相应的权限赋予这些 用户。请看程序D a t a b a s e S c h e m a . s q l中的实现代码。 现在我们可以在SQL *Plus会话中看到不同触发器的影响。 节选自在线代码DatabaseSchema.sql SQL> connect UserA/UserA Connected. SQL> connect UserB/UserB Connected. SQL> connect example/example Connected. SQL> SQL> SELECT * FROM temp_table; NUM_COL CHAR_COL ---------- ----------------------------------------------------------- 3 LogAllConnects fired! 2 LogUserBConnects fired! 3 LogAllConnects fired! 3 LogAllConnects fired! 1 LogUserAConnects fired! L o g A l l C o n n e c t s触发器被激活了三次(每个连接激活一次),而 L o g U s e r A C o n n e c t s和 L o g U s e r B C o n n e c t s按预期只激活了一次。 注意 S TA RT U P和S H U T D O W N触发器只与数据库级有关。虽然在模式级创建它们是合 法的,但它们不会被激活。 2. 事件属性函数 系统触发器有几个内部的属性函数可供使用。这些函数类似于我们在前一节介绍的触发器 参数(I N S E T I N G,U P D AT I N G,D E L E T I N G),这些参数允许触发器体获得有关触发事件的信 息。尽管从其他的 P L / S Q L块中调用这些函数是合法的(在系统触发器中不必这样调用),但有 时这些函数也会返回我们不希望的结果。表 6 - 5对这些事件属性函数做了说明。 我们在本章的开始部分介绍的触发器 L o g C r e a t i o n s中使用了这些属性函数。与触发器参数不 同,事件属性函数是 S Y S拥有的独立P L / S Q L函数。系统没有为这些函数指定默认的替代名称, 所以为了识别这些函数,在程序中必须在它们的前面加上前缀 S Y S。 节选自在线代码LogCreations.sql 214计计第二部分 非对象功能 下载CREATE OR REPLACE TRIGGER LogCreations AFTER CREATE ON SCHEMA BEGIN INSERT INTO ddl_creations (user_id, object_type, object_name, object_owner, creation_date) VALUES (USER, SYS.DICTIONARY_OBJ_TYPE, SYS.DICTIONARY_OBJ_NAME, SYS.DICTIONARY_OBJ_OWNER, SYSDATE); END LogCreations; 表6-5 事件属性函数 属 性 函 数 数 据 类 型 可应用的系统事件 说 明 S Y S E V E N T VA R C H A R 2(2 0) 所有事件 返回激活触发器的系统事件 I N S TA N C E _ N U M N U M B E R 所有事件 返回当前实例号。在不运行O P S的情况下, 该号为1 D ATA B A S E _ N A M E VA R C H A R 2 ( 5 0 ) 所有事件 返回当前数据库名 S E RV E R _ E R R O R N U M B E R S E RV E R E R R O R 接收一个 N U M B E R类型的参数,返回由 该参数所指示的错误堆栈中相应位置的错 误。错误堆栈的顶部对应于位置 1 I S _ S E V E R E R R O R B O O L E A N S E RV E R E R R O R 接收一个错误号作为参数,如果所指示的 O r a c l e错误返回在堆栈中,则返回真值 (T R U R) L O G I N _ U S E R VA R C H A R 2 ( 3 0 ) 所有事件 返回激活触发器的用户的u s e r i d D I C T I O N A RY _ O B VA R C H A R 2 ( 2 0 ) C R E ATE, DROP, 返回激活触发器的D D L操作使用的字典 J _ T Y P E A LT E R 对象的类型 D I C T I O N A RY _ O B VA R C H A R 2 ( 3 0 ) C R E ATE, DROP, 返回激活触发器的D D L操作使用的字典 J _ N A M E A LT E R 对象的名称 D I C T I O N A RY _ O B VA R C H A R 2 ( 3 0 ) C R E ATE, DROP, 返回激活触发器的D D L操作使用的字典 J _ O W N E R A LT E R 对象的拥有者 D E S _ E N C RY P T E VA R C H A R 2 ( 3 0 ) C R E AT E用户或 返回正在创建或变更用户的使用 D E S加 D _ PA S S W O R D A LT E R 密的口令 3. 使用S E RV E R E R R O R事件 事件S E RV E R E R R O R可以用于跟踪数据库中发生的错误。其错误代码可以使用触发器内部 的S E RV E R _ E R R O R属性函数取出。该函数可以让用户确定堆栈中的错误码。然而,该函数不能 返回与该错误码相关的错误信息。 上述缺点可以通过使用过程 D B M S _ U T I L I T Y. F O R M AT _ E R R O R _ S TA C K来解决。尽管触发 器本身不会引发错误,但借助于该过程,我们可以使用 P L / S Q L来访问错误堆栈。下面是演示上 述过程的例子,该程序将错误记录在下面的表中: 节选自在线代码LogErrors.sql CREATE TABLE error_log ( timestamp DATE, username VARCHAR2(30), instance NUMBER, database_name VARCHAR2(50), error_stack VARCHAR2(2000) ); 第6章 数据库触发器计计215下载我们可以创建一个如下所示的插入表 e r r o r _ l o g的触发器: 节选自在线代码LogErrors.sql CREATE OR REPLACE TRIGGER LogErrors AFTER SERVERERROR ON DATABASE BEGIN INSERT INTO error_log VALUES (SYSDATE, SYS.LOGIN_USER, SYS.INSTANCE_NUM, SYS. DATABASE_NAME, DBMS_UTILITY.FORMAT_ERROR_STACK); END LogErrors; 最后,我们可以生成几个错误并来看过程 L o g E r r o r s怎样来记录这些错误信息。请注意可以 捕捉S Q L中的错误、运行时P L / S Q L错误和编译时的P L / S Q L错误。 节选自在线代码LogErrors.sql SQL> SELECT * FROM non_existent_table; SELECT * FROM non_existent_table * ERROR at line 1: ORA-00942: table or view does not exist SQL> BEGIN 2 INSERT INTO non_existent_table VALUES ('Hello!'); 3 END; 4 / INSERT INTO non_existent_table VALUES ('Hello!'); * ERROR at line 2: ORA-06550: line 2, column 15: PLS-00201: identifier 'NON_EXISTENT_TABLE' must be declared ORA-06550: line 2, column 3: PL/SQL: SQL Statement ignored SQL> BEGIN 2 -- This is a syntax error! 3 DELETE FROM students 4 END; 5 / END; * ERROR at line 4: ORA-06550: line 4, column 1: PLS-00103: Encountered the symbol "END" when expecting one of the following: . @ ; return RETURNING_ partition where The symbol ";" was substituted for "END" to continue. SQL> SELECT * 2 FROM error_log; TIMESTAMP USERNAME INSTANCE DATABASE 216计计第二部分 非对象功能 下载--------- -------- --------- -------- ERROR_STACK ------------------------------------------------------------- 30-AUG-99 EXAMPLE 1 V815 ORA-00942: table or view does not exist 30-AUG-99 EXAMPLE 1 V815 ORA-06550: line 2, column 15: PLS-00201: identifier 'NON_EXISTENT_TABLE' must be declared ORA-06550: line 2, column 3: PL/SQL: SQL Statement ignored 30-AUG-99 EXAMPLE 1 V815 ORA-06550: line 4, column 1: PLS-00103: Encountered the symbol "END" when expecting one of the following: . @ ; return RETURNING_ partition where The symbol ";" was substituted for "END" to continue. 4. 系统触发器和事务 系统触发器的事务特性与触发事件有关。系统触发器可以作为基于触发器正常结束时提交 的独立事务激活,也可以作为当前用户事务的一部分激活。触发器 S TA RT U P,S H U T D O W N, S E V E R E R R O R和L O G O N都是由独立事务激活的,而 L O G O F F和D D L触发器则作为当前事务的 一部分被激活。 需要注意的是,触发器实现的任务将被提交处理。在使用 D D L触发器的情况下,当前事务 (也就是C R E AT E、A LT E R或D R O P语句)将自动提交。触发器 L O G O F F的操作也将作为会话中 最后事务的一部分提交。 注意 由于系统触发器一般都要提交,因此把这类触发器声明为自主事务是没有意义的。 请看本书第11章介绍自主事务的内容。 5. 系统触发器和W H E N字句 就象D M L触发器一样,系统触发器可以使用 W H E N子句来指定触发器激活条件。然而,对 每一种系统触发器所指定的条件类型有如下限制: • STA RT U P和S H U T D O W N触发器不能带有任何条件。 • SERV E R E R R O R触发器可以使用E R R N O测试来检查特定的错误。 • LOGON 和L O G O F F触发器可以使用U S E R I D或U S E R N A M E测试来检查用户标识或用户 名。 • DDL触发器可以检查正在修改对象的名称和类型。 6.2.4 其他触发器问题 我们在本节将讨论有关触发器的最后一些问题。其中包括触发器名称的命名空间( N a m e - 第6章 数据库触发器计计217下载s p a c e),使用触发器的各种限制,和不同的触发器体。本节的最后将讨论与触发器有关的权限问题。 1. 触发器名称 触发器的命名空间不同于其他子程序的命名空间。所谓命名空间就是一组合法的可供对象 作为名字使用的标识符。过程,包和表都共享同一个命名空间,这就是说,在一个数据库模式 范围内,同一命名空间内的所有的对象必须具有唯一的名称。例如,把同一个名字同时赋予一 个过程和包就是非法的。 然而,触发器隶属于一个独立的命名空间。也就是说,触发器可以有与表或过程相同的名 称。在一个模式范围内,然而,给定的名称只能用于一个触发器。例如,我们可以创建一个建 立在表m a j o r _ s t a t s上叫做m a j o r _ s t a t s触发器,当但是,如果再创建一个叫做 m a j o r _ s t a t s的过程就 是非法操作,下面的SQL *Plus会话演示了上述规则: 节选自在线代码Samename.sql SQL> -- Legal, since triggers and tables are in different namespaces. SQL> CREATE OR REPLACE TRIGGER major_stats 2 BEFORE INSERT ON major_stats 3 BEGIN 4 INSERT INTO temp_table (char_col) 5 VALUES ('Trigger fired!'); 6 END major_stats; 7 / Trigger created. SQL> -- Illegal, since procedures and tables are in the same namespace. SQL> CREATE OR REPLACE PROCEDURE major_stats AS 2 BEGIN 3 INSERT INTO temp_table (char_col) 4 VALUES ('Procedure called!'); 5 END major_stats; 6 / CREATE OR REPLACE PROCEDURE major_stats AS * ERROR at line 1: ORA-00955: name is already used by an existing object 提示 尽管触发器和表可以共用一个名称,但我们不推荐使用这种命名方法。最好是给 每个触发器和表都赋予一个标识其功能的唯一的名称,也可以在触发器的名称加上如 T R G _之类的前缀。 1. 对触发器的限制 触发器的体是一个 P L / S Q L块。(O r a c l e 8 i允许其他类型的触发器体,下节将讨论。)除去下 面的限制外,在P L / S Q L块中可以使用的语句也可以使用在触发器的体中: • 触发器不能发布任何事务控制语句,如 C O M M I T、R O L L B A C K、S AV E P O I N T或S E T T R A N S A C T I O N。P L / S Q L编译器允许触发器体中出现上述控制语句,但当该触发器激活 时,将出现错误提示。这是因为该触发器是作为触发语句的执行部分被激活,并且与触发 语句位于同一个事务中。当触发语句被提交或重新运行时,则该触发器所做的工作也将被 218计计第二部分 非对象功能 下载提交或重新开始。(在O r a c l e 8 i下,我们可以创建作为自动事务运行的触发器,这时,触发 器所做的工作就可以独立于触发语句的状态而独立提交或返回起始点。本书第 11章对自动 事务做了详细介绍。) • 与上一条类似,由触发器体调用的任何过程或函数都不能发布任何事务控制命令(除非在 O r a c l e 8 i下把它们声明为自动类型)。 • 触发器体不能声明任何L O N G或LONG RAW变量。同样, : n e w和: o l d也不能引用定义触发 器所用表的LONG 或LONG RAW类型的列。 • 在O r a c l e 8及更高版本中,触发器体内的代码可以引用和使用 L O B(大型对象)的列,但不 能修改该列的值。上述限制也适用于对象列。 除此之外,还有对触发器访问的表的操作限制。根据触发器类型和对该表的限制,有时, 表可能要进行调整。 3. 触发器体 在O r a c l e 8 i之前的版本中,触发器的体必须是 P L / S Q L块。而在O r a c l e 8 i下,触发器 的体可以由调用语句组成,所调用的过程子程序可以是 P L / S Q L存储子程序,或是 C 语言使用的包(w r a p p e r),以及J a v a程序。借助于这种选择,我们可以创建一个其基础代码是用 J a v a编制的触发器。例如,假设我们要把数据库的连接和断开信息记录在下面的表中: 节选自在线代码relGTables.sql CREATE TABLE connect_audit ( user_name VARCHAR2(30), operation VARCHAR2(30), timestamp DATE); 现在,我们用下面的包来记录连接和断开信息: 节选自在线代码LogPkg.sql CREATE OR REPLACE PACKAGE LogPkg AS PROCEDURE LogConnect(p_UserID IN VARCHAR2); PROCEDURE LogDisconnect(p_UserID IN VARCHAR2); END LogPkg; CREATE OR REPLACE PACKAGE BODY LogPkg AS PROCEDURE LogConnect(p_UserID IN VARCHAR2) IS BEGIN INSERT INTO connect_audit (user_name, operation, timestamp) VALUES (p_USerID, 'CONNECT', SYSDATE); END LogConnect; PROCEDURE LogDisconnect(p_UserID IN VARCHAR2) IS BEGIN INSERT INTO connect_audit (user_name, operation, timestamp) VALUES (p_USerID, 'DISCONNECT', SYSDATE); END LogDisconnect; END LogPkg; 包L o n g P k g . L o g C o n n e c t和L o g P k g . L o g D i s c o n n e c t都以用户名作为其入口参数,并在表 c o n n e c t _ a u d i t中插入一行。最后,我们可以从触发器 L O G O N和L O G O F F中按下列代码调用这两 第6章 数据库触发器计计219下载个触发器: 节选自在线代码LogConnects.sql CREATE OR REPLACE TRIGGER LogConnects AFTER LOGON ON DATABASE CALL LogPkg.LogConnect(SYS.LOGIN_USER) / CREATE OR REPLACE TRIGGER LogDisconnects BEFORE LOGOFF ON DATABASE CALL LogPkg.LogDisconnect(SYS.LOGIN_USER) / 注意 由于L o g C o n n e c t和L o g D i s c o n n e c t都是系统数据库触发器,(与模式相反),创建 上述触发器必须具有系统权限 ADMINISTER DATABASE TRIGGER。 L o g C o n n e c t和L o g D i s c o n n e c t的触发器体只是简单的调用语句,用来指出待执行的过程,当 前用户是它们的唯一入口参数。在前面的例子中,语句 C A L L的目标是标准的P L / S Q L打包过程。 然而,语句C A L L的目标也可以是简单的 C语言使用的包或 J a v a外部例程使用的已包装。例如, 假设我们把下面的J a v a类载入到数据库中: 节选自在线代码Loger.sql import java.sql.*; import oracle.jdbc.driver.*; public class Logger { public static void LogConnect(String userID) throws SQLException { // Get default JDBC connection Connection conn = new OracleDriver().defaultConnection(); String insertString = "INSERT INTO connect_audit (user_name, operation, timestamp)" + " VALUES (?, 'CONNECT', SYSDATE)"; // Prepare and execute a statement that does the insert PreparedStatement insertStatement = conn.prepareStatement(insertString); insertStatement.setString(1, userID); insertStatement.execute(); } public static void LogDisconnect(String userID) throws SQLException { // Get default JDBC connection Connection conn = new OracleDriver().defaultConnection(); String insertString = "INSERT INTO connect_audit (user_name, operation, timestamp)" + 220计计第二部分 非对象功能 下载" VALUES (?, 'DISCONNECT', SYSDATE)"; // Prepare and execute a statement that does the insert PreparedStatement insertStatement = conn.prepareStatement(insertString); insertStatement.setString(1, userID); insertStatement.execute(); } } 如果我们接着再创建一 个如下所示的作为该类包装的包 L o g P k g : 节选自在线代码LogPkg2.sql CREATE OR REPLACE PACKAGE LogPkg AS PROCEDURE LogConnect(p_UserID IN VARCHAR2); PROCEDURE LogDisconnect(p_UserID IN VARCHAR2); END LogPkg; CREATE OR REPLACE PACKAGE BODY LogPkg AS PROCEDURE LogConnect(p_UserID IN VARCHAR2) IS LANGUAGE JAVA NAME 'Logger.LogConnect(java.lang.String)'; PROCEDURE LogDisconnect(p_UserID IN VARCHAR2) IS LANGUAGE JAVA NAME 'Logger.LogDisconnect(java.lang.String)'; END LogPkg; 我们可以使用相同的触发器来实现上述要求。读者请看本书第 1 0章有关外部例程及如何把 J a v a过程载入到数据库中的内容的介绍。 注意 触发器参数如I N S E RT I N G、U P D AT I N G和D E L E T I N G,以及: n e w和: o l d相关标识 符(还有: p a r e n t),只有在触发器体是完整的 P L / S Q L块而不是C A L L语句的情况下才可 以使用。 4. 触发器权限 下面的表6 - 6说明了五个适用于触发器的系统权限。除此之外,触发器的拥有者必须还具有 对其引用的对象的权限。由于触发器是已经编译的对象(从 O r a c l e 7的7 . 3版本开始),所有权限 都必须直接授权,不能通过角色授权 . 表6-6 与触发器有关的系统权限 系 统 权 限 说 明 C R E ATE TRIGGER 授予在其自己的模式下创建触发器的权限 C R E ATE ANY TRIGGER 授予在任何模式(除了 S Y S外)下创建触发器的权限。在 数据字典表中不推荐使用创建触发器 A LTER ANY TRIGGER 授予在任何模式(除了S Y S外)下启用、禁用或编译数据库 触发器的权限。注意,如果没有授予G R E ATE ANY TRIGGER 权限,用户不能改变发触发器代码 第6章 数据库触发器计计221下载(续) 系 统 权 限 说 明 DROP ANY TRIGGER 授予在任何模式下(除去 S Y S)撤消数据库触发器的权限 ADMINISTER DATABASE TRIGGER 授予创建或变更数据库系统触发器的权限(与当前模式相 反)。所授的权限必须具有 C R E ATE TRIGGER或C R E AT E ANY TRIGGER 6.2.5 触发器与数据字典 与存储子程序类似,数据字典视图包括了有关触发器和其执行状态的信息。这些视图必须 在触发器创建或撤消时进行更新。 1. 数据字典视图 当创建了一个触发器时,其源程序代码存储在数据库视图 u s e r _ t r i g g e r s中。该视图包括了触 发器体,W H E N子句,触发表,和触发器类型。例如,下面的查询返回有关 U p d a t e M a j o r S t a t s的 信息: SQL> SELECT trigger_type, table_name, triggering_event 2 FROM user_triggers 3 WHERE trigger_name = 'UPDATEMAJORSTATS'; TRIGGER_TYPE TABLE_NAME TRIGGERING_EVENT ---------------- -------------- -------------------------- AFTER STATEMENT STUDENTS INSERT OR UPDATE OR DELETE 有关数据字典视图的详细介绍,请看附录 C。 2. 撤消和禁止触发器 与过程和包相类似,触发器也可以被撤消。实现撤消功能的命令如下: DROP TRIGGER triggername; 其中,t r i g g e r n a m e是触发器的名称。该命令把指定的触发器从数据字典中永久性地删除。 类似于子程序,子句 OR REPLACE可用在触发器的 C R E AT E语句中。在这种情况下,要创建的 触发器已存在的话,则先将其删除。 与过程和包不同的是,触发器可以被禁止使用。当触发器被禁止时,它仍将存储在数据字 典中,但不再激活。禁止触发器的语句如下: ALTER TRIGGER triggername{DISABLE | ENABLE}; 其中,t r i g g e r n a m e是触发器的名称。当创建触发器时,所有触发器的默认值都是允许状态 (E N A B L E D)。语句A LTER TRIGGER可以禁止,或再允许任何触发器。例如,下面的代码先禁 止接着再允许激活触发器U p d a t e M a j o r S t a t s: SQL> ALTER TRIGGER UpdateMajorStats DISABLE; Trigger altered. SQL> ALTER TRIGGER UpdateMajorStats ENABLE; Trigger altered. 在使用命令 A LTER TA B L E的同时加入 ENABLE ALL TRIGGERS或DISABLE ALL 222计计第二部分 非对象功能 下载T R I G G E R S子句可以将指定表的所有触发器禁止或允许。例如: SQL> ALTER TABLE students 2 ENABLE ALL TRIGGERS; Table altered. SQL> ALTER TABLE students 2 DISABLE ALL TRIGGERS; Table altered. 视图u s e r _ t r i g g e r s的s t a t u s列包括有E N A B L E D或D I S A B L E D两个字符串用来指示触发器的当 前状态。禁止一个触发器将不从其数据字典中删除。 3. p-code触发器 当包或子程序存储在数据字典中时,其编译后的中间代码将同该对象的源代码一起存储, 对于触发器来说情况也一样。这就是说触发器不需重新编译就可调用,并且其相关信息也得到 保存。因此,触发器也可以向包和子程序那样自动无效。当触发器处于无效状态时,该触发器 将在下次激活时自动重编译。 在O r a c l e 7的7 . 3版前,系统对触发器的处理是不同的。在数据字典中唯一存储的是触发器的 源代码,而没有中间代码。结果,触发器每次从数据字典中载入时都要进行编译。虽然这样处 理并不影响触发器的定义和使用,但降低了触发器的运行效率。 6.3 变异表 系统对触发器将要访问的表和列有一些限制。为了定义这些限制,有必要来理解什么是变 异表(mutating table)和约束表(constraining table),变异表就是当前被D M L语句修改的表。对 触发器来说,变异表就是触发器在其上进行定义的表;由于执行 DELETE CASCADE引用完整性 约束而需要更新的表也是变异表。(有关引用完整性约束的详细信息,请看 O r a c l e服务器参考手 册。)约束表是一种需要实施引用完整性约束而读入的表。为了说明这些定义,请看表 r e g i s t e r e d _ s t u d e n t s ,该表的结构如下: 节选自在线代码relTables.sql CREATE TABLE registered_students ( student_id NUMBER(5) NOT NULL, department CHAR(3) NOT NULL, course NUMBER(3) NOT NULL, grade CHAR(1), CONSTRAINT rs_grade CHECK (grade IN ('A', 'B', 'C', 'D', 'E')), CONSTRAINT rs_student_id FOREIGN KEY (student_id) REFERENCES students (id), CONSTRAINT rs_department_course FOREIGN KEY (department, course) REFERENCES classes (department, course) ); 表r e g i s t e r e d _ s t u d e n t s有两个声明的引用完整性约束。在这种约束下,对表 r e g i s t e r e d - s t u e l e n t s来说,表s t u d e n t s和c l a s s e s都是约束表。由于这些限制,表 c l a s s e s和s t u d e n g t s也需要由 第6章 数据库触发器计计223下载D M L语句修改或查询。同样,表 r e g i s t e r e d _ s t u d e n t s自身在D M L语句的对其操作期间也是变异 表。 触发器体中的S Q L语句不能进行下列操作: • 读或修改触发语句的任何变异表,其中包括触发表本身。 • 读或修改触发表的约束表中的主关键字,唯一关键字和外部关键字列。除此之外的其他列 可以修改。 上述限制适用于所有的行级触发器,这些限制只在语句触发器作为 DELETE CASCADE操作 的结果激活时适用于语句触发器。 注意 如果I N S E RT语句只影响一行的话,则在该行的之前和之后触发器将不把触发表 作为变异表对待,这是在行级触发器可能载入或修改触发表时的唯一案例。下面一类的 语句 INSERT INTO table SELECT ... 总是把触发表作为变异表对待,即使其子查询仅返回一行也是如此。 作为例子,请考虑下面的触发器 C a s c a d e R S I n s e r t s。尽管该触发器修改表 s t u d e n t s和c l a s s e s, 但由于修改的表s t u d e n t s和c l a s s e s中的列不是关键字列,所以修改是合法的。下面,我们将分析 一个非法的触发器案例。 节选自在线代码CascadeRSInserts .sql CREATE OR REPLACE TRIGGER CascadeRSInserts /* Keep the registered_students, students, and classes tables in synch when an INSERT is done to registered_students. */ BEFORE INSERT ON registered_students FOR EACH ROW DECLARE v_Credits classes.num_credits%TYPE; BEGIN -- Determine the number of credits for this class. SELECT num_credits INTO v_Credits FROM classes WHERE department = :new.department AND course = :new.course; -- Modify the current credits for this student. UPDATE students SET current_credits = current_credits + v_Credits WHERE ID = :new.student_id; -- Add one to the number of students in the class. UPDATE classes SET current_students = current_students + 1 WHERE department = :new.department AND course = :new.course; 224计计第二部分 非对象功能 下载END CascadeRSInserts; 6.3.1 变异表案例介绍 假设我们要把每个专业的学生名额限制在五个。我们可以通过使用表 s t u d e n g t s上的之前插入 或更新行级触发器来实现这种限制,下面是该触发器的代码: 节选自在线代码LimitMajors .sql CREATE OR REPLACE TRIGGER LimitMajors /* Limits the number of students in each major to 5. If this limit is exceeded, an error is raised through raise_application_error. */ BEFORE INSERT OR UPDATE OF major ON students FOR EACH ROW DECLARE v_MaxStudents CONSTANT NUMBER := 5; v_CurrentStudents NUMBER; BEGIN -- Determine the current number of students in this -- major. SELECT COUNT(*) INTO v_CurrentStudents FROM students WHERE major = :new.major; -- If there isn't room, raise an error. IF v_CurrentStudents + 1 > v_MaxStudents THEN RAISE_APPLICATION_ERROR(-20000, 'Too many students in major ' || :new.major); END IF; END LimitMajors; 初看,该触发器好象可以实现需要的功能。然而,如果我们更新表 s t u d e n t s并激活该触发器, 我们会得到下面的输出: 节选自在线代码LimitMajors .sql SQL> UPDATE students 2 SET major = 'History' 3 WHERE ID = 10003; UPDATE students * ERROR at line 1: ORA-04091: table EXAMPLE.STUDENTS is mutating, trigger/function may not see it ORA-06512: at line 7 ORA-04088: error during execution of trigger 'EXAMPLE.LIMITMAJORS' 由于触发器L i m i t M a j o r查询其自己的触发表(该表是变异表),所以导致了错误 O R A - 4 0 9 1。 另外,错误O R A - 4 0 9 1是在该触发器激活时引发的,而不是在创建时引发的。 第6章 数据库触发器计计225下载6.3.2 变异表错误的处理 表s t u d e n t s由于使用了行级触发器而成了变异表,这就使我们不能在该行级触发器中对该表 进行查询,而只能在语句级触发器使用查询语句。然而,我们还不能只是简单地把触发器 L i m i t M a j o r修改为语句级触发器,这是因为我们需要在触发器体中使用 : n e w. m a j o r的值。解决该 问题的方法是创建两个触发器,即一个行级触发器和一个语句级触发器。在行级触发器中,我 们记录: n e w. m a j o r的值,但我们不查询表 s t u d e n t s;而查询任务由语句级触发器来实现并使用行 触发器记录的值。 至于如何记录: n e w. m a j o r的值。一种方法是在包的内部使用 P L / S Q L表来记录。用这种方法, 我们可以在每次更新时保存多个值。同样,每个会话也得到其自己打包变量的实例,因此,我 们就不必担心由于不同会话进行同时更新操作而引起的麻烦。实现上述方案的包 s t u d e n t _ d a t a ,以 及改进的触发器R L i m i t M a j o r s和S L i m i M a j o r s的程序如下: 节选自在线代码mutating .sql CREATE OR REPLACE PACKAGE StudentData AS TYPE t_Majors IS TABLE OF students.major%TYPE INDEX BY BINARY_INTEGER; TYPE t_IDs IS TABLE OF students.ID%TYPE INDEX BY BINARY_INTEGER; v_StudentMajors t_Majors; v_StudentIDs t_IDs; v_NumEntries BINARY_INTEGER := 0; END StudentData; CREATE OR REPLACE TRIGGER RLimitMajors BEFORE INSERT OR UPDATE OF major ON students FOR EACH ROW BEGIN /* Record the new data in StudentData. We don't make any changes to students, to avoid the ORA-4091 error. */ StudentData.v_NumEntries := StudentData.v_NumEntries + 1; StudentData.v_StudentMajors(StudentData.v_NumEntries) := :new.major; StudentData.v_StudentIDs(StudentData.v_NumEntries) := :new.id; END RLimitMajors; CREATE OR REPLACE TRIGGER SLimitMajors AFTER INSERT OR UPDATE OF major ON students DECLARE v_MaxStudents CONSTANT NUMBER := 5; v_CurrentStudents NUMBER; v_StudentID students.ID%TYPE; v_Major students.major%TYPE; BEGIN /* Loop through each student inserted or updated, and verify 226计计第二部分 非对象功能 下载that we are still within the limit. */ FOR v_LoopIndex IN 1..StudentData.v_NumEntries LOOP v_StudentID := StudentData.v_StudentIDs(v_LoopIndex); v_Major := StudentData.v_StudentMajors(v_LoopIndex); -- Determine the current number of students in this major. SELECT COUNT(*) INTO v_CurrentStudents FROM students WHERE major = v_Major; -- If there isn't room, raise an error. IF v_CurrentStudents > v_MaxStudents THEN RAISE_APPLICATION_ERROR(-20000, 'Too many students for major ' || v_Major || ' because of student ' || v_StudentID); END IF; END LOOP; -- Reset the counter so the next execution will use new data. StudentData.v_NumEntries := 0; END LimitMajors; 注意 请注意要在运行前面的脚本之前,一定要撤消不正确的 L i m i t M a j o r s触发器。 现在,我们可以通过更新表 s t u d e n t来测试这一系列触发器直到我们的程序中出现了过多的 历史专业为止 节选自在线代码mutating .sql SQL> UPDATE students 2 SET major = 'History' 3 WHERE ID = 10003; 1 row updated. SQL> UPDATE students 2 SET major = 'History' 3 WHERE ID = 10002; 1 row updated. SQL> UPDATE students 2 SET major = 'History' 3 WHERE ID = 10009; UPDATE students * ERROR at line 1: ORA-20000: Too many students for major History because of student 10009 ORA-06512: at "EXAMPLE.SLIMITMAJORS", line 19 ORA-04088: error during execution of trigger 'EXAMPLE.SLIMITMAJORS' 上面的结果就是我们期望的功能。当行级触发器读入或修改变异表( mutating table)时, 第6章 数据库触发器计计227下载上述技术可以引发错误信息 O R A - 4 0 9 1。为了避免在行级触发器中进行非法处理,我们把该处理 推迟到一个之后的语句级触发器实现,这样一来该处理就是合法的了。打包的 P L / S Q L表是用来 存储被修改的行的。 下面是应用该技术时要注意的几个问题: • 由于P L / S Q L表是位于包中,所以这些表对于行级触发器和语句级触发器都是可见的,确 保变量的全局性的唯一方法是把要定义的全局变量放在包中。 • 我们在该程序中使用了计数器变量 S t u d e n t s D a t a . v _ N u m E n t r i e s,当其所在的包首次创建时, 该变量的初始值为0。然后,该变量的值由行级触发器修改。语句级触发器对该变量进行 引用并在处理结束后将该变量设置为0。上述的步骤是必须的,因为只有这样该会话发布 的U P D AT E语句将实现正确的结果。 • 对触发器S L i m i t M a j o r s进行最多学生数量的检查将稍有变化。由于该触发器是语句之后触 发器,所以变量 v _ C u r r e n t S t u d e n t s将在执行插入或更新命令后保存某专业的学生数量。因 此,在触发器L i m i t M a j o r中对变量v_CurrentStudents+1 的检查也被变量v _ C u r r e n t S t u d e n t s 代替。 • 我们也可以使用数据库表来代替 P L / S Q L表。我个人不喜欢这种技术,这是由于发布 U P D AT E的多个同时会话之间可能有相互用(在 O r a c l e 8 i中,我们可以使用临时表)。打包 的P L / S Q L表是众多会话之间唯一的,因此解决了上面的问题。 6.4 小结 正如我们在本章所看到的,触发器是继 P L/S Q L与O r a c l e数据库之后的重要工具。我们可以 使用该工具来加强比正常的引用完整性约束条件更复杂的数据约束。 O r a c l e 8 i把触发器扩展到了 除了在表或视图上进行DML操作以外的事件。本章介绍的触发器结束了我们在前三章中对命 名P L / S Q L块的讨论。我们将在第1 3章中看到的另一种命名P L / S Q L块是对象类型体。下面一章将 讨论P L / S Q L中的内置包问题。 228计计第二部分 非对象功能 下载下载 第7章 数据库作业和文件输入输出 本章将讨论P L / S Q L内置包D B M S _ J O B和U T L _ F I L E的特性。P L / S Q L 2 . 2以上版本提供的包 D B M S _ J O B支持存储过程在系统的管理下周期性自动运行而无须用户的介入。而 P L / S Q L 2 . 3以 上版本提供的包 U T L _ F I L E则扩充了读写系统文件的功能。这两个包提供的功能使 P L / S Q L具备 了与其他第三代程序设计语言相同的处理能力。 7.1 数据库作业 在P L / S Q L 2 . 2及更高版本下,我们可以指定 P L / S Q L程序在指定时间运行,支持这种 定时运行功能的是包 D B M S _ J O B提供的作业序列。 O r a c l e中作业的运行是通过将该 作业和说明该作业运行运行方式的参数共同提交给作业序列实现的。有关当前运行的作业,上 一个提交的作业的成功或失败状态等信息可以在数据字典中找到(请参阅 7 . 1 . 4节)。 读者需要注意的是,O r a c l e 8及更高版本支持的高级查询功能提供了比包 D B M S _ J O B 功能更强大的P L / S Q L查询功能。有关该功能的细节,请参考 O r a c l e文档资料。 7.1.1 后台进程 一个O r a c l e实例是由运行在系统上的各种进程所组成,不同的进程负责实现不同的数据库操 作,如把数据库记录读入内存,把数据库记录写回磁盘,以及把数据归档到脱机存储器中。除 了管理数据库的处理外,数据库系统还具有叫做 S N P的进程。这些后台进程除了要处理快照 (S n a p s h o t)的自动刷新外,还要通过 D B M S _ J O B来管理访问作业队列的访问通道。 象其他的数据库进程运行方式一样,S N P进程也运行在后台。然而,与数据库进程不同的是, 如果S N P进程失败的话,O r a c l e就将重新启动该进程并不影响数据库的其他进程。如果其他的数 据库进程失败的话,这些失败的进程将会使数据库停止运行。S N P周期性地激活来检查作业序列。 如果论到某个作业运行,则 S N P进程就在启动该进程运行后再进入睡眠状态。一个给定的进程只 能一次运行一个作业。在 O r a c l e 7系统下,系统限定的最大 S N P进程的数目是 1 0个(从S N P 0 - S N P 9),因此,系统中可同时运行的最大作业的数目也是 1 0个。在O r a c l e 8系统中,S N P进程最 大数目增加到了3 6个,其进程号是S N P 0 - S N P 9 , S N PA - S N P Z。 数据库初始化文件( i n i t . o r a)中有两个参数用来控制 S N P进程的属性。这两个参数是 J O B _ Q U E U E _ P R O C E S S E S和J O B _ Q U E U E _ I N T E RVA L,下面的表7 - 1是该参数的说明。值得注 意的是,如果将J O B _ Q U E U E _ P R O C E S S E S设置为0的话,系统将禁止作业运行。由于每个进程 都将在查询新的作业之前按 J O B _ Q U E U E _ I N T E RVA L指定的时间(以秒为单位)进行睡眠,所 以参数J O B _ Q U E U E _ I N T E RVA L就指定了运行两个作业之间的最小时间间隔。上述两个参数都 不能使用A LTER SYSTEM或A LTER SESSION进行动态修改,因此用户必须先对数据库初始化文件进行修改并重启数据库才能使修改的参数生效。 注意 O r a c l e 7 . 3以及更高版本中已经不在使用 O r a c l e 7 . 2版使用的初始化参数 J O B _ Q U E U E _ K E E P _ C O N N E C T I O N S。从O r a c l e 7 . 3版开始数据库的物理连接完全由系统自 动实施控制。 表7-1 作业初始化参数 参 数 默 认 值 范 围 说 明 J O B _ Q U E U E _ P R O C E S S E S 0 0~ 1 0 (在O r a c l e 8中为0~3 6 ) 可同时运行的进程数目 J O B _ Q U E U E _ I N T E RVA L 6 0 1~3 6 0 0 s 两个进程间唤醒的间隔。进程按指定的时 间进行睡眠 7.1.2 运行作业 运行作业的方法有两种,一种是将作业提交给作业队列,另一种是强制作业立即运行。当 作业被提交给作业队列时, S N P就在该作业的启动时刻运行该作业。如果指定了作业的运行间隔 的话,该作业将自动地周期运行。如果作业是立即启动的,该作业就运行一次。 1. 提交:S U B M I T 使用下面的S U B M I T过程可以将作业提交到作业队列中。该过程的的语法是: 节选自在线代码TempInsert.sql PROCEDURE SUBMIT( job OUT BINARY_INTEGER, what IN VARCHAR2, next_date IN DATE DEFAULT SYSDATE, interval IN VARCHAR2 DEFAULT NULL, no_parse IN BOOLEAN DEFAULT FALSE); 下面说明了S U B M I T语句中使用的参数: 参 数 类 型 说 明 j o b B I N A RY _ I N T G E R 作业号。创建作业时,作业将被赋予一个作业号。只要该作业存在,其作业号 将保持不变。作业号在实例的范围内是唯一的 w h a t VA R C H A R 2 组成作业的P L / S Q L代码。通常,该代码是存储过程的调用 n e x t _ d a t e D AT E 作业下一次运行的日期 i n t e r v a l VA R C H A R 2 计算作业再次运行时间的函数。该函数的值必须是一个时间值或为空 N U L L n o _ p a r s e B O O L E A N 如果该参数为真的话,作业将在其第一次运行时才进行语法分析。如果该参数 为假值的话(默认值),则作业在提交时就对其进行语法分析。如果作业引用 的数据库对象不存在但又必须提交该作业时,可以将该参数设置为真。(被引 用的对象在运行时必须存在。) O r a c l e 8 i允许作业运行在 Oracle Parallel Server(OPS)环境下的指定实例中。 DBMS_JOB. SUBMIT提供的两个参数— i n s t a n c e和f o r c e以实现上述功能。这两个 参数的意义7 . 1 . 3节的“实例仿射性”中讨论。 例如,假设我们用下面的程序来创建过程 Te m p I n s e r t : 230计计第二部分 非对象功能 下载节选自在线代码TempInsert.sql CREATE SEQUENCE temp_seq START WITH 1 INCREMENT BY 1; CREATE OR REPLACE PROCEDURE TempInsert AS BEGIN INSERT INTO temp_table (num_col, char_col) VALUES (temp_seq.nextval, TO_CHAR(SYSDATE, 'DD-MON-YYYY HH24:MI:SS')); COMMIT; END TempInsert; 我们可以使用下面的SQL *Plus脚本指定过程Te m p I n s e r t每9 0秒钟运行一次: 节选自在线代码TempInsert.sql SQL> VARIABLE v_JobNum NUMBER SQL> BEGIN 2 DBMS_JOB.SUBMIT(:v_JobNum, 'TempInsert;', SYSDATE, 3 'sysdate + (90/(24*60*60))'); 4 COMMIT; 5 END; 6 / PL/SQL procedure successfully completed. SQL> print v_JobNum V_JOBNUM --------- 2 注意 要启动作业运行必须提交包括调用 D B M S _ J O B . S U B M I T语句的事务。其原因是 S U B M I T语句通过在数据字典表中插入一行来记录作业信息, S N P进程将查询该表以决 定是否有作业需要运行。在这种情况下, I N S E RT和S E L E C T语句是由不同的会话实现 的,所以必须提交事务。 如果把初始化参数 J O B _ Q U E U E _ P R O C E S S E S设置为本0,我们仍然可以发布 S U B M I T命令 而不会出错。但在这种情况下,作业无法运行。这样一来,为了观察过程 Te m p I n s e r t对表 t e m p _ t a b l e的插入操作,我们至少要把 J O B _ Q U E U E _ P R O C E S S E S的值设置为 1。如果 J O B _ Q U E U E _ I N T E RVA L是其默认值(6 0 s),则作业可能不会在9 0 s后开始运行。在运行该作业 所需时间之后的 9 0 s内,该作业将被标记为就绪运行状态。然而,该作业的真正运行时间是在 S N P进程唤醒后才可能开始,(唤醒S N P需要6 0 s时间)。因此,在该作业再次运行前,有可能要 等待长达1 5 0 s。例如,下面是作业运行三次后表 Te m p _ t b a l e的输出样本: SQL> SELECT * FROM temp_table; NUM_COL CHAR_COL --------- ------------------------- 1 25-APR-1999 18:18:59 2 25-APR-1999 18:21:02 3 25-APR-1999 18:23:05 第7章 数据库作业和文件输入输出计计231下载232计计第二部分 非对象功能 下载 在上例中,在作业第一次和第二次运行期间,以及第二次和第三次运行之间的间隔都是 1 2 3 s。 注意 上面S U B M I T调用将启动过程Te m p I n s e r t周期性地运行直到数据库关闭,或该作 业被R E M O V E调用删除为止。7 . 1 . 3节中的“删除作业”将介绍 R E M O V E语句。 作业号 当一个作业初次被提交时,该作业就被赋予一个作业号,该作业号是由序列 S Y S . J O B E Q生成的。一旦给作业赋予了作业号,该作业号将在作业被删除或再次提交前保持不 变。 警告 可以象处理数据库对象那样对作业进行导入或导出操作,这种操作不会变更作业 的作业号。如果企图导入一个其作业号已经存在的作业的话,系统将给出错误信息,并 将禁止将该作业导入。在这种情况下,只须再次提交该作业,就可以生成新的作业号。 我们也可以使用过程U S E R _ E X P O RT来导出作业。 作业定义 参数w h a t指定了作业的代码。通常,作业由存储过程组成,因此参数 w h a t应是调 用存储过程的字符串。我们将在本节的后面介绍该字符串的完整格式。参数 w h a t调用的过程可 以带有任何数量的参数。但所有参数都必须是 I N类型参数,这是由于该过程没有实参来接收 O U T或IN OUT形参的值。该规则的唯一例外是特殊标识符 n e x t _ d a t e和b r o k e n (将在后面介绍)。 警告 一旦提交了作业,该作业将由后台的 S N P进程控制运行。为了了解运行结果,一 定要在作业过程末尾写上提交 C O M M I T代码。如果作业不发布 C O M M I T命令,当运行 该作业的会话结束时,则事务将自动地重新开始。 如表7 - 2所示,在作业定义中有三个可以合法使用的标识符。其中,参数 j o b是I N类型的参数, 因此作业只能读该参数的值。参数 n e x t _ d a t e和b r o k e n都是IN OUT参数,所以,该作业可以对它 们进行修改。 表7-2 作业控制标识符 标 示 符 类 型 说 明 j o b B I N A RY _ I N T E G E R 计算当前作业的作业号 n e x t _ d a t e D AT E 计算作业下一次运行的日期。如果作业将该参数设置为空的话, 则该作业将被从队列中删除 b r o k e n B O O L E A N 计算作业的状态,如果作业被中断,则为真值,否则为假值。 如果作业自身将该参数设置为真的话,该作业将被标记为执行中 断,但不会从队列中删除 假设我们对Te m p I n s e r t修改如下: 节选自在线代码TempInsert.sql CREATE OR REPLACE PROCEDURE TempInsert (p_NextDate IN OUT DATE) AS v_SeqNum NUMBER; v_StartNum NUMBER; v_SQLErr VARCHAR2(60); BEGIN第7章 数据库作业和文件输入输出计计233下载 SELECT temp_seq.NEXTVAL INTO v_SeqNum FROM dual; -- See if this is the first time we're called. BEGIN SELECT num_col INTO v_StartNum FROM temp_table WHERE char_col = 'TempInsert Start'; -- We've been called before, so insert a new value. INSERT INTO temp_table (num_col, char_col) VALUES (v_SeqNum, TO_CHAR(SYSDATE, 'DD-MON-YYYY HH24:MI:SS')); EXCEPTION WHEN NO_DATA_FOUND THEN -- First time we're called. First clear out the table. DELETE FROM temp_table; -- And now insert. INSERT INTO temp_table (num_col, char_col) VALUES (v_SeqNum, 'TempInsert Start'); END; -- If we've been called more than 5 times, exit. IF v_SeqNum - V_StartNum > 5 THEN p_NextDate := NULL; INSERT INTO temp_table (num_col, char_col) VALUES (v_SeqNum, 'TempInsert End'); END IF; COMMIT; EXCEPTION WHEN OTHERS THEN -- Record the error in temp_table. v_SQLErr := SUBSTR(SQLERRM, 1, 60); INSERT INTO temp_table (num_col, char_col) VALUES (temp_seq.NEXTVAL, v_SQLErr); -- Exit the job. p_NextDate := NULL; COMMIT; END TempInsert; 提交如下: 节选自在线代码TempInsert1.sql BEGIN234计计第二部分 非对象功能 下载 DBMS_JOB.SUBMIT(:v_JobNum, 'TempInsert(next_date);', SYSDATE, 'SYSDATE + (60/(24*60*60))'); COMMIT; END; 现在,该作业将每 6 0秒运行一次,并在被调用五次之后自动地将自己从作业队列中删除 (通过把P _ N e x t D a t e改置为N u l l)。该作业的输出样本如下: SQL> SELECT * FROM temp_table ORDER BY num_col; NUM_COL CHAR_COL --------- ------------------------------------- 1 TempInsert Start 2 25-APR-1999 18:45:37 3 25-APR-1999 18:46:38 4 25-APR-1999 18:47:40 5 25-APR-1999 18:48:41 6 25-APR-1999 18:49:43 7 25-APR-1999 18:50:44 7 TempInsert End 注意 在运行该上面的例子之前,一定要使用 D B M S _ J O B . R E M O V E来删除前面调用的 Te m p I n s e r t作业。 通过参数n e x t _ d a t e (与参数b r o k e n一起或单独使用)的返回值,作业可以把自己从作业队列中 删除。 参数w h a t是一个VA R C H A R 2类型的字符串。这样做的结果使得用于调用作业过程中使用的 任何字符都必须用两个单引号括起来,而过程调用也必须以分号结束。例如,如果我们使用下 面的声明创建一个过程: CREATE OR REPLACE PROCEDURE Test(p_InParameter In VARCHAR2); 这时,我们可以使用下面的 w h a t字符串来提交该过程: 'Test("This is a character string");' 运行间隔 参数n e x t _ d a t e指定了作业在提交后的运行时间,该参数的值是在该作业第一次运 行时计算的。就在该作业运行之前,对由参数 i n t e r v a l给定的函数进行求值。如果该作业执行成 功,则由i n t e r v a l返回的结果就变成了新的 n e x t _ d a t e参数(假设该作业没有说明 n e x t _ d a t e参数)。 如果作业执行成功并且对 i n t e r v a l的求值结果为空,则该业就被从作业队列中删除。参数 i n t e r v a l 给出的表达式是按字符串传递的,但其结果应是日期值。下面是对某些常用的表达式和它们的 作用的说明: 间 隔 值 执 行 结 果 ' S Y S D AT E + 7 ' 从上次执行后的整一个星期。如果作业在星期二初次提交,则下一次运行将在下一个 星期二。如果第二次运行失败的话,而在星期三获得成功,则后续的运行将在星期三开始 ' N E X T _ D AY ( T R U N C 每星期五中午运行。请在字符串内使用两个单引号把 F R I D AY栝起来 ( S Y S D AT E ) , " F R I D AY " ) + 1 2 / 2 4 ' S Y S D AT E + 1 / 2 4 每小时第7章 数据库作业和文件输入输出计计235下载 2. 运行命令R U N 过程D B M S _ J O B . R U N可以立即启动作业运行。其语法如下: RUN(job IN BIANRY_INTEGER); 其中,作业必须是通过调用 S U B M I T已创建的作业。该命令不管指定作业的当前状况如何, 立即在当前进程下运行该作业。需要提醒的是, S N P后台进程与该作业无关。 警告 D B M S _ J O B . R U N将会初始化当前会话包,这样做的原因是为了给作业提供一个 连续的环境来运行,就象S N P进程对作业的处理一样。 7.1.3 其他的D B M S _ J O B子程序 下面,我们来讨论过程 D B M S _ J O B中其他子程序的功能。表 7 - 3对这些子程序做了说明。在 D B M S _ J O B的包头中有一个额外的程序— I S U B M I T。该程序是由其他过程使用指定的作业号 进入作业的接口程序。 表7-3 DBMS_JOB子程序说明 子 程 序 名 功 能 说明 S U B M I T 把把一个新的作业提交给队列,由 S N P进程控制运行 I S U B M I T 把由其他过程使用指定的作业号进入作业的接口程序。程序员不能在其程序代码中直接使 用该程序,只能使用S U B M I T R U N 把强迫指定的作业运行在当前进程中 R E M O V E 把从作业队列中删除一个作业 B R O K E N 把标记作业中断或没有中断 C H A N G E 把变更作业中任何可配置的字段 W H AT 把变更w h a t字段 N E X T _ D AT E 把变更n e x t _ d a t e字段 I N T E RVA L 把变更i n t e r v a l字段 I N S TA N C E 把在O r a c l e 8 i及更高版本下,变更i n s t a n c e字段 U S E R _ E X P O RT 把返回需要重新创建作业调用的文本 C H E C K _ P R I V S 把确认给定的作业号可访问 1. 删除作业 我们在本章前几节中已经看到,通过把参数 n e x t _ d a t e设置为空,作业可以把自己从作业队 列中删除。我们也可以使用下面的过程显式地把作业从队列中删除: R E M O V E(job IN BINARY _ I N T E G E R); 上面唯一的参数是作业号。如果该作业的 n e x t _ d a t e的值为空的话(该作业所设置的值或参 数间隔的值为空),则该作业在其结束运行后将被删除。如果调用删除过程时,该作业正在运行 期间,则该作业将在其运行结束后从队列中删除。 2. 中断作业 对于运行失败的作业, O r a c l e系统将自动重新运行该作业。该作业将在其第一次运行失败 一分钟后再次运行。如果上述努力再次失败,下一次运行将在两分钟后开始。每次运行的间隔 将逐次加倍,从四分钟,到八分种,一直持续下去。如果重试间隔超过了该作业的执行间隔,则使用执行间隔。一旦该作业失败了 1 6次,它就被标识为中断的作业。中断的作业将不能再次 运行。 我们可以使用 R U N命令来运行中断的作业。如果调用成功的话,则该作业的失败记数器将 重置为0,并且将该作业的中断标志取消。过程 B R O K E N也可以用来修改作业的状态。该过程的 语法如下: BROKEN( job IN BINARY_INTEGER, broken IN BOOLEAN, next_date IN DATE DEFAULT SYSDATE); 该过程参数的说明如下: 参 数 类 型 说 明 j o b B I A N A RY _ I N T E G E R 把作业的状态将要发生变更的作业号 b r o k e n B O O L E A N 把作业的新状态。如果该值为真,则该作业就被标识为中断的作业。如果 为假值,则该作业没有被中断并将在由n e x t _ d a t e指定的时间到来时再次运行。 n e x t _ d a t e D AT E 把作业下次运行的日期。其默认值是 S Y S D AT E 3. 变更作业 在作业被提交后,该作业的参数也可以变更。实现这种功能的过程的语法如下: PROCEDURE CHANGE( job IN BINARY_INTEGER, what IN VARCHAR2 DEFAULT NULL, next_date IN DATE DEFAULT NULL, interval IN VARCHAR2 DEFAULT NULL); PROCEDURE WHAT( job IN BINARY_INTEGER, what IN VARCHAR2); PROCEDURE NEXT_DATE( job IN BINARY_INTEGER, next_date IN DATE); PROCEDURE INTERVAL( job IN BINARY_INTEGER, interval IN VARCHAR2); 过程C H A N G E用来一次变更一个以上作业的属性,过程 W H AT、N E X T _ D AT E、I N T E RVA L 则用来改变与它们相关参数标识的属性。 以上所有参数的作用都与过程 S U B M I T中参数的功能一样。如果我们使用 C H A N G E或 W H AT来修改w h a t参数,则当前环境就将变为适应该作业的新的执行环境。有关作业环境的进 一步介绍,请参阅7 . 1 . 5节。 4. 实例仿射性 当我们使用O P S(Oracle Parallel Server)时,我们可以指定一个实例来运行给定的作 业。这就叫做实例仿射性( Instance Aff i n i t y)。实例可以由过程I N S TA N C E指定,其 语法是: PROCEDURE INSTANCE( job IN BINARY_INTEGER, instance IN BINARY_INTEGER, force IN BOOLEAN DEFAULT FALSE); 236计计第二部分 非对象功能 下载其中,i n s t a n c e是运行作业的实例号。如果实例是 D B M S _ J O B . A N Y _ I N S TA N C E ( 0 ),则作业 的仿射性将变更并且任何可用的实例都可以运行该作业,此时与参数 f o r c e的值无关。如果 i n s t a n c e是正值并且参数 f o r c e为假值,则作业仿射性只有在指定的实例处于运行状态才被变更。 如果指定的实例没有运行,或该实例不合法,这时 O r a c l e 8 i将返回错误:“O R A - 2 3 4 2 8:与实例 号字符串相关联的作业非法。” 其他D B M S _ J O B子程序也支持实例仿射性。例如,如下所示的过程 S U B M I T就带有参数 i n s t a n c e和f o r c e: PROCEDURE SUBMIT( job OUT BINARY_INTEGER, what IN VARCHAR2, next_date IN DATE DEFAULT SYSDATE, interval IN VARCHAR2 DEFAULT NULL, no_parse IN BOOLEAN DEFAULT FALSE, instance IN BINARY_INTEGER DEFAULT ANY_INSTANCE, force IN BOOLEAN DEFAULT FALSE); 参数i n s t a n c e和f o r c e的含义和使用方法都与过程 D B M S _ J O B . I N S TA N C E的同类参数一样。 过程C H A N G E在O r a c l e 8 i下得到了增强: PROCEDURE CHANGE( job IN BINARY_INTEGER, what IN VARCHAR2 DEFAULT NULL, next_date IN DATE DEFAULT NULL, interval IN VARCHAR2 DEFAULT NULL, instance IN BINARY_INTEGER DEFAULT NULL, force IN BOOLEAN DEFAULT FALSE); 其中参数i n s t a n c e和f o r c e与上面的参数一样。 最后,D B M S _ J O B . R U N也带有f o r c e参数: PROCEDURE RUN( job IN BINARY_INTEGER, force IN BOOLEAN DEFAULT FALSE); 如果参数f o r c e为真,则该作业仅可以在从指定实例内部调用 D B M S _ J O B . R U N的情况下运 行。 5. 导出作业 过程U S E R _ E X P O RT返回重新创建给定作业所需的文本: PROCEDURE USER_EXPORT( job in BINARY_INTEGER, mycall IN OUT VARCHAR2); 例如,如果我们通过Te m p I n s e r t的第二版提交的作业调用过程 U S E R _ E X P O RT的话,该过程 将返回下面的文本: 节选自在线代码jobExport.sql SQL> DECLARE 2 v_JobText VARCHAR2(2000); 3 BEGIN 4 DBMS_JOB.USER_EXPORT(:v_JobNum, v_JobText); 5 DBMS_OUTPUT.PUT_LINE(v_JobText); 6 END; 第7章 数据库作业和文件输入输出计计237下载238计计第二部分 非对象功能 下载 7 / dbms_job.isubmit(job=>10,what=>'TempInsert(next_date);',next _date=>to_date('1999-03-29:00:07:37','YYYY-MM-DD:HH24:MI:SS' ),interval=>'sysdate + (5/(24*60*60))',no_parse=>TRUE); 该作业必须在当前作业队列中,否则过程 U S E R _ E X P O RT将返回错误:“O R A - 2 3 4 2 1:作业 号j o b _ n u m不在作业队列中”。 提示 U S E R _ E X P O RT返回对 D B M S _ J O B . I S U B M I T的调用,而不是 D B M S _ J O B . S U B M I T 的调用。如果要重新创建具有相同作业号的作业的话,我们可以使用 I S U B M I T。然而,这种方法不太常用。我们推荐使用过程 S U B M I T。该命令将重新提 交作业并生成一个新的作业号。 U S E R _ E X P O RT是由导出工具程序自动调用并生成后面 重新导入时所使用的代码。 6. 检查作业的权限 过程C H E C K _ P R I V S有两个功能:其一是校验给定的作业号是否存在,其二是锁定数据字典 中的相应的行以便程序对其进行修改。该过程的语法定义如下: PROCEDURE CHECK_PRIVS(job IN BINARY _ I N T E G E R ) ; 其中,j o b是作业号,如果该作业号不存在,或该作业号没有被当前用户提交,则 O r a c l e将 提示错误信息:“O R A - 2 3 4 2 1:作业号j o b _ n u m不在作业队列中”。 7.1.4 在数据库视图中观察作业 O r a c l e有几个数据字典视图专门用来记录作业信息。其中视图 d b a _ j o b s和u s e r _ j o b s返回作业 的w h a t、n e x t _ d a t e、i n t e r v a l等有关信息。除此之外,这些视图还提供运行环境的信息。视图 d b a _ j o b s _ r u n n i n g描述了当前运行的作业。有关这些视图的的介绍,请看本书附录 C的内容。 7.1.5 作业运行环境 当我们把作业提交给队列时,当前的环境就被记录下来。记录信息中包括如 N L S _ D AT E _ F O R M AT之类的N L S参数的设置。这些在作业创建时记录下来的信息将在该作业运行时使用。 如果我们使用C H A N G E或W H AT过程修改w h a t的属性时,上述设置将随之改变。 注意 作业可以通过发布DBMS_SQL 包的A LTER SESSION命令(或O r a c l e 8 i中的本地 动态S Q L命令)来修改其运行环境。如果运行环境发生了变更,它将仅影响当前作业的 运行,而不会对将来的运行有影响。包 D B M S _ S Q L和本地动态S Q L的内容在本书的第8 章中介绍。 作业将运行在其自己提交器所属的权限组之下,不允许任何角色运行。如果我们需要在作 业中允许角色执行,我们可以使用动态 S Q L来发布SET ROLE命令。当然,如果该作业是由存储 过程组成的话,则所有的角色都将被禁止。 作业仅可以由提交器( S u b m i t t e r)控制运行。作业的执行权限与作业没有任何关联;包自 身的E X E C U T E权限是唯一必要的数据库权限。7.2 文件输入输出 如上所述,P L / S Q L语言并没有把输入、输出功能配置在该语言内部, P L / S Q L下的 输入输出是通过所提供的包所支持的功能实现的。 P L / S Q L输出到屏幕的功能是通过 本书第3章介绍的包DBMS_OUTPUT 所提供的功能实现的。 P L / S Q L 2 . 3版通过包U T L _ F I L E的功 能把文件I / O扩充到了文本文件。在该版本下无法借助其 U T L _ F I L E包来把输出直接转换为二进 制文件。 O r a c l e 8允许通过使用类型 B F I L E来读入二进制文件 ,这里B F I L E是特殊形式的外部 L O B。本书的第1 5,1 6将专门讨论B F I L E类型以及L O B的其他类型。然而,即使使 用O r a c l e 8 i,包U T L _ F I L E也不能用来处理二进制文件。 本节将描述U T L _ F I L E的工作原理。本章结尾处将提供三个完整的案例来介绍该包的使用方 法。 7.2.1 安全 客户端P L / S Q L有一个类似于U T L _ F I L E的包叫做T E X T _ I O的包。值得注意的是,客户端要 处理的安全问题要比服务器端要多,使用客户端包 T E X T _ I O首次创建的文件在其操作系统权限 的限制下,可以放置在客户端的任何地方。 P L / S Q L或数据库本身没有与其关联的用于客户端 I / O 操作的权限。我们将在本节讨论 U T L _ F I L E的安全实现。 1. 数据库安全 数据库服务器需要更可靠的安全机制来保证系统安全运行。 O r a c l e数据库通过限制包 U T L _ F I L E可以访问目录的范围来实现安全运行。系统允许访问的目录由数据库初始化文件中的 参数U T L _ F I L E _ D I R说明。每个可以访问的目录都在初始化文件中用下面的参数行指定: UTL_FILE_DIR = diretory_name 参数d i r e t o r y _ n a m e所指定的目录在很大程度上与操作系统有关。如果操作系统对大小写敏 感,则d i r e t r o y _ n a m e也区分大小写字母。例如,下面的目录入口在 U n i x系统下是合法的(假设 所指定的目录是存在的): UTL_FILE_DIR=/tmp UTL_FILE_DIR=/home/oracle/output_files 为了能够使用 U T L _ F I L E来访问文件,目录名和文件名都要作为单独的参数传递给函数 F O P E N。该函数将接收的目录名与可访问的文件清单进行比较。如果清单中有该文件,则允许 对给文件进行操作,其具体操作方式还与下一节将介绍的操作系统的安全约束有关。如果函数 F O P E N所接收的文件不可访问,该函数就返回错误。除此之外,可访问目录的子目录一般也是 不可访问的,除非该子目录也显式地出现在可访问目录表中。假设上面的目录是可访问的,表 7 - 4对合法与非法的目录/文件名给予了说明。 注意 即使操作系统对大小写不敏感,在指定目录与可访问目录之间的比较也是大小写 第7章 数据库作业和文件输入输出计计239下载240计计第二部分 非对象功能 下载 敏感的。 如果初始化文件中有下面一行: UTL_FILE_DIR = * 则数据库访问权限将被禁止。该命令将使所有的目录都可由 U T L _ F I L E访问。 表7-4 合法与非法文件的说明 目 录 名 文 件 名 说 明 / t m p m y f i l e . o u t 合法 / h o m e / o r a c l e / o u t p u t _ f i l e s s t u d e n g t s . l i s t 合法 / t m p / 1 9 9 5 j a n u a r y. r e s u l t s 非法,子目录/ t m p / 1 9 9 5不能访问 / h o m e / o r a l c e o u t p u t _ f i l e s / c l a s s e s . l i s t 非法,子目录名不能作为文件名的一部分 / T M P m y f i l e . o u t 非法,大写字母不对 警告 执行关闭数据库访问权限的操作必须十分小心。 O r a c l e并不推荐用户在产生式系 统中使用该选择项,其原因是该选择项的使用可能会限制操作系统的访问权限。除此之 外,也不要使用‘ .’来代表要访问目录的当前目录部分( U n i x使用该符号表示当前目 录),目录的指定一定要使用目录的全名。 2. 操作系统的安全性 由U T L _ F I L E执行的文件操作是作为 O r a c l e用户实现的。(O r a c l e用户是用于运行数据库所需 文件的拥有者,同时它也是组成数据库实例的进程的拥有者。)这样一来,O r a c l e用户就必须具 有操作系统读写所有可访问文件的权限。如果 O r a c l e用户没有访问权限,则任何对该目录的访问 将被操作系统禁止。 由U T L _ F I L E创建的任何文件将归属于 O r a c l e用户所有,这些文件在创建时已具有操作系统 为O r a c l e用户配置的权限。如果有其他用户要在 U T L _ F I L E之外访问这些文件的话,就需要操作 系统变更这些文件的访问权限。 警告 禁止对所有可访问目录的写操作也是一种安全措施。在这种情况下,写操作的权 限只赋予O r a c l e用户。 7.2.2 UTL_FILE引发的异常 如果U T L _ F I L E中的过程或函数遇到了错误,它们将引发异常。这些可能发生的异常在表 7 - 5中给予了说明,这些异常中有八个在 U T L _ F I L E中有定义,其余的两个是予定义的异常 (N O _ D ATA _ F O U N D和VA L U E _ E R R O R)。U T L _ F I L E异常可以按名字或由异常处理程序 O T H E R S来捕捉处理。予定义的异常还可以由 S Q L C O D E的值(1 4 0 3或6 5 0 2)标识。 7.2.3 打开和关闭文件 U T L _ F I L E中的所有操作都使用文件句柄实现。所谓文件句柄就是 P L / S Q L中用来标识文件 的值,它类似于D B M S _ S Q L中使用的游标I D。所有的文件句柄都是U T L _ F I L E . F I L E _ T Y P E类型。文件句柄由F O P E N返回并作为I N类型参数传递给U T L _ F I L E中其他子程序使用。 1. F O P E N F O P N E可以打开用于输入或输出的文件。在任何时候,给定的文件每次只能打开用于输入 或用于输出。打开的文件不能同时用于输入和输出操作。 F O P E N的语法如下: FUNCTION FOPEN( location IN VARCHAR2, filename IN VARCHAR2, open_mode IN VARCHAR2) RETURN FILE_TYPE; 要打开文件的目录路径必须是已经存在的,否则 F O P E N不会创建新的目录。如果打开方式 为‘w’,则F O P E N将覆盖现有的文件。F O P E N的返回值和参数的说明如下所示: 参 数 类 型 说 明 l o c a t i o n VA R C H A R 2 把文件位于的目录路径。如果该目录与可访问目录表中的目录不 匹配,则将引发异常U T L _ F I L E . I N VA L I D _ PAT H。 f i l e n a m e VA R C H A R 2 把要打开的文件名。如果o p e n _ m o d e为‘w’,则将现存文件覆盖。 o p e n _ m o d e VA R C H A R 2 把打开方式。其合法值有:‘r’读文本 ,‘w’写文本,‘a’ 追加 文本。该参数对大小写不敏感。如果指定了‘ a’方式但该文件并 不存在,则就按‘w’方式创建新文件 return value U T L _ F I L E . F I L E _ T Y P E 把后续函数使用的文件句柄 表7-5 UTL_FILE引发的异常 异 常 引 发 条 件 引 发 函 数 I N VA L I D _ PAT H 把非法或不能访问的目录或文件名 把F O P E N I N VA L I D _ M O D E 把非法打开模式 把F O P E N I N VA L I D _ F I L E H A N D L E 把文件句柄指示的文件没有打开 把F C L O S E,G E T _ L I N E , P U T, PUT_LINE,NEW L I N E , P U T F, F F L U S H I N VA L I D _ O P E R AT I O N 把文件不能按要求打开,该异常与 把 G E T _ L I N E , P U T, PUT_LINE,NEW_LINE, 操作系统的权限有关。该异常也可 P U T F, F F L U S H 能在企图对以读方式打开的进行写操 作时,或企图对以写方式打开的文件 进行读操作时引发 I N VA L I D _ M A X L I N E S I Z E 把所指定的最大行数太大或太小。该 把F O P E N 异常只在O r a c l e 8 . 0 . 5及更高的版本下 引发 R E A D _ E R R O R 把在读操作期间出现的操作系统错误 把G E T _ L I N E W E I T E _ E R R O R 把在写操作期间出现的操作系统错误 把P U T, P U T _ L I N E , P U T F, N E W _ L I N E , F F L U S H , F C L O S E , F C L O S E _ A L L I N T E R N A L _ E R R O R 把未说明的内部错误 把所有函数 N O _ D ATA _ F O U N D 把读操作中遇到了文件结束符 把G E T _ L I N E VA L U E _ E R R O R 把输入文件大于 G E T _ L I N E中指定的 把G E T _ L I N E 缓冲区 F O P E N可以引发下列异常之一: • UTL_FILE.INVA L I D _ PAT H 第7章 数据库作业和文件输入输出计计241下载• UTL_FILE.INVA L I D _ M O D E • UTL_FILE.INVA L I D _ O P E R AT I O N • UTL_FILE.INTERNAL_ERROR 2. 使用参数m a x _ l i n e s i z e的F O P E N 在O r a c l e 8 . 0 . 5及更高版本下,U T L _ F I L E提供了F O P E N的另一个重载版本: FUNCTION FOPEN( location IN VARCHAR2, filename IN VARCHAR2, open_mode IN VARCHAR2, max_linesize IN BINARY_INTEGER) RETURN FILE_TYPE; 其中,l o c a t i o n , f i l e n a m e , o p e n _ m o d e参数的意义与F O P E N的第一版相同。参数m a x _ l i n e s i z e用 来说明文件的最大行数,其范围从 1 - 3 2 7 6 7 .如果没有使用该参数,则最大行数为 1 0 2 4。如果该参 数的值小于1或大于3 2 7 6 7,则引发U T L _ F I L E . I N VA L I D _ M A X L I N E S I Z E异常。 3. FCLOSE 当文件的读写操作结束后,要使用 F C L O S E将该文件关闭。关闭文件将释放 U T L _ F I L E用来 操作该文件所占用的资源。 F C L O S E的语法如下: PROCEDURE FCLOSE(file_handle IN OUT FILE_TYPE); F C L O S E的唯一参数是文件句柄 f i l e _ h a n d l e。在关闭该文件之前,任何等待写入文件的未决 变更都将实现。如果在写入过程中出现了错误,则将引发 U T L _ F I L E . W R I T E _ E R R O R异常。如 果文件句柄没有指向合法打开的文件,将引发 U T L _ F I L E . I N VA L I D _ F I L E H A N D L E异常。 4. IS_OPEN 该逻辑值函数在指定的文件处于打开的状态下返回 T R U E,反之返回FA L S E。I S _ O P E N的定 义如下: FUNCTION IS_OPEN(file_handle IN FILE_TYPE) RETURN BOOLEAN; 即使I S _ O P E N返回T R U E,在文件处于打开状态下也存在着操作系统出错的可能。 5. FCLOSE_ALL 该过程将关闭所有打开的文件。该功能可用来清理错误句柄。该过程的定义如下: PROCEDURE FCLOSE_ALL; 该过程没有参数。在文件关闭前,文件的所有未决状态都将被写入文件。由于可能执行写 操作,所以如果在写操作中出现错误时,该过程可能会引发 U T L _ F I L E . W R I T E _ E R R O R异常。 警告 F C L O S E _ A L L将关闭文件并释放U T L _ F I L E占用的资源。然而,该过程并不对文 件做关闭标志。这样一来,在执行 F C L O S E _ A L L后,I S _ O P E N仍将返回T R U E。除此之 外,执行F C L O S E _ A L L之后的任何读或写操作都将失败。 7.2.4 文件输出 有五个过程可用来把数据输出到文件中。它们分别是: P U T、P U T _ L I N E、N E W _ L I N E、 242计计第二部分 非对象功能 下载P U T F和F F L U S H。其中P U T、P U T _ L I N E、N E W _ L I N E的作用非常类似于我们在第 3章中讨论过 的包D B M S _ O U T P U T中的对应参数。输出记录的最大容量是 1 0 2 3个字节(除非使用F O P E N说明 新的容量值)。该记录中包括了一个表示新行的字符。 1. 过程P U T P U T将把指定的字符串输出到指定文件中。该文件在执行 P U T操作前应处于打开状态。 P U T 的定义如下: PROCEDURE PUT( file_handle IN FILE_TYPE, buffer IN VARHCAR2); P U T将不在该文件中追加新行字符,如果需要的话,则必须使用 P U T _ L I N E或N E W _ L I N E在 行中 加入 行结 束符 。如 果在 写入 操作 时出 现了 操作 系统 错误 ,将 引发 U T L _ F I L E . W R I T E _ E R R O R异常。P U T参数的说明如下: 参 数 类 型 说 明 f i l e _ h a n d l e U T L _ F I L E . F I L E _ T Y P E 把由F O P E N返回的文件句柄。如果该句柄是非法的,则将引发 U T L _ F I L E . I N VA L I D _ F I L E H A N D L E异常 b u ff e r VA R C H A R 2 把等待写入文件中的文本字符串。如果没有使用‘ w’或‘a’方式来打 开文件,就引发 U T L _ F I L E . I N VA L I D _ O P E R AT I O N 2. 过程N E W _ L I N E N E W _ L I N E将把一个或多个行结束符写入到指定的文件中。该过程的语法定义如下: PROCEDURE NEW_LINE(file_handle IN FILE_TYPE, lines IN NATURAL:=1); 行结束符与操作系统有关,不同的操作系统可能使用不同的行结束符。如果在写入操作时 出现了操作系统错误,则引发 U T L _ F I L E . W R I T E _ E R R O R异常。参数N E W _ L I N E的说明如下: 参 数 类 型 说 明 f i l e _ h a n d l e U T L _ F I L E . F I L E _ T Y P E 把由F O P E N返回的文件句柄。如果该句柄不合法,将引发 U T L _ F I L E . I N VA L I D _ F I L E H A N D L E异常 l i n e s N AT U R A L 把输出的行结束符个数。其默认值是 1 ,即输出一个新行。如果文件没有 用‘ w’或‘ a’方式打开,系统将引发 U T L _ F I L E . I N VALID_ O P E R AT I O N异常 3. PUT_LINE P U T _ L I N E把指定的字符串输出到指定的文件中,被写入的文件必须以写操作方式打开,在 字符串写入该文件后,系统在行尾加上系统定义的行结束符。 P U T _ L I N E的定义如下: PROCEDURE PUT_LINE(file_handle IN FILE_TYPE, buffer IN VARCHAR2); P U T _ L I N E的参数的含义如下表所示。调用 P U T _ L I N E等价与调用P U T后再调用N E W _ L I N E 来写入行结束符。如果在写入期间发生了操作系统错误,则引发 U T L _ F I L E . W R I T E _ E R R O R异 常。 第7章 数据库作业和文件输入输出计计243下载244计计第二部分 非对象功能 下载 参 数 类 型 说 明 f i l e _ h a n d l e U T L _ F I L E . F I L E _ T Y P E 由由F O P E N返回的文件句柄。如果该句柄不合法,将引发 UTL_FILE. I N VA L I D _ F I L E H A N D L E异常 b u ff e r VA R C H A R 2 由写入到文件中的文本字符串。如果写入文件没有以‘ w’或‘a’模式 打开,则引发U T L _ F I L E . I N VA L I D _ O P E R AT I O N异常 4. 过程P U T F P U T F的功能与P U T类似,但该过程允许对输出字符串进行格式。 P U T F实现了C语言p r i n t f ( ) 的部分功能,其语法也类似于 C语言的p r i n t f ( )语句。P U T F的语法如下: PROCEDURE PUTF( file_handle IN FILE_TYPE, format IN VARCHAR2, arg1 IN VARCHAR2 DEFAULT NULL, arg2 IN VARCHAR2 DEFAULT NULL, arg3 IN VARCHAR2 DEFAULT NULL, arg4 IN VARCHAR2 DEFAULT NULL, arg5 IN VARCHAR2 DEFAULT NULL); 请注意,参数a rg 1 - a rg 5具有默认值,因此这几个参数可以省略。格式字符串 f o r m a t除了带有 正常的文本外,还有两个特殊字符‘ % s’和‘\ n’。在格式字符串中出现‘ % s’的地方都将用一 个上述可选参数替代。格式字符串中出现‘ \ n’的地方都将用一个新的行结束符替代。上述参数 的详细用法在下面案例后面的表中给予说明。对于 P U T和P U T _ L I N E,如果在写入过程中出现了 操作系统错误,则引发U T L _ F I L E . W R I T E _ E R R O R异常。 例如,如果我们运行下面的块: DECLARE v_OutputFile UTL_FILE.FILE_TYPE; v_Name VARCHAR2(10) := 'Scott'; BEGIN v_OutputFile := UTL_FILE.FOPEN(...); UTL_FILE.PUTF(v_OutputFile, 'Hi there!\nMy name is %s, and I am a %s major.\n', v_Name, 'Computer Science'); FCLOSE(v_OutputFile); END; 则输出文件将带有下面内容: Hi There! My name is Scott, and I am a Computer Science major. 参 数 类 型 说 明 f i l e _ h a n d l e U T L _ F I L E . F I L E _ T Y P E 由F O P E N返回的文件句柄。如果该句柄不合法,则引发 U T L _ F I L E . I N VA L I D _ F I L E H A N D L E异常 f o r m a t VA R C H A R 2 由格式字符串包括常规文本和两个特殊的格式字符‘ % s’和‘ \ n’。如果 文件没有以‘ w’或‘ a’的模式打开,则引发 UTL_FILE. INVALID _ O P E R AT I O N异常 a rg 1 - a g r 5 VA R C H A R 2 由五个可选的参数。每个参数将被对应‘ % s’的格式字符代替。如果 ‘% s’字符多于参数的话,就使用空字符串来代替格式字符5. 过程F F L U S H 使用P U T,P U T _ L I N E , P U T F,或N E W _ L I N E输出的数据通常是存储在缓冲区中的。当该缓 冲区装满后,该缓冲区中的字符将被送往输出文件。 F F L U S H强行把缓冲区中的字符立即写入指 定文件。需要提醒的是, F F L U S H只是把缓冲区中以 N E W _ L I N E字符结尾的行写入文件中。任 何最后执行P U T操作放入缓冲区的字符都将在缓冲区中保留。该过程的语法是: PROCEDURE FFLUSH(file_handle IN FILE_TYPE); F F L U S H可以引发下列异常: • UTL_FILE.INVA L I D _ F I L E H A N D L E • UTL_FILE.INVA L I D _ O P E R AT I O N • UTL_FILE.WRITE_ERROR 7.2.5 文件输入 G E T _ L I N E是用来从文件中执行读入操作的,该过程每次从指定的文件中读入一行文本并到 将该文本串送入缓冲区,新行字符不包括在返回字符串中。下面是该过程的定义: PROCEDURE GET_LINE(file_handle IN FILE_TYPE, buffer OUT VARCHAR2); 当从指定文件中读入最后一行时,异常 N O _ D ATA _ F O U N D将会引发。如果该行不适宜进入 作为实参提供的缓冲区,就将引发异常 VA L U E _ E R R O R。读入空行将返回一个空字符串 N U L L。 如果在读入期间,操作系统系统出现了错误,则引发 U T L _ F I L E . R E A D _ E R R O R异常。输入行的 最大长度是1 0 2 2字节。(除非使用F O P E N的参数m a x _ l i n e s i z e指定最大行数。)下面是这些参数的 说明: 参 数 类 型 说 明 f i l e _ h a n d l e U T L _ F I L E . F I L E _ T Y P E F O P E N返回的文件句柄。如果该句柄不合法,则将引发UTL_FILE. I N VA L I D _ F I L E H A N D L E异常 b u ff e r VA R C H A R 2 将一行写入的缓冲区。如果文件没有以读‘r’的模式打开,则引发 U T L _ F I L E . I N VA L I D _ O P E R AT I O N异常 7.2.6 文件操作案例 本节将分析三个使用U T L _ F I L E的程序案例。第一个案例是我们在第 3章介绍的包D E B U G的 新版本。第二个案例从一个文件中将学生信息读入并装入到表中。第三个程序是打印成绩单。 1. 包D E B U G U T L _ F I L E的第一种用法是实现调试程序包。由于 D B M S _ O U T P U T只能在块运行结束后才 能打印执行结果(我们在第 3章讨论过该问题),而U T L _ F I L E可以提供更快捷的输出。下面是用 U T L _ F I L E实现的D E B U G包: 节选自在线代码Debug.sql CREATE OR REPLACE PACKAGE Debug AS /* Global variables to hold the name of the debugging file and 第7章 数据库作业和文件输入输出计计245下载246计计第二部分 非对象功能 下载 directory. */ v_DebugDir VARCHAR2(50) := '/tmp'; v_DebugFile VARCHAR2(20) := 'debug.out'; /* Call Debug to output a line consisting of: p_Description: p_Value to the debugging file. */ PROCEDURE Debug(p_Description IN VARCHAR2, p_Value IN VARCHAR2); /* Closes the debugging file first, then calls FileOpen to set the packaged variables and open the file with the new parameters. */ PROCEDURE Reset(p_NewFile IN VARCHAR2 := v_DebugFile, p_NewDir IN VARCHAR2 := v_DebugDir); /* Sets the packaged variables to p_NewFile and p_NewDir, and opens the debugging file. */ PROCEDURE FileOpen(p_NewFile IN VARCHAR2 := v_DebugFile, p_NewDir IN VARCHAR2 := v_DebugDir); /* Closes the debugging file. */ PROCEDURE FileClose; END Debug; CREATE OR REPLACE PACKAGE BODY Debug AS v_DebugHandle UTL_FILE.FILE_TYPE; PROCEDURE Debug(p_Description IN VARCHAR2, p_Value IN VARCHAR2) IS BEGIN IF NOT UTL_FILE.IS_OPEN(v_DebugHandle) THEN FileOpen; END IF; /* Output the info, and flush the file. */ UTL_FILE.PUTF(v_DebugHandle, '%s: %s\n', p_Description, p_Value); UTL_FILE.FFLUSH(v_DebugHandle); EXCEPTION WHEN UTL_FILE.INVALID_OPERATION THEN RAISE_APPLICATION_ERROR(-20102, 'Debug: Invalid Operation'); WHEN UTL_FILE.INVALID_FILEHANDLE THEN RAISE_APPLICATION_ERROR(-20103, 'Debug: Invalid File Handle'); WHEN UTL_FILE.WRITE_ERROR THEN RAISE_APPLICATION_ERROR(-20104, 'Debug: Write Error'); WHEN UTL_FILE.INTERNAL_ERROR THEN第7章 数据库作业和文件输入输出计计247下载 RAISE_APPLICATION_ERROR(-20104, 'Debug: Internal Error'); END Debug; PROCEDURE Reset(p_NewFile IN VARCHAR2 := v_DebugFile, p_NewDir IN VARCHAR2 := v_DebugDir) IS BEGIN /* Make sure the file is closed first. */ IF UTL_FILE.IS_OPEN(v_DebugHandle) THEN FileClose; END IF; FileOpen(p_NewFile,p_NewDir); END Reset; PROCEDURE FileOpen(p_NewFile IN VARCHAR2 := v_DebugFile, p_NewDir IN VARCHAR2 := v_DebugDir) IS BEGIN /* Open the file for writing. */ v_DebugHandle := UTL_FILE.FOPEN(p_NewDir, p_NewFile, 'w'); /* Set the packaged variables to the values just passed in. */ v_DebugFile := p_NewFile; v_DebugDir := p_NewDir; EXCEPTION WHEN UTL_FILE.INVALID_PATH THEN RAISE_APPLICATION_ERROR(-20100, 'Open: Invalid Path'); WHEN UTL_FILE.INVALID_MODE THEN RAISE_APPLICATION_ERROR(-20101, 'Open: Invalid Mode'); WHEN UTL_FILE.INVALID_OPERATION THEN RAISE_APPLICATION_ERROR(-20101, 'Open: Invalid Operation'); WHEN UTL_FILE.INTERNAL_ERROR THEN RAISE_APPLICATION_ERROR(-20101, 'Open: Internal Error'); END FileOpen; PROCEDURE FileClose IS BEGIN UTL_FILE.FCLOSE(v_DebugHandle); EXCEPTION WHEN UTL_FILE.INVALID_FILEHANDLE THEN RAISE_APPLICATION_ERROR(-20300, 'Close: Invalid File Handle'); WHEN UTL_FILE.WRITE_ERROR THEN RAISE_APPLICATION_ERROR(-20301, 'Close: Write Error'); WHEN UTL_FILE.INTERNAL_ERROR THEN RAISE_APPLICATION_ERROR(-20302,'Close: Internal Error'); END FileClose; END Debug; 使用该D E B U G包的方法是很直观的。该包的变量 v _ D e b u g D i r和v _ D e b u g F i l e指定了输出文 件的位置和名称。如果我们先调用 D e b u g . D e b u g,则该文件打开时将带有上述的值。为了修改这 些值,我们可以调用 D e b u g . F i l e O p e n来复位该包的变量并重新打开该文件。或者调用 D e b u g . R e s e t首先将该文件关闭,然后再执行 F i l e O p e n的操作。D e b u g . F i l e C l o s e将在操作结束时 关闭该文件。例如,如果我们执行下面的块: 节选自在线代码CallDebug.sql BEGIN Debug.Debug('Scott', 'First call'); Debug.Reset('debug2.out', '/tmp'); Debug.Debug('Scott', 'Second call'); Debug.Debug('Scott', 'Third call'); Debug.FileClose; END; 接着文件/ t m p / d e b u g . o u t中将包括下面的内容: Scott: First call 文件/ t m p / d e b u g 2 . o u t将包括下面的内容: Scott:Second call Scott:Third call 提示 请注意程序中各种例程的异常处理程序。这些异常处理程序识别引发的错误类型 以及引发该错误的过程。这种方法是值得我们在使用 U T L _ F I L E时借鉴。 2. 加载学生数据程序 过程L o a d S t u d e n t s将根据其接收的文件内容来对表进行插入操作。该文件是用逗号分界的, 也就是说,该文件的每一行都是一个记录,该行中的逗号用来分割字段。这是文本文件的常用 格式。下面是该过程的代码: 节选自在线代码LoadStudents.sql CREATE OR REPLACE PROCEDURE LoadStudents ( /* Loads the students table by reading a comma-delimited file. The file should have lines that look like first_name,last_name,major The student ID is generated from student_sequence. The total number of rows inserted is returned by p_TotalInserted. */ p_FileDir IN VARCHAR2, p_FileName IN VARCHAR2, p_TotalInserted IN OUT NUMBER) AS 248计计第二部分 非对象功能 下载第7章 数据库作业和文件输入输出计计249下载 v_FileHandle UTL_FILE.FILE_TYPE; v_NewLine VARCHAR2(100); -- Input line v_FirstName students.first_name%TYPE; v_LastName students.last_name%TYPE; v_Major students.major%TYPE; /* Positions of commas within input line. */ v_FirstComma NUMBER; v_SecondComma NUMBER; BEGIN -- Open the specified file for reading. v_FileHandle := UTL_FILE.FOPEN(p_FileDir, p_FileName, 'r'); -- Initialize the output number of students. p_TotalInserted := 0; -- Loop over the file, reading in each line. GET_LINE will -- raise NO_DATA_FOUND when it is done, so we use that as the -- exit condition for the loop. LOOP BEGIN UTL_FILE.GET_LINE(v_FileHandle, v_NewLine); EXCEPTION WHEN NO_DATA_FOUND THEN EXIT; END; -- Each field in the input record is delimited by commas. We -- need to find the locations of the two commas in the line -- and use these locations to get the fields from v_NewLine. -- Use INSTR to find the locations of the commas. v_FirstComma := INSTR(v_NewLine, ',', 1, 1); v_SecondComma := INSTR(v_NewLine, ',', 1, 2); -- Now we can use SUBSTR to extract the fields. v_FirstName := SUBSTR(v_NewLine, 1, v_FirstComma - 1); v_LastName := SUBSTR(v_NewLine, v_FirstComma + 1, v_SecondComma - v_FirstComma - 1); v_Major := SUBSTR(v_NewLine, v_SecondComma + 1); -- Insert the new record into students. INSERT INTO students (ID, first_name, last_name, major) VALUES (student_sequence.nextval, v_FirstName, 过程L o a d S t u d e n t s使用的一个输入文件的内容如下: 节选自在线代码students.sql v_LastName, v_Major); p_TotalInserted := p_TotalInserted + 1;END LOOP; -- Close the file. UTL_FILE.FCLOSE(v_FileHandle); COMMIT; EXCEPTION -- Handle the UTL_FILE exceptions meaningfully, and make sure -- that the file is properly closed. WHEN UTL_FILE.INVALID_OPERATION THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20051, 'LoadStudents: Invalid Operation'); WHEN UTL_FILE.INVALID_FILEHANDLE THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20052, 'LoadStudents: Invalid File Handle'); WHEN UTL_FILE.READ_ERROR THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20053, 'LoadStudents: Read Error'); WHEN UTL_FILE.INVALID_PATH THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20054, 'LoadStudents: Invalid Path'); WHEN UTL_FILE.INVALID_MODE THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20055, 'LoadStudents: Invalid Mode'); WHEN UTL_FILE.INTERNAL_ERROR THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20056, 'LoadStudents: Internal Error'); WHEN VALUE_ERROR THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20057, 'LoadStudents: Value Error'); WHEN OTHERS THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE; END LoadStudents; 过程L o a d S t u d e n t s使用的一个输入文件的内容如下 Scott,Smith,Computer Science Margaret,Mason,History Joanne,Junebug,Computer Science Manish,Murgratroid,Economics Patrick,Poll,History Timothy,Taller,History 250计计第二部分 非对象功能 下载第7章 数据库作业和文件输入输出计计251下载 Barbara,Blues,Economics David,Dinsmore,Music Ester,Elegant,Nutrition Rose,Riznit,Music Rita,Razmataz,Nutrition Shay,Shariatpanahy,Computer Science 注意 过程L o a d S t u d e n t s使用s t u d e n t _ s e q u e n c e序列来确认学生的 I D。如果该序列在表 s t u d e n t s被载入后进行了重建的话,该过程将可能返回一个已经在表 s t u d e n t s中的值。在 这种情况下,表s t u d e n t s的主约束键将会非法。 3. 打印成绩单程序 该案例介绍了如何使用U T L _ F I L E来打印学生的成绩单。下面是过程 C a l c u l a t e G PA的代码: 节选自在线代码CalculateGPA.sql CREATE OR REPLACE PROCEDURE CalculateGPA ( /* Returns the grade point average for the student identified by p_StudentID in p_GPA. */ p_StudentID IN students.ID%TYPE, p_GPA OUT NUMBER) AS CURSOR c_ClassDetails IS SELECT classes.num_credits, rs.grade FROM classes, registered_students rs WHERE classes.department = rs.department AND classes.course = rs.course AND rs.student_id = p_StudentID; v_NumericGrade NUMBER; v_TotalCredits NUMBER := 0; v_TotalGrade NUMBER := 0; BEGIN FOR v_ClassRecord in c_ClassDetails LOOP -- Determine the numeric value for the grade. SELECT DECODE(v_ClassRecord.grade, 'A', 4, 'B', 3, 'C', 2, 'D', 1, 'E', 0) INTO v_NumericGrade FROM dual; v_TotalCredits := v_TotalCredits + v_ClassRecord.num_credits; v_TotalGrade := v_TotalGrade + (v_ClassRecord.num_credits * v_NumericGrade); END LOOP; p_GPA := v_TotalGrade / v_TotalCredits;END CalculateGPA; P r i n t Transcript 用以下代码创建 节选自在线代码printTranscript.sql CREATE OR REPLACE PROCEDURE PrintTranscript ( /* Outputs a transcript to the indicated file for the indicated student. The transcript will consist of the classes for which the student is currently registered and the grade received for each class. At the end of the transcript, the student's GPA is output. */ p_StudentID IN students.ID%TYPE, p_FileDir IN VARCHAR2, p_FileName IN VARCHAR2) AS v_StudentGPA NUMBER; v_StudentRecord students%ROWTYPE; v_FileHandle UTL_FILE.FILE_TYPE; v_NumCredits NUMBER; CURSOR c_CurrentClasses IS SELECT * FROM registered_students WHERE student_id = p_StudentID; BEGIN -- Open the output file in append mode. v_FileHandle := UTL_FILE.FOPEN(p_FileDir, p_FileName, 'a'); SELECT * INTO v_StudentRecord FROM students WHERE ID = p_StudentID; -- Output header information. This consists of the current -- date and time, and information about this student. UTL_FILE.PUTF(v_FileHandle, 'Student ID: %s\n', v_StudentRecord.ID); UTL_FILE.PUTF(v_FileHandle, 'Student Name: %s %s\n', v_StudentRecord.first_name, v_StudentRecord.last_name); UTL_FILE.PUTF(v_FileHandle, 'Major: %s\n', v_StudentRecord.major); UTL_FILE.PUTF(v_FileHandle, 'Transcript Printed on: %s\n\n\n', TO_CHAR(SYSDATE, 'Mon DD,YYYY HH24:MI:SS')); UTL_FILE.PUT_LINE(v_FileHandle, 'Class Credits Grade'); UTL_FILE.PUT_LINE(v_FileHandle, '------- ------- -----'); FOR v_ClassesRecord in c_CurrentClasses LOOP 252计计第二部分 非对象功能 下载第7章 数据库作业和文件输入输出计计253下载 -- Determine the number of credits for this class. SELECT num_credits INTO v_NumCredits FROM classes WHERE course = v_ClassesRecord.course AND department = v_ClassesRecord.department; -- Output the info for this class. UTL_FILE.PUTF(v_FileHandle, '%s %s %s\n', RPAD(v_ClassesRecord.department || ' ' || v_ClassesRecord.course, 7), LPAD(v_NumCredits, 7), LPAD(v_ClassesRecord.grade, 5)); END LOOP; -- Determine the GPA. CalculateGPA(p_StudentID, v_StudentGPA); -- Output the GPA. UTL_FILE.PUTF(v_FileHandle, '\n\nCurrent GPA: %s\n', TO_CHAR(v_StudentGPA, '9.99')); -- Close the file. UTL_FILE.FCLOSE(v_FileHandle); EXCEPTION -- Handle the UTL_FILE exceptions meaningfully, and make sure -- that the file is properly closed. WHEN UTL_FILE.INVALID_OPERATION THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20061, 'PrintTranscript: Invalid Operation'); WHEN UTL_FILE.INVALID_FILEHANDLE THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20062, 'PrintTranscript: Invalid File Handle'); WHEN UTL_FILE.WRITE_ERROR THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20063, 'PrintTranscript: Write Error'); WHEN UTL_FILE.INVALID_MODE THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20064, 'PrintTranscript: Invalid Mode'); WHEN UTL_FILE.INTERNAL_ERROR THEN UTL_FILE.FCLOSE(v_FileHandle); RAISE_APPLICATION_ERROR(-20065, 'PrintTranscript: Internal Error'); END PrintTranscript;假设我们给定表r e g i s t e r e d _ s t u d e n t s(由过程r e l Ta b l e s . s q l创建的)的内容如下: SQL> select * from registered_students; STUDENT_ID DEP COURSE G ---------- --- --------- - 10000 CS 102 A 10002 CS 102 B 10003 CS 102 C 10000 HIS 101 A 10001 HIS 101 B 10002 HIS 101 B 10003 HIS 101 A 10004 HIS 101 C 10005 HIS 101 C 10006 HIS 101 E 10007 HIS 101 B 10008 HIS 101 A 10009 HIS 101 D 10010 HIS 101 A 10008 NUT 307 A 10010 NUT 307 A 10009 MUS 410 B 10006 MUS 410 E 10011 MUS 410 B 10000 MUS 410 B 20 rows selected. 如果我们调用过程 P r i n t Tr a n s c r i p t来打印学生I D号为1 0 0 0 0和1 0 0 0 9的成绩单,我们将得到下 面两个输出文件: Student ID: 10000 Student Name: Scott Smith Major: Computer Science Transcript Printed on: Apr 26,1999 22:24:07 Class Credits Grade ------- ------- ----- CS 102 4 A HIS 101 4 A MUS 410 3 B Current GPA: 3.73 Student ID: 10009 Student Name: Rose Riznit Major: Music Transcript Printed on: Apr 26,1999 22:24:31 Class Credits Grade ------- ------- ----- 254计计第二部分 非对象功能 下载HIS 101 4 D MUS 410 3 B Current GPA: 1.86 7.3 小结 我们在本章分析了两个实用程序包:一个是 D B M S _ J O B,另一个是U T L _ F I L E。数据库作业 允许过程由数据库在预先定义的时间自动启动运行。包 U T L _ F I L E在确保服务器安全的前提下为 P L / S Q L语言增加了文件输入输出的功能。这些实用程序为 P L / S Q L语言提供了新的功能。 第7章 数据库作业和文件输入输出计计255下载
还剩253页未读

继续阅读

pdf贡献者

it_boy

贡献于2013-03-25

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf