PL/SQL 子程序是可以重复调用的命名PL/SQL 块。如果子程序具有参数,则每次调用它们的值可能不同。
子程序可以是过程,也可以是函数。通常,使用过程来执行操作,并使用函数来计算和返回值。
子程序支持可靠的开发和维护, 具有以下功能的可重用代码:
模块化
子程序使您可以将程序分解为可管理的、定义良好的模块。
更简单的应用程序设计
在设计应用程序时,您可以推迟子程序的实现细节,直到您测试了主程序,然后逐步细化它们。 (要定义没有实现细节的子程序,请使用 NULL 语句,如:ref:示例 5-30 所示。)
可维护性
您可以更改子程序的实现细节而不更改其调用程序。
可包装性
子程序可以分组到包中,其优点在“使用包的原因”中进行了说明。
可重用性
在许多不同的环境中,任何数量的应用程序都可以使用相同的包子程序或独立子程序。
更好的性能
每个子程序都以可执行的形式编译和存储,可以重复调用。因为存储的子程序在数据库服务器中运行,所以通过网络进行的一次调用就可以启动一项大型作业。这种工作分工减少了网络流量并缩短了响应时间。存储的子程序被缓存并在用户之间共享,从而降低了内存需求和调用开销。
子程序是其他可维护性功能的重要组成部分,例如包(在 PL/SQL 包`中解释)和抽象数据类型(在“:ref:`抽象数据类型”中解释)。
您可以在 PL/SQL 块(可以是另一个子程序)内、包内或模式级别创建子程序。
在 PL/SQL 块内创建的子程序是嵌套子程序。您可以同时声明和定义它,也可以先声明它,然后在同一块中定义它。只有嵌套在独立子程序或包子程序中的嵌套子程序才会存储在数据库中。
在包内创建的子程序是包子程序。您在包规范中声明它并在包体中定义它。它存储在数据库中,直到您删除包。
在模式级别创建的子程序是一个独立的子程序。您可以使用 CREATE FUNCTION 或 CREATE PROCEDURE 语句创建它。它存储在数据库中,直到您使用 DROP FUNCTION 或 DROP PROCEDURE 语句删除它。
存储子程序可以是包子程序,也可以是独立子程序。存储子程序受 AUTHID 子句的影响,这些子句可以出现在 CREATE FUNCTION、CREATE PROCEDURE 和 CREATE PACKAGE 语句中。 AUTHID 子句影响子程序在运行时发出的 SQL 语句的名称解析和权限检查。
子程序调用具有以下形式:
subprogram_name [ ( [ parameter [, parameter]... ] ) ]
如果子程序没有参数,或为每个参数指定一个默认值,则可以省略该参数列表或指定一个空的参数列表。
一个过程调用是一个PL/SQL语句。例如:
raise_salary(Stuloyee_id, amount);
一个函数调用是一个表达式。例如:
new_salary := get_salary(Stuloyee_id); IF salary_ok(new_salary, new_title) THEN ...
另请参阅
":ref:`子程序参数`获取有关在子程序调用中指定参数的更多信息"
每个子程序属性在子程序声明中只能出现一次。 属性可以按任何顺序出现。 属性出现在子程序标题中的 IS 或 AS 关键字之前。 属性不能出现在嵌套的子程序中。
子程序以子程序标题开始,该标题指定其名称和(可选)其参数列表。
像匿名块一样,子程序具有以下部分: - 声明部分(可选)
这部分声明和定义局部类型、游标、常量、变量、异常和嵌套子程序。 当子程序完成执行时,这些项目将不复存在。
可执行部分(必填)
这部分包含一个或多个用于赋值、控制执行和操作数据的语句。 (在应用程序设计过程的早期,这部分可能只包含一个 NULL 语句,如:ref:示例 5-30 所示。)
异常处理部分(可选)
这部分包含处理运行时错误的代码。
另请参阅
示例 9-1 声明、定义和调用简单的 PL/SQL 过程。
在这个例子中,一个匿名块同时声明和定义一个过程并调用它三次。 第三次调用引发过程的异常处理部分处理的异常。
\set SQLTERM / DECLARE str varchar(30) = ''; -- Declare and define procedure PROCEDURE add_str ( str1 text ) IS error_message VARCHAR2(30) := 'str is too long.'; BEGIN str := str || '_' || str1; EXCEPTION WHEN VALUE_ERROR THEN RAISE NOTICE '%', error_message; END add_str; BEGIN add_str('add_string1'); -- invocation RAISE NOTICE 'str is %', str; add_str('add_string2'); -- invocation RAISE NOTICE 'str is %', str; add_str('add_string3'); -- invocation END; /
结果:
NOTICE: str is _add_string1 NOTICE: str is _add_string1_add_string2 ERROR: value too long for type character varying(30) CONTEXT: PL/SQL function add_str line 12 at assignment SQL statement "CALL add_str('add_string3')" PL/SQL function inline_code_block line 24 at CALL
函数与过程具有相同的结构,除了: - 函数头部必须包含 RETURN 子句,该子句指定函数返回值的数据类型。(过程头部不能有 RETURN 子句。) - 在函数的可执行部分,每个执行路径都必须指向一个 RETURN 语句。 否则,PL/SQL 编译器会发出编译时警告。 (在过程中,RETURN语句是可选的,不推荐使用,详情见 "RETURN 语句"。) - 函数声明可以包括以下选项:
选项 | 描述 | 表格 |
---|---|---|
DETERMINISTIC | option | 帮助优化器避免冗余的函数调用。 |
PARALLEL_ENABLE | option | 启用并行执行函数,使其在并行DML评估的并发会话中安全使用 |
PIPELINED | option | 使一个表函数流水线化,以便用作行源。 |
RESULT_CACHE | option | 存储函数结果存储在PL/SQL函数结果缓存中。 |
另请参阅
"函数的声明和定义"
"PLSQL 函数结果缓存"
示例 9-2 声明、定义和调用一个简单的PL/SQL函数
在本示例中,一个匿名块同时声明和定义一个函数并调用它。
\set SQLTERM / DECLARE -- Declare and define function FUNCTION square (num NUMBER) RETURN NUMBER AS num_squared NUMBER; BEGIN num_squared := num * num; RETURN num_squared; END; BEGIN RAISE NOTICE '%', square(90); END; /
结果:
NOTICE: 8100
RETURN 语句立即结束子程序或包含它的匿名块的执行。 一个子程序或匿名块可以包含多个 RETURN 语句。
另请参阅
"RETURN 语句"用于 RETURN 语句的用法
9.5.2.1. 函数中的RETURN语句
在函数中,每个执行路径都必须指向一个 RETURN 语句,并且每个 RETURN 语句都必须指定一个表达式。 RETURN 语句将表达式的值分配给函数标识符并将控制权返回给调用者,调用者在调用后立即恢复执行。
注意
在pipelined 表函数中,返回语句不需要指定表达式。有关pipelined 表函数的各个部分的信息,请参见: "创建 pipelined 表函数".
在:ref:示例 9-3 中,匿名块调用相同的函数两次。第一次,RETURN 语句将控制权返回到调用语句的内部。 第二次,RETURN 语句将控制权返回给调用语句之后的语句。
在:ref:示例 9-4 中,函数有多个 RETURN 语句,但如果参数不是 0 或 1,则没有执行路径导致 RETURN 语句。
:ref:`示例9-5`与:ref:`示例9-4`类似,除了添加了ELSE子句之外。
示例9-3函数中返回语句后执行恢复
\set SQLTERM / DECLARE x INTEGER; FUNCTION f (n INTEGER) RETURN INTEGER AS BEGIN RETURN (n*n); END; BEGIN RAISE NOTICE 'f(2) = %', f(2); x := f(2); RAISE NOTICE 'x = %', x; END; /
结果:
NOTICE: f(2) = 4 NOTICE: x = 4
示例9-4 并非每个执行路径都指向RETURN语句的函数
\set SQLTERM / CREATE OR REPLACE FUNCTION f (x INTEGER) RETURN INTEGER AUTHID DEFINER IS BEGIN IF x = 0 THEN RETURN x; ELSIF x > 1 THEN RETURN 10; END IF; END; /
示例 9-5 每个路径都指向RETURN语句的函数
\set SQLTERM / CREATE OR REPLACE FUNCTION f (x INTEGER) RETURN INTEGER AUTHID DEFINER IS BEGIN IF x = 0 THEN RETURN 1; ELSIF x > 1 THEN RETURN 10; ELSE RETURN x*x; END IF; END; / BEGIN FOR i IN 0 .. 3 LOOP RAISE NOTICE 'f(%) = %', i, f(i); END LOOP; END; /
结果:
NOTICE: f(0) = 1 NOTICE: f(1) = 1 NOTICE: f(2) = 10 NOTICE: f(3) = 10
9.5.2.2. 过程中的RETURN语句
在一个过程中,RETURN 语句将控制权返回给调用者,在调用后立即恢复执行。 RETURN 语句不能指定表达式。
在示例 9-6 中,RETURN 语句将控制权返回给调用语句之后的语句。
示例 9-6 过程中 RETURN 语句后恢复执行
\set SQLTERM / DECLARE PROCEDURE p IS BEGIN RAISE NOTICE 'In procedure p'; RETURN; RAISE NOTICE 'Can not be executed.'; END; BEGIN p; RAISE NOTICE 'Out of procedure p.'; END; /
Result:
NOTICE: In procedure p NOTICE: Out of procedure p.
9.5.2.3. 匿名块中的RETURN语句
在匿名块中,RETURN 语句退出它自己的块和所有封闭块。 RETURN 语句不能指定表达式。
在示例 9-7 中,RETURN 语句同时退出内部块和外部块。
示例 9-7 在匿名块中的 RETURN 语句后恢复执行
\set SQLTERM / BEGIN BEGIN RAISE NOTICE'inside block.'; RETURN; RAISE NOTICE 'Can not be executed.'; END; RAISE NOTICE 'Outer block. Can not be executed.'; END; /
结果:
NOTICE: inside block.
如果同一 PL/SQL 块中的嵌套子程序相互调用,则需要前向声明,因为必须先声明子程序才能调用它。
前向声明声明了一个嵌套子程序,但没有定义它。 您必须稍后在同一块中定义它。 前向声明和定义必须具有相同的子程序头。
在示例 9-8 中,匿名块创建了两个相互调用的过程。
示例 9-8 嵌套子程序相互调用
\set SQLTERM / DECLARE -- 声明proc1,前向声明 PROCEDURE proc1(num1 NUMBER); -- 声明和定义proc2,其中调用了proc1 PROCEDURE proc2(num2 NUMBER) IS BEGIN proc1(number2); END; -- 定义proc1 PROCEDURE proc1(num1 NUMBER) IS BEGIN proc2 (number1); END; BEGIN NULL; END; /
如果一个子程序有参数,则它们的值可以因每次调用而有所不同。
如果您希望子程序具有参数,请在子程序头中声明形式参数。在每个形式参数声明中,指定参数的名称和数据类型,以及(可选)其模式和默认值。 在子程序的执行部分,通过名称引用形式参数。
调用子程序时,指定实际参数,其值将分配给形式参数。 对应的实参和形参必须具有兼容的数据类型。
提示
为了避免混淆,请对正式参数和实际参数使用不同的名称。
注意
实际参数(包括形式参数的默认值)可以按任何顺序计算。 如果一个程序确定了计算的顺序,那么在程序这样做的时候,它的行为是未定义的。
在示例 9-9 中,该过程具有形式参数 id 和 add_num。 在第一次过程调用中,对应的实际参数是score和add_num,其值分别为120和100。 在第二次过程调用中,实际参数是Stu_num和merit+bonus,其值分别为120和150。
另请参阅
"形式参数声明" 了解形式参数声明的语法和语义
"function_call" 了解函数调用的语法和语义
"procedure_call" 了解存储过程调用的语法和语义
示例 9-9形式化参数和实际参数
DROP TABLE IF EXISTS student; DROP TABLE IF EXISTS stu_temp; CREATE TABLE student (id int PRIMARY KEY, name text, score number); insert into student values (1, 'xx', 99); CREATE TABLE stu_temp AS SELECT * FROM student; \set SQLTERM / DECLARE score NUMBER(6) := 80; add_num1 NUMBER(6) := 10; add_num2 NUMBER(4) := 5; PROCEDURE raise_score ( id INTEGER, -- 形式参数 score NUMBER, -- 形式参数 add_num NUMBER -- 形式参数 ) IS BEGIN UPDATE stu_temp SET score = score + add_num -- 引用形式参数 WHERE id = id; -- 引用形式参数 END raise_score; BEGIN raise_score(1, score, add_num1); -- 实际参数 raise_score(1, score, add_num1 + add_num2); -- 实际参数 END; / \set SQLTERM ; select * from stu_temp;
结果:
id | name | score ----+------+------- 1 | xx | 124 (1 row)
9.7.1.1. 受约束子类型的形式化参数
如果形参的数据类型是受约束的子类型,则: - 如果子类型具有 NOT NULL 约束,则实际参数会继承它。 - 如果子类型具有基类型 VARCHAR2,则实际参数不会继承子类型的大小。 - 如果子类型具有数字基类型,则实际参数继承子类型的范围,但不继承精度或小数位数。
示例 9-10 显示一个实际的子程序参数继承了 NOT NULL 约束,但没有继承 VARCHAR2 子类型的大小。
正如 PL/SQL 预定义数据类型所示,PL/SQL有许多预定义数据类型,它们是其他数据类型的受限子类型。 例如,INTEGER 是 NUMBER 的受约束子类型:
SUBTYPE INTEGER IS NUMBER(38,0);
在:ref:示例 9-10 中,该函数同时具有 INTEGER 形式参数和 INTEGER 返回类型。匿名块使用不是整数的实际参数调用函数。 因为实参继承了INTEGER的范围而不是精度和小数位数,并且实参在INTEGER范围内,所以调用成功。 出于同样的原因,RETURN 语句成功返回了非整数值。 在:ref:示例 9-11 中,函数在返回之前将其形式参数隐式转换为受约束的子类型 INTEGER。
另请参阅
"约束子类型" 获取有关约束子类型的一般信息
示例 9-10实际参数仅从子类型继承非NULL
\set SQLTERM / DECLARE SUBTYPE varchr7 IS VARCHAR2(7) NOT NULL; i varchr7 := 'ABCD123'; PROCEDURE p (x varchr7) IS BEGIN RAISE NOTICE 'x'; END; BEGIN p('ABCD1234'); -- 成功,长度没有被继承 p(NULL); -- 失败,NOT NULL约束被继承 END; /
结果:
NOTICE: x ERROR: null value cannot be assigned to variable "x" declared NOT NULL CONTEXT: PL/SQL function p while storing call arguments into local variables SQL statement "CALL p(NULL)" PL/SQL function inline_code_block line 12 at CALL
示例 9-11函数隐式地将形式参数转换为约束子类型
\set SQLTERM / DECLARE FUNCTION f1 (p NUMBER) RETURN NUMBER IS q INTEGER := p; BEGIN RAISE NOTICE 'p = %', q; RETURN q; END f1; BEGIN RAISE NOTICE 'f1(p) = %', f1(0.51); END; /
结果:
NOTICE: p = 1 NOTICE: f1(p) = 1
PL/SQL编译器有两种方式,可以将实际参数传递给子程序: - 引用传递
编译器向子程序传递一个指向实际参数的指针。 实参和形参指的是同一个内存位置。
值传递
编译器将实参的值赋给相应的形参。 实参和形参指的是不同的内存位置。 如有必要,编译器会将实参的数据类型隐式转换为形参的数据类型。
在示例 9-12 中,该过程有一个参数 ,它是按值传递的。 匿名块调用三次,避免了两次隐式转换。
编译器传递特定实际参数的方法取决于其模式,如“子程序参数模式”中所述。
示例 9-12 避免实际参数的隐式转换
\set SQLTERM / CREATE OR REPLACE PROCEDURE p ( num NUMBER ) AUTHID DEFINER IS BEGIN NULL; END; / DECLARE a NUMBER := 1; b VARCHAR2(1) := '1'; BEGIN p(a); -- 不需要转换 p(b); -- 隐式转换 p(TO_NUMBER(b)); -- 显式转换 END; /
一个形式化参数的模式决定了它的行为。
下表总结并比较了子程序参数模式的特征。
参数模式 | 是否默认 | 角色 |
---|---|---|
In | 默认模式 | 将值传递给子程序。 |
out | 必须指定。 | 向调用者返回一个值。 |
In out | 必须指定。 | 将初始值传递给子程序并将更新的值返回给调用者。 |
参数模式 | 形式参数 | 实际参数 | 通过引用传递? |
---|---|---|---|
in | 形参就像一个常数:当子程序开始时,它的值要么是它的实参,要么是默认值,子程序不能改变这个值。 | 实际参数可以是常量、初始化变量、文字或表达式。 | 实际参数通过引用传递。 |
out | 形参被初始化为其类型的默认值。该类型的默认值是NULL具有非默认值的记录类型除外NULL(参见示例 9-16)。当子程序开始时,形式参数有它的初始值,而不管它的实际参数的值。建议子程序为形参赋值。 | 如果形参类型的默认值为NULL,则实参必须是数据类型未定义为 的变量NOT NULL。 | 实参传值 |
In out | 形参就像一个初始化变量:当子程序开始时,它的值就是它的实参。建议子程序更新其值。 | 实际参数必须是变量(通常是字符串缓冲区或数值累加器)。 | 实参按值传递(双向); |
提示
不要将 OUT 和 IN OUT 用于函数参数。 理想情况下,函数接受零个或多个参数并返回单个值。 带有 IN OUT 参数的函数会返回多个值并具有副作用。
不管如何传递一个OUT或INOUT参数: - 如果子程序成功退出,那么实参的值就是最终赋给形参的值。 (形参至少有一个值——初始值。) - 如果子程序以异常结束,则实际参数的值未定义。 - 可以按任何顺序返回正式的 OUT 和 IN OUT 参数。 在此示例中,x 和 y 的最终值未定义:
CREATE OR REPLACE PROCEDURE p (x OUT INTEGER, y OUT INTEGER) AS BEGIN x := 17; y := 93; END; /
当 OUT 或 IN OUT 参数通过引用传递时,实际参数和形式参数引用相同的内存位置。 因此,如果子程序更改了形参的值,更改会立即显示在实参中。
在:ref:示例 9-13 中,过程 p 有两个 IN 参数、一个 OUT 参数和一个 IN OUT 参数。 OUT 和 IN OUT 参数按值传递(默认)。 匿名块使用不同的实际参数调用 p 两次。 在每次调用之前,匿名块打印实际参数的值。 过程 p 打印其形式参数的初始值。 每次调用后,匿名块再次打印实际参数的值。
在:ref:示例 9-14 中,匿名块使用实际参数调用过程 p(来自示例 9-14),该参数导致 p 引发预定义的异常 ZERO_DIVIDE,而 p 不处理该异常。异常传播到匿名块,该块处理 ZERO_DIVIDE 并显示 p 的 IN 和 IN OUT 参数的实际参数保留了它们在调用之前的值。 (异常传播在“异常传播”中进行了说明。)
在:ref:示例 9-15 中,过程 p 具有三个 OUT 形式参数:x,具有非 NULL 默认值的记录类型; y,没有非 NULL 默认值的记录类型;和 z,这不是记录。
x、y 和 z 对应的实际参数分别为 r1、r2 和 s。 s 用初始值声明。然而,当 p 被调用时,s 的值被初始化为 NULL。 r1 和 r2 的值分别初始化为其记录类型的默认值“abcde”和 NULL。
示例 9-13 过程调用之前、期间和之后的参数值
\set SQLTERM / CREATE OR REPLACE PROCEDURE p ( a PLS_INTEGER, --未指定参数模式默认为IN b IN PLS_INTEGER, c OUT PLS_INTEGER, d IN OUT BINARY_FLOAT ) AUTHID DEFINER IS BEGIN RAISE NOTICE 'Inside procedure p: '; RAISE NOTICE 'IN a = %', a; RAISE NOTICE 'IN b = %', b; RAISE NOTICE 'OUT c = %', c; RAISE NOTICE 'IN OUT d = %', d; c := a+10; d := 10/b; END; / DECLARE aa INT := 1; bb INT := 2; cc INT := 3; dd FLOAT := 4; ee INT; ff FLOAT := 5; BEGIN RAISE NOTICE 'Before invoking procedure p:'; RAISE NOTICE 'aa = %', aa; RAISE NOTICE 'bb = %', bb; RAISE NOTICE 'cc = %', cc; RAISE NOTICE 'dd = %', dd; p (aa, bb, cc, dd); RAISE NOTICE 'After invoking procedure p:'; RAISE NOTICE 'aa = %', aa; RAISE NOTICE 'bb = %', bb; RAISE NOTICE 'cc = %', cc; RAISE NOTICE 'dd = %', dd; RAISE NOTICE 'ee = %', ee; RAISE NOTICE 'ff = %', ff; DBMS_OUTPUT.PUT('ee = '); p (1, (bb+1)*2, ee, ff); RAISE NOTICE 'After invoking procedure p:'; RAISE NOTICE 'ee = %', ee; RAISE NOTICE 'ff = %', ff; END; /
结果:
NOTICE: Before invoking procedure p: NOTICE: aa = 1 NOTICE: bb = 2 NOTICE: cc = 3 NOTICE: dd = 4 NOTICE: Inside procedure p: NOTICE: IN a = 1 NOTICE: IN b = 2 NOTICE: OUT c =NOTICE: IN OUT d = 4 NOTICE: After invoking procedure p: NOTICE: aa = 1 NOTICE: bb = 2 NOTICE: cc = 11 NOTICE: dd = 5 NOTICE: ee = NOTICE: ff = 5 NOTICE: Inside procedure p: NOTICE: IN a = 1 NOTICE: IN b = 6 NOTICE: OUT c = NOTICE: IN OUT d = 5 NOTICE: After invoking procedure p: NOTICE: ee = 11 NOTICE: ff = 1
示例 9-14经过异常处理后的OUT和INOUT参数值
\set SQLTERM / DECLARE i INT := 1; j FLOAT := 5; BEGIN RAISE NOTICE 'Before invoking procedure p:'; RAISE NOTICE 'i = %', i; RAISE NOTICE 'j = %', j; p(4, 0, i, j); EXCEPTION WHEN ZERO_DIVIDE THEN RAISE NOTICE 'After invoking procedure p:'; RAISE NOTICE 'i = %', i; RAISE NOTICE 'j = %', j; END; /
结果:
NOTICE: Before invoking procedure p: NOTICE: i = 1 NOTICE: j = 5 NOTICE: Inside procedure p: NOTICE: IN a = 4 NOTICE: IN b = 0 NOTICE: OUT c =NOTICE: IN OUT d = 5 NOTICE: After invoking procedure p: NOTICE: i = 1 NOTICE: j = 5
示例 9-15默认值为非空的记录类型的IN形式化参数
\set SQLTERM / CREATE OR REPLACE PACKAGE pkg1 AUTHID DEFINER IS TYPE rec1 IS RECORD (f VARCHAR2(5) := 'abcde'); TYPE rec2 IS RECORD (f VARCHAR2(5)); END; / CREATE OR REPLACE PROCEDURE p ( x IN pkg1.rec1, y OUT pkg1.rec2, z OUT VARCHAR2) IS BEGIN RAISE NOTICE 'x.f = %', x.f; RAISE NOTICE 'y.f = %', y.f; RAISE NOTICE 'z = %', z; END; / DECLARE r1 pkg1.rec1; r2 pkg1.rec2; s VARCHAR2(5) := 'fghij'; BEGIN p (r1, r2, s); END; /
结果:
NOTICE: x.f = abcde NOTICE: y.f =NOTICE: z =
别名是同一内存位置有两个不同的名称。 如果存储的项目通过多个路径可见,并且您可以通过一个路径更改该项目,那么您可以看到所有路径的更改。
子程序参数别名总是发生在编译器通过引用传递实参时,也可能在子程序具有游标变量参数时发生。
9.7.4.1. 游标变量参数的子程序参数别名
游标变量参数是指针。因此,如果一个子程序将一个游标变量参数分配给另一个,它们指向的是相同的内存位置。这种混叠可能会产生意想不到的结果。
在示例 9-16中,该过程有两个游标变量参数,Stu_cv1和Stu_cv2。该过程打开Stu_cv1并将其值(它是一个指针)分配给Stu_cv2. 现在Stu_cv1并Stu_cv2引用相同的内存位置。当程序关闭Stu_cv1时,它也会关闭Stu_cv2。因此,当过程试图从 fetch 时Stu_cv2,PL/SQL 会引发异常。
示例 9-16 游标变量子程序参数的别名
DROP TABLE IF EXISTS stu_temp; CREATE TABLE stu_temp AS SELECT * FROM student; insert into stu_temp values (1001, 'xs', 61); insert into stu_temp values (1002, 'xd', 74); insert into stu_temp values (1003, 'xc', 83); insert into stu_temp values (1004, 'xg', 79); insert into stu_temp values (1005, 'xl', 98); \set SQLTERM / DECLARE TYPE StuCurTyp IS REF CURSOR; c1 StuCurTyp; c2 StuCurTyp; PROCEDURE get_Stu_data ( Stu_cv1 IN OUT StuCurTyp, Stu_cv2 IN OUT StuCurTyp ) IS Stu_rec stu_temp%ROWTYPE; BEGIN OPEN Stu_cv1 FOR SELECT * FROM stu_temp; Stu_cv2 := Stu_cv1; -- now both variables refer to same location FETCH Stu_cv1 INTO Stu_rec; -- fetches first row of student FETCH Stu_cv1 INTO Stu_rec; -- fetches second row of student FETCH Stu_cv2 INTO Stu_rec; -- fetches third row of student CLOSE Stu_cv1; -- closes both variables FETCH Stu_cv2 INTO Stu_rec; -- causes error when get_Stu_data is invoked END; BEGIN get_Stu_data(c1, c2); END; /
结果:
ERROR: cursor "" does not exist CONTEXT: PL/SQL function get_stu_data line 18 at FETCH SQL statement "CALL get_Stu_data(c1, c2)" PL/SQL function inline_code_block line 21 at CALL
当你声明一个形参IN时,你可以为它指定一个默认值。具有默认值的形参称为可选参数,因为其对应的实参在子程序调用中是可选的。如果省略了实参,则调用将默认值分配给形参。没有默认值的形参称为必填参数,因为在子程序调用中需要对应的实参。
省略一个实参不会产生对应的形参的值NULL。要使形式参数的值NULL,请指定NULL为默认值或实际参数。
在:ref:`示例 9-17`中,该过程具有一个必需参数和两个可选参数。
在:ref:`示例 9-17`中,过程调用指定实际参数的顺序与其相应的形式参数声明的顺序相同——也就是说,调用使用位置符号。位置表示法不允许你省略第二个参数,raise_salary而是指定第三个;为此,您必须使用命名或混合表示法。有关详细信息,请参阅“实际参数的位置、命名和混合表示法”。
形参的默认值可以是任何可以赋值给形参的表达式;也就是说,值和参数必须具有兼容的数据类型。如果子程序调用为形参指定了实参,则该调用不会评估默认值。
在:ref:`示例 9-18`中,过程p有一个参数,其默认值是函数的调用f。该函数f增加一个全局变量的值。当p在没有实际参数的情况下调用时,p调用f并f递增全局变量。当p使用实参调用时,p不调用f,全局变量的值不会改变。
示例 9-19创建一个带有两个必需参数的过程,调用它,然后添加第三个可选参数。因为第三个参数是可选的,所以原始调用仍然有效。
示例 9-17 使用默认参数值的过程
\set SQLTERM / DECLARE PROCEDURE raise_score ( Stu_id IN stu_temp.id%TYPE, add_score IN stu_temp.score%TYPE := 5 ) IS BEGIN UPDATE stu_temp SET score = score + add_score WHERE id = Stu_id; END raise_score; BEGIN raise_score(1001); raise_score(1002, 10); END; /
示例 9-18 函数提供默认参数值
\set SQLTERM / DECLARE global INT := 0; FUNCTION f RETURN INT IS BEGIN RAISE NOTICE 'Inside f.'; global := global + 1; RETURN global * 2; END f; PROCEDURE p ( x IN INT := f() ) IS BEGIN RAISE NOTICE 'Inside p. global = %, x = %', global, x; RAISE NOTICE '--------------------------------'; END p; PROCEDURE pre_p IS BEGIN RAISE NOTICE 'Before invoking p, global = %', global; RAISE NOTICE 'Invoking p.'; END pre_p; BEGIN pre_p; p(); pre_p; p(100); pre_p; p(); END; /
结果:
NOTICE: Inside f. NOTICE: Before invoking p, global = 1 NOTICE: Invoking p. NOTICE: Inside f. NOTICE: Inside p. global = 2, x = 4 NOTICE: -------------------------------- NOTICE: Before invoking p, global = 2 NOTICE: Invoking p. NOTICE: Inside p. global = 2, x = 100 NOTICE: -------------------------------- NOTICE: Before invoking p, global = 2 NOTICE: Invoking p. NOTICE: Inside f. NOTICE: Inside p. global = 3, x = 6 NOTICE: --------------------------------
示例 9-19在不更改现有的访问请求的情况下添加子程序参数
创建过程:
\set SQLTERM / CREATE OR REPLACE PROCEDURE show_name ( id INT, name VARCHAR2 ) AUTHID DEFINER IS BEGIN RAISE NOTICE '%. %',id ,name; END show_name; /
调用过程:
\set SQLTERM / BEGIN show_name(1, 'xx'); END; /
结果:
NOTICE: 1. xx
添加具有默认值的第三个参数:
\set SQLTERM / CREATE OR REPLACE PROCEDURE show_name ( id INT, name VARCHAR2, s VARCHAR2 := NULL ) AUTHID DEFINER IS BEGIN IF s IS NULL THEN RAISE NOTICE '%. %',id ,name; ELSE RAISE NOTICE '%.% %',id ,s, name; END IF; END show_name; /
调用过程:
\set SQLTERM / BEGIN show_name(1, 'xx'); show_name(1, 'xx', 'a'); END; /
结果:
NOTICE: 1. xx NOTICE: 1.a xx
调用子程序时,您可以使用位置、命名或混合表示法指定实际参数。表 9-3总结并比较了这些符号。
符号 | 句法 | 可选参数 | 优点 | 缺点 |
位置 | 以与声明形式参数相同的顺序指定实际参数。 | 您可以省略尾随可选参数。 | 无 | 以错误的顺序指定实际参数可能会导致难以检测的问题,尤其是当实际参数是文字时。如果形参列表改变,子程序调用必须改变,除非列表只获取新的尾随可选参数(如例 9-22 所示)。降低了代码的清晰度和可维护性。如果子程序有大量参数,不推荐使用。 |
命名 | 使用以下语法以任意顺序指定实际参数: | |||
formal => actual | ||||
formal是形参的名称,actual是实参。 | 您可以省略任何可选参数。 | 指定实际参数没有错误的顺序。只有当形式参数列表获得新的必需参数时,子程序调用才必须更改。当您调用由其他人定义或维护的子程序时建议使用。 | ||
混合 | 从位置符号开始,然后对其余参数使用命名符号。 | 在位置表示法中,您可以省略尾随可选参数;在命名符号中,您可以省略任何可选参数。 | 当您调用具有必需参数后跟可选参数的子程序时很方便,并且您必须仅指定几个可选参数。 | 在位置符号中,错误的顺序可能会导致难以检测的问题,尤其是在实际参数是文字的情况下。对形式参数列表的更改可能需要更改位置符号。 |
在:ref:`示例 9-20`中,过程调用使用不同的符号,但是是等价的。
在:ref:`示例 9-21`中,SQLSELECT语句调用 PL/SQL 函数compute_bonus,使用不同符号的等效调用。
示例 9-20 匿名块中具有不同符号的等效调用
\set SQLTERM / DECLARE id NUMBER(6) := 1001; num NUMBER(6) := 5; PROCEDURE raise_score ( Stu_id NUMBER, add_score NUMBER ) IS BEGIN UPDATE stu_temp SET score = score + add_score WHERE id = Stu_id; END raise_score; BEGIN raise_score(id, num); raise_score(add_score => num, Stu_id => id); raise_score(Stu_id => id, add_score => num); raise_score(id, add_score => num); END; /
示例 9-21 SELECT 语句中具有不同表示法的等效调用
\set SQLTERM / CREATE OR REPLACE FUNCTION compute_score ( Stu_id NUMBER, add_score NUMBER ) RETURN NUMBER AUTHID DEFINER IS Stu_score NUMBER; BEGIN SELECT score INTO Stu_score FROM stu_temp WHERE id = Stu_id; RETURN Stu_score + add_score; END compute_score; / \set SQLTERM ; SELECT compute_score(1001, 10) FROM DUAL; SELECT compute_score(add_score => 10, Stu_id => 1001) FROM DUAL; SELECT compute_score(1001, add_score => 10) FROM DUAL;
当 PL/SQL 编译器遇到子程序调用时,首先它会在当前范围内搜索匹配的子程序声明,如果没找到再去其他范围搜索。
如果它们的子程序名称和参数列表匹配,则声明和调用匹配。如果声明中的每个必需的形式参数在调用中都有对应的实际参数,则参数列表匹配。
如果编译器没有找到匹配的调用声明,则会生成语义错误。
在:ref:示例 9-22 中,函数 f 尝试使用适当的实际参数调用封闭过程交换。 但是,f 包含两个名为 swap 的嵌套过程,并且两者都没有与封闭过程 swap 相同类型的参数。因此,调用会导致编译错误。
示例 9-22 解析 PL/SQL 过程名称
\set SQLTERM / DECLARE PROCEDURE swap ( n1 NUMBER, n2 NUMBER ) IS num1 NUMBER; num2 NUMBER; FUNCTION f (num NUMBER) RETURN NUMBER IS x NUMBER := 10; PROCEDURE swap ( d1 DATE, d2 DATE ) IS BEGIN NULL; END; PROCEDURE swap ( b1 BOOLEAN, b2 BOOLEAN ) IS BEGIN NULL; END; BEGIN swap(num1, num2); RETURN x; END f; BEGIN NULL; END swap; BEGIN -- anonymous block NULL; END; -- anonymous block /
PL/SQL 允许您重载嵌套子程序、封装子程序和类型方法。如果多个不同的子程序的形式参数在名称、编号、顺序或数据类型族方面不同,您可以为多个不同的子程序使用相同的名称。(数据类型族是一种数据类型及其子类型。有关预定义 PL/SQL 数据类型的数据类型族,请参阅:ref:PL/SQL 预定义数据类型。有关用户定义的 PL/SQL 子类型的信息,请参阅“用户定义PL/SQL 子类型" .) 如果形参仅在名称上不同,则必须使用命名符号来指定对应的实参。(有关命名符号的信息,请参阅“实际参数的位置、命名和混合符号”.)
:ref:`示例 9-23`定义了两个同名的子程序,initialize. 这些过程初始化不同类型的集合。因为程序中的处理是相同的,所以给它们命名是合乎逻辑的。
您可以将这两个initialize过程放在同一个块、子程序、包或类型体中。PL/SQL 通过检查其形式参数来确定调用哪个过程。PL/SQL 使用的版本initialize取决于您是否使用date_tab_typ或num_tab_typ参数调用过程。
有关包中重载过程的示例,请参见:ref:示例 11-9。
示例 9-23 重载的子程序
\set SQLTERM / DECLARE TYPE date_tab_typ IS TABLE OF DATE INDEX BY INT; TYPE num_tab_typ IS TABLE OF NUMBER INDEX BY INT; date_tab date_tab_typ; num_tab num_tab_typ; PROCEDURE initialize (date_tab OUT date_tab_typ, i INTEGER) IS BEGIN RAISE NOTICE 'Invoked first version'; FOR i IN 1..i LOOP date_tab(i) := SYSDATE; END LOOP; END initialize; PROCEDURE initialize (num_tab OUT num_tab_typ, i INTEGER) IS BEGIN RAISE NOTICE 'Invoked second version'; FOR i IN 1..i LOOP num_tab(i) := 1.0; END LOOP; END initialize; BEGIN initialize(date_tab, 50); initialize(num_tab, 100); END; /
结果:
NOTICE: Invoked first version NOTICE: Invoked second version
您不能重载这些子程序:
独立子程序
形式参数仅在模式上不同的子程序,例如:
PROCEDURE s (p IN VARCHAR2) IS ... PROCEDURE s (p OUT VARCHAR2) IS ...
形式参数仅在子类型上不同的子程序,例如:
PROCEDURE s (p INTEGER ) IS ... PROCEDURE s (p REAL ) IS ...INTEGER和REAL是 的子类型NUMBER,因此它们属于同一数据类型族。
仅返回值数据类型不同的函数,即使数据类型属于不同的族;例如:
FUNCTION f (p INTEGER) RETURN BOOLEAN IS ... FUNCTION f (p INTEGER) RETURN INTEGER IS ...
PL/SQL 编译器一旦确定它无法判断调用了哪个子程序,就会立即捕获重载错误。当子程序具有相同的头时,当您尝试编译子程序本身(如果它们是嵌套的)或尝试编译声明它们的包规范时, 编译器会捕获重载错误。否则,当您尝试编译子程序的模棱两可的调用时,编译器会捕获错误。
子程序有相同的参数名和参数类型时,编译报错,如:ref:`示例 9-24`中所示。
子程序有相同的参数名和不同的参数类型时,编译报错,如:ref:`示例 9-25`中所示。
假设您通过为重载子程序的形参指定不同的名称来纠正:ref:示例 9-25`中的重载错误,如:ref:`示例 9-26 所示。
现在您可以编译pkg2.s如果您使用命名符号指定实际参数,则不会出错,如:ref:示例 9-27 所示。
当试图确定调用了哪个子程序时,如果 PL/SQL 编译器将一个参数隐式转换为匹配类型,则编译器会查找它可以隐式转换为匹配类型的其他参数。如果有多个匹配项,则会出现编译时错误,如:ref`示例 9-29` 所示。
示例 9-24 重载错误导致编译时错误
\set SQLTERM / CREATE OR REPLACE PACKAGE pkg1 AUTHID DEFINER IS PROCEDURE s (a TEXT); PROCEDURE s (a TEXT); END pkg1; /
示例 9-25 不同类型相同参数名重载错误
\set SQLTERM / CREATE OR REPLACE PACKAGE pkg2 AUTHID DEFINER IS SUBTYPE s1 IS TEXT; SUBTYPE s2 IS TEXT; PROCEDURE s (a s1); PROCEDURE s (a s2); END pkg2; /
示例 9-26 更正示例 9-25中的重载错误
CREATE OR REPLACE PACKAGE pkg2 AUTHID DEFINER IS SUBTYPE s1 IS TEXT; SUBTYPE s2 IS TEXT; PROCEDURE s (a1 s1); PROCEDURE s (a2 s2); END pkg2; /
示例 9-27 调用示例 9-26中的子程序
\set SQLTERM / CREATE OR REPLACE PROCEDURE p AUTHID DEFINER IS a pkg2.s1 := 'a'; BEGIN pkg2.s(a1=>a); END p; /
示例 9-28 没有重载错误的包规范
\set SQLTERM / CREATE OR REPLACE PACKAGE pkg3 AUTHID DEFINER IS PROCEDURE s (a1 TEXT); PROCEDURE s (a1 TEXT, a2 TEXT := 'a2'); END pkg3; /
示例 9-29 参数的隐式转换导致重载错误
\set SQLTERM / CREATE OR REPLACE PACKAGE pkg4 AUTHID DEFINER AS PROCEDURE p1 (a NUMBER, b TEXT); PROCEDURE p1 (a NUMBER, b NUMBER); END; / CREATE OR REPLACE PACKAGE BODY pkg4 AS PROCEDURE p1 (a NUMBER, b TEXT) IS BEGIN NULL; END; PROCEDURE p1 (a NUMBER, b NUMBER) IS BEGIN NULL; END; END; / BEGIN pkg4.p1(1,'2'); -- 正确 pkg4.p1(1,2); -- 出错 pkg4.p1('1','2'); -- 正确 pkg4.p1('1',2); -- 出错 END; /
递归子程序调用自身。递归是一种用于简化算法的强大技术。
递归子程序必须至少有两条执行路径——一条通往递归调用,一条通往终止条件。如果没有后者,递归将继续,直到 PL/SQL 内存不足并引发预定义的异常。
在示例 9-30中,该函数实现了以下n阶乘 ( n! ) 的递归定义,即从 1 到n的所有整数的乘积:
n! = n * (n - 1)!
在示例 9-31中,该函数返回第n个斐波纳契数,它是第n 个-1 和n -2 个斐波纳契数的和。第一个和第二个斐波那契数分别为零和一。
子程序的每次递归调用都会为子程序声明的每个项目和它执行的每个 SQL 语句创建一个实例。
FOR LOOP游标语句内部或OPEN 或 OPEN FOR语句与 CLOSE 语句之间的递归调用会在每次调用时打开另一个游标,这可能会导致打开的游标数量超过数据库初始化参数 OPEN_CURSORS 设置的限制。
示例 9-30 递归函数返回 n 阶乘 (n!)
\set SQLTERM / CREATE OR REPLACE FUNCTION factorial ( n INT ) RETURN INT AUTHID DEFINER IS BEGIN IF n = 1 THEN -- terminating condition RETURN n; ELSE RETURN n * factorial(n-1); -- recursive invocation END IF; END; / BEGIN FOR i IN 1..5 LOOP RAISE NOTICE '%! = %',i ,factorial(i); END LOOP; END; /
结果:
NOTICE: 1! = 1 NOTICE: 2! = 2 NOTICE: 3! = 6 NOTICE: 4! = 24 NOTICE: 5! = 120
示例 9-31 递归函数返回第 n 个斐波那契数
\set SQLTERM / CREATE OR REPLACE FUNCTION fibonacci ( n PLS_INTEGER ) RETURN PLS_INTEGER AUTHID DEFINER IS f1 PLS_INTEGER := 0; f2 PLS_INTEGER := 1; BEGIN IF n = 1 THEN RETURN f1; ELSIF n = 2 THEN RETURN f2; ELSE RETURN fibonacci(n-2) + fibonacci(n-1); END IF; END; / BEGIN FOR i IN 1..10 LOOP RAISE NOTICE '%',fibonacci(i); IF i < 10 THEN DBMS_OUTPUT.PUT(', '); END IF; END LOOP; RAISE NOTICE ' ...'; END; /
结果:
NOTICE: 0 NOTICE: 1 NOTICE: 1 NOTICE: 2 NOTICE: 3 NOTICE: 5 NOTICE: 8 NOTICE: 13 NOTICE: 21 NOTICE: 34 NOTICE: ...
如果一个子程序改变了除了它自己的局部变量的值之外的任何东西,它就会产生副作用。 例如,更改以下任何内容的子程序都会产生副作用: - 它自己的 OUT 或 IN OUT 参数 - 全局变量 - 包中的公共变量 - 一个数据库表 - 数据库 - 外部状态(例如,通过调用 DBMS_OUTPUT 或发送电子邮件)
副作用可能会阻止查询的并行化、产生与顺序相关的(因此是不确定的)结果,或者需要跨用户会话维护包状态。
在定义结果缓存函数或存储函数以供 SQL 语句调用时,最大限度地减少副作用尤其重要。
另请参阅
《数据库开发指南》,了解有关控制从 SQL 语句调用 PL/SQL函数中的副作用信息
要从 SQL 语句中调用,存储的函数(以及它调用的任何子程序)必须遵守以下纯度规则,这些规则旨在控制副作用: - 当从 SELECT 语句或并行化的 INSERT、UPDATE、DELETE 或 MERGE 语句调用时,子程序不能修改任何数据库表。 - 当从 INSERT、UPDATE、DELETE 或 MERGE 语句调用时,子程序不能查询或修改由该语句修改的任何数据库表。
如果函数查询或修改表,并且该表上的 DML 语句调用该函数,则会发生 ORA-04091(变异表错误)。 有一个例外:如果不在 FORALL 语句中的单行 INSERT 语句调用 VALUES 子句中的函数,则不会发生 ORA-04091。
当从SELECT, INSERT, UPDATE, DELETE, 或者 MERGE语句调用时,子程序不能执行以下任何 SQL 语句(除非PRAGMA AUTONOMOUS_TRANSACTION指定):
事务控制语句(如COMMIT)
会话控制语句(如SET ROLE)
系统控制语句(如ALTER SYSTEM)
CREATE自动提交的 数据库定义语言 (DDL) 语句(例如)(有关PRAGMA AUTONOMOUS_TRANSACTION的描述,请参阅“ AUTONOMOUS_TRANSACTION Pragma ”。)
如果函数执行部分中的任何 SQL 语句违反规则,则在解析该语句时会发生运行时错误。
函数的副作用越少,它在 SELECT 语句中的优化效果就越好,特别是如果函数使用选项 DETERMINISTIC 或声明 PARALLEL_ENABLE。(有关这些选项的描述,请参阅"DETERMINSTIC 子句"和"PARALLEL_ENABLE 子句")
另请参阅
《数据库开发指南》了解有关SQL语句可以调用的 PL/SQL 函数的限制的信息
"调用在查询中的函数调用"
存储的 PL/SQL 单元的AUTHID属性会影响该单元在运行时发出的 SQL 语句的名称解析和权限检查。该AUTHID属性不影响编译,对于没有代码的单元(例如集合类型)没有意义。
AUTHID属性值在sys_proc数据字典视图中公开。CURRENT_USER权利prosecdf字段显示f;DEFINER权利prosecdf字段显示t;
对于您使用以下语句创建或更改的存储 PL/SQL 单元,您可以使用可选AUTHID子句指定DEFINER(默认值,用于向后兼容)或CURRENT_USER(首选用法): - "CREATE FUNCTION Statement" - "CREATE PACKAGE Statement" - "CREATE PROCEDURE Statement" - "CREATE TYPE Statement" - "ALTER TYPE Statement"
其AUTHID值CURRENT_USER称为调用者权限单元或IR 单元的单元。AUTHID值为(默认值)的单元DEFINER称为定义者权限单元或DR 单元。
单元的AUTHID属性决定了该单元是 IR 还是 DR,它影响名称解析和运行时的权限检查。
在服务器调用期间,CURRENT_USER值服务器检查当前调用者的用户权限,definer值服务器检查定义者的用户权限。
进入 IR 单元后,运行时系统会在进行任何初始化或运行任何代码之前检查权限。若系统中存在权限不足的情况,则运行时系统会引发错误。
另请参阅
《数据库安全指南》了解有关管理 DR 和 IR 单元安全性的信息
《数据库安全指南》了解有关捕获编译 DR 和 IR 单元所需权限的信息
使用 SQL 的GRANT命令,您可以将角色授予 PL/SQL 包和独立子程序。授予 PL/SQL 单元的角色不影响编译。它们会影响单元在运行时发出的 SQL 语句的权限检查:单元以它自己的角色和任何其他当前启用的角色的权限运行。
通常,您将角色授予 IR 单元,以便权限低于您的用户可以仅使用所需权限运行该单元。仅当 DR 单元发出动态 SQL 时,才将角色授予 DR 单元(其调用者以您的所有权限运行它),动态 SQL 仅在运行时进行检查。
将角色授予 PL/SQL 单元的基本语法是:
GRANT role [, role ]... TO unit [, unit ]..
例如,此命令将执行角色授予函数 scott.func 和包 sys.pkg:
GRANT execute TO FUNCTION scott.func, PACKAGE sys.pkg
有关该GRANT命令的完整语法和语义,请参阅SQL 语言参考。
另请参阅
《SQL 语句参考手册》获取有关REVOKE命令的信息,该命令允许您从 PL/SQL 单元撤消角色
《数据库安全指南》了解有关配置应用程序用户和应用程序角色的更多信息
一个用户(即一个模式)拥有一个 IR 单元,其他用户在他们的模式中运行它。 如果 IR 单元发出静态 SQL 语句,那么这些语句影响的模式对象在编译时必须存在于所有者的模式中(以便编译器可以解析引用)和在运行时调用者的模式中。 对应架构对象的定义必须匹配(例如对应的表必须具有相同的名称和列); 否则,您会收到错误或意外结果。 但是,所有者模式中的对象不需要包含数据,因为编译器不需要它; 因此,它们被称为模板对象。