所谓的存储过程(Stored Procedure),即是指数据库中的用于完成某一功能的一组SQL语句,这组语句生成后就会被数据库编译并保存,与在控制台中直接调用语句比起来,存储过程有着更高的效率和安全性。
存储函数可以看作是特殊的存储过程,存储函数被强制要求使用return语句返回函数值,而存储过程不需要。尽管就目前而言,存储过程可以借助in和out参数完成存储函数所能承担的工作,但为了兼容旧版本,Oracle仍旧保留了存储函数的相关内容[1]。本文中只讨论存储过程。
在代码中,所有被 “{ }” 包裹起来的表示必填项,所有被 “[ ]” 包裹起来的均为选填项。比如语句“create [ or replace ]”中,[ or replace ]表示用户可以选择性输入“or replace”字段,替换我们已经创建的存储过程。
在本文的演示中,我会尽可能用清晰明了的英文短语表述变量或过程的含义。我使用的环境是 PL/SQL Developer 13.0 和 Oracle 11g,相关教程各位可以在网络上找到。
创建存储过程需要我们的账户至少要拥有 CREATE 和 EXECUTE 存储过程的权限。该步骤为可选步骤,使用管理员账号学习Oracle语法的同志可以跳过。下面有两种给予用户相应权限的方式 [2]:
开发的时候,我们可以直接授予开发人员以管理员权限(dba)或是开发者权限(resource),给予相关人员最多的权限内容,让他们能做更多的事情(当然开发完成后就得反过来,给予外部人员最少的权限以降低安全性问题发生的风险):
grant resource to [ user_name ];
-- 或者是 grant dba to [ user_name ];
从我个人看法而言,我们应该尽可能的只给予用户工作所需的最低权限,用于确保系统的安全性。比如开发人员只需要做存储过程设计,那我就只给他相关的权限:
grant create any procedure to [ user_name ];
grant execute any procedure to [ user_name ];
通过以上这种方法,用户就可以获得创建和执行存储过程的权利。在这里有个小插曲,在使用PL\SQL软件操作Oracle的时候,你还需要给用户创建回话的权限,否则可能会触发异常ORA-01045:
grant create session to [ user_name ];
以下是一个创建无参存储过程的语法块:
create [ or replace ] procedure procedure_name
as
begin
-- PL-SQL blocks
end;
/
明白语法之后,我们可以自己动手试一下。下面的这段代码是一个简单的例子,它创建了一个叫做 “examsp_query_cs” 的存储过程,每次调用的时候它都会在屏幕上打印出一句“Hello World”:
create or replace procedure examsp_query_cs as
begin
dbms_output.put_line('Hello World');
end examsp_query_cs;
/
如果是在SQLPLUS中执行该语句段,我们应该要能看见屏幕上显示“存储过程已创建”(Procedure created)的提示,这时候我们还需要再执行一个语句set serveroutput on;
设置 serveroutput 的值为on可以让dbms_output输出的内容显示在屏幕上[3],之后我们再调用存储过程,就可以看到效果了。执行存储过程的方式有以下几种:
-- 通过 exec 执行,只能在 SQLPLUS 中调用,过程名后面跟上参数列表,用逗号分隔
exec proc_name [ param_1, param_2... ];
-- 通过 call 执行,可以在任意场合使用,无论有没有参数都要写括号,在括号中写入参数
call proc_name([ param_1, param_2... ]);
-- 通过 begin - end 句段调用,在PL/SQL Developer中,建议使用此方式
begin
proc_name([ param_1, param_2... ]);
end;
关于Oracle的表命名规范可以参考下面这篇文章,尽管严格来说,表命名并没有太多的强制要求,但规范的命名习惯有利于我们日后的维护:Oracle命名规范。
dbms_output有很多的用途,想知道其细节的话可查阅本文末的相关链接。
在设计存储过程的时候,我们必然会用到变量与参数,它们可以扩展代码的灵活性,让我们做到更多事情。在Oracle中,参数与变量有着截然不同的语法。
开讲之前有个小细节我想和大家提一下,我相信诸位在查找相关资料的时候一定有看到“as”和“is”这两种不同的写法,严格来说在存储过程中二者没有什么显著的差别,它们是同义词,但使用as的情况居多。值得注意的是,在创建视图的时候我们只能用as,而在声明游标的时候只能用is。[4]
首先让我们看看声明变量的语法[5]:
create [ or replace ] procedure procedure_name
as
[ var_1 var_type (var_size); ]
begin
-- PL-SQL blocks
end;
/
在这里,var_1表示变量名,var_type表示变量的类型,var_size表示取值范围(变量大小),当我们要声明一个变量的时候,这三个元素缺一不可。Oracle的变量命名遵循系统命名规则,在此我们不做赘述,但变量类型则有多种不同的分类:标量类型、复合变量类型、参照类型、大型数据对象。
标量类型既包括了系统中的标准数据类型,诸如varchar
、number
等;亦包括了一些比较少用的类型,比如BINARY_INTEGER
、boolean
等。这些类型使用广泛、声明简单,是变量类型中的基础。下面这个例子会创建一个名为“proc_findGirl”的存储过程,它会从“Employee”表中找到一个ID为6的雇员:
create or replace procedure proc_findGirl
as
girl_id number(4);
girl_name varchar(20);
girl_sex varchar(10);
girl_salary number;
begin
select emp_id, emp_name, emp_sex, emp_salary
into girl_id, girl_name, girl_sex, girl_salary
from employee
where emp_id = '6';
dbms_output.put_line('name: ' || girl_name || ' id: '
|| girl_id || ' sex: ' || girl_sex);
end proc_findGirl;
这个例子仅仅只是用来演示变量效果的,实际情况中我们肯定不会干这种在存储过程中只塞一个select语句的蠢事。上图即是执行效果,在存储过程中调用select语句必须使用变量接收查询结果,否则会出现异常。
还有一种变量类型叫做 “%TYPE[6]” ,你可以把它看做是一种动态数据类型,它由一个已经定义了的变量调用,并返回该变量的类型。比如说:
v_msg varchar(20);
v_msg_back v_msg%TYPE;
-- 在这里,v_msg 和 v_msg_back 的类型都是 varchar(20)
这很OOP,尤其在我们使用参数的时候,我们很难确定输入的参数类型;或者是通过表格给变量赋值的时候,如果字段类型变了,我们还得跟着修改所有的过程。用一个%TYPE
就可以解决这些问题,提高了代码的可复用率和稳定性。在接下来的例子中,我们还会看到更多使用%TYPE
情况出现。
与之相似的还有%ROWTYPE
,顾名思义,它能保存一个表格中所有列的类型,你可以直接将它看做是一条行记录:
create or replace procedure proc_findGirl
as
girl employee%ROWTYPE;
begin
select emp_id, emp_name, emp_sex, emp_salary
into girl
from employee
where emp_id = '6';
dbms_output.put_line('name: ' || girl.emp_name || ' id: '
|| girl.emp_id || ' sex: ' || girl.emp_sex);
end proc_findGirl;
-- 效果与前者一致
复合变量类型要比标量类型更加复杂,在这里我只做一些简单的解释。它包含以下几种类型:
就我个人看法而言我觉得它无论是看起来还是用起来都很像java里的结构体。我们会声明一种record类型的变量,该变量内含有多个标量类型的变量,随后声明该record类型的“对象”[7]:
type record_type_name is record (
var_name var_type(var_size)[, var_name var_type(var_size)]
);
var_record_type record_type_name;
该语法声明了一个叫做 “record_type_name” 的记录类型,里面含有复数个变量(单个变量没有声明成记录的必要)。随后,我们声明了一个名为 “var_record_type” 的 “record_type_name” 类型的变量。下面这个例子就是一种应用,如我们前面所说,使用%TYPE
可以给我们很大的帮助:
create or replace procedure proc_findGirl
as
type emp_record_type is record (
r_name employee.emp_name%TYPE ,
r_salary employee.emp_salary%TYPE
);
employee_record emp_record_type;
begin
select emp_name, emp_salary
into employee_record
from employee
where emp_id = '7';
dbms_output.put_line('name: ' || employee_record.r_name || ' salary: '
|| employee_record.r_salary);
end proc_findGirl;
索引表(关联数组)是一种更为复杂的记录类型,尽管在声明的时候我们会用到 “is table of” ,但本质上来讲它更接近数组,索引表通过指定类型的索引确定其元素所在位置。下面是声明索引表的语法:
type table_type_name is table of type_name
index by index_type;
var_table table_type_name;
在这里,table_type_name 即是我们所声明的索引表的名字;type_name 是索引号的类型,它可以是标量类型,也可以是我们自己声明的记录类型,声明索引号的时,除非使用的是有固定大小或有默认大小的类型(如number
、BINARY_INTEGER
),否则我们必须声明其大小(如varchar2(20)
)。下面是一个示例,我们声明了一个叫做 v_table_emp 的索引表,其索引号类型为BINARY_INTEGER
,我们将查到的一条记录保存到了表中下标(索引号)为0的位置上:
create or replace procedure proc_findGirl
as
type emp_record_type is record (
r_name employee.emp_name%TYPE ,
r_salary employee.emp_salary%TYPE
);
type table_employee_record is table of emp_record_type
index by binary_integer;
v_table_emp table_employee_record;
begin
select emp_name, emp_salary
into v_table_emp(0)
from employee
where emp_id = '1';
dbms_output.put_line('name: ' || v_table_emp(0).r_name || ' salary: '
|| v_table_emp(0).r_salary);
end proc_findGirl;
在Oracle中,索引表的下标可以是负数,所以在上面这个例子中你也可以把信息保存在-1这个位置上。索引表保存的实际上是数据的逻辑地址,这与数组的保存原理一脉相承。
变长数组(varray,即variable array)是一种可以自行设置长度的数组,它能保存一系列相同类型的元素。声明变长数组的语法如下:
type var_name is varray(var_size) of type_name[(type_size)] [NOT NULL];
其中,var_name 是变长数组名;var_size是数组的最大长度,和索引表不同的是,数组的下标从1开始,没有负数;type_name 是类型名,和索引表一样,没有固定大小和默认大小的类型需要声明大小(type_size)。如果不允许元素取空,你可以在末尾加上 “NOT NULL” 字段[8]。下面这个例子演示了数组的赋值和通过下标输出元素值:
DECLARE
type exam_array_type is varray(4) of varchar(5);
exam_array exam_array_type;
BEGIN
exam_array := exam_array_type('Tom', 'Sam', 'Lily', 'Jonas');
dbms_output.put_line('[1] ' || exam_array(1));
dbms_output.put_line('[2] ' || exam_array(2));
dbms_output.put_line('[3] ' || exam_array(3));
dbms_output.put_line('[4] ' || exam_array(4));
END;
输出结果如下:
除了直接使用下标读取之外,我们也可以通过循环遍历数组输出:
DECLARE
type exam_array_type is varray(4) of varchar(5);
exam_array exam_array_type;
BEGIN
exam_array := exam_array_type('Tom', 'Sam', 'Lily', 'Jonas');
dbms_output.put_line('loop start..');
-- 下面是一个简单的for循环,关于循环的内容我将会 “循环分支控制” 中介绍
for i in 1..exam_array.count loop
dbms_output.put_line('[' || i || '] ' || exam_array(i));
end loop;
dbms_output.put_line('loop end..');
END;
参照类型主要是用在游标上,我会在 “游标” 里具体介绍这部分内容。
参数是编写存储过程中的另一个重要组成元素。在前面的演示中,我们经常能看见这个方法被调用:dbms_output.put_line()
,细心的同志们在PL/SQL Developer中敲出来的时候就会注意到,put_line
其实也是一个存储过程,dbms_output
是它所属的包:
这就意味着,我们先前在括号中输入的其实就是参数,在这里我们不去研究这个过程的内部细节,我们先看看参数分别有哪些类型。
对于一个参数而言,如果它被标记成 “in” ,则意味着它是一个输入参数,在调用存储过程的时候,我们必须输入符合要求的参数,否则系统就会抛出异常:
create or replace procedure procedure_name
( param_name in param_type [ , param_name in param_type ] )
as
begin
-- PL-SQL blocks
end;
在这段代码中我们定义了一个类型为 param_type 的输入参数 param_name ,这很好理解,直接通过参数名就可以调用参数了。但是请注意:
param IN varchar2(20)
,这就是种错误的写法,应写成 param IN varchar2
)OUT参数即是输出参数,被标记为OUT的参数在程序段执行完毕后会将该参数的最终值赋给对应的“实参变量”[9]。注意:
-- 创建存储过程,定义输入输出变量
create or replace procedure proc_query_by_id (
query_id in employee.emp_id%type,
query_name out employee.emp_name%type
)
as
begin
select emp_name
into query_name
from employee
where emp_id = query_id;
end proc_query_by_id;
/
-- 定义变量接收输出参数值并输出
declare emp_name varchar2(20);
begin
proc_query_by_id(1, emp_name);
dbms_output.put_line(emp_name);
end;
/
将IN参数与OUT参数合二为一的产物,可以赋值也可以返回值,但必须得是实参变量调用。
-- 创建存储过程,使传入的数字型参数加1
create or replace procedure proc_add (
param in out number
)
as
begin
param := param + 1;
end;
/
-- 执行存储过程并输出执行前后的结果
declare num number := 1;
begin
dbms_output.put_line('num = ' || num);
proc_add(num);
dbms_output.put_line('num = ' || num);
end;
/
编写存储过程的时候,我们可以通过 “default” 字段声明参数的初始值。注意:
在这里我要解释下第二点,对于Oracle而言,你可以给任何位置上的IN参数添加初始值,但能不能用就是另外一回事儿了。举个例子,下方是一段执行程序,它调用了 proc_add 这个过程,使传入的参数加1并将计算结果传递出来:
declare num number := 5;
begin
dbms_output.put_line('num = ' || num);
proc_add(num);
dbms_output.put_line('num = ' || num);
end;
如果我们的存储过程是像下面这样写的,那这段代码就没问题:
-- Plan A
create or replace procedure proc_add(
param_out out number,
param_in in number default 0
)
as
begin
param_out := param_in + 1;
end;
但如果写成这样,系统在执行的时候就会抛出异常ORA-06550:
-- Plan B
create or replace procedure proc_add(
param_in in number default 0,
param_out out number
)
as
begin
param_out := param_in + 1;
end;
这两段代码都可以通过编译,因为从语法上来讲它们没有任何问题,但是在执行的时候,二者的差异就出来了:在语句 proc_add(num);
中,我只调用了一个参数,它必定对应着参数列表中的第一个。所以对于A而言,这个参数是OUT型参数 param_out ,没有给定参数的IN型参数 param_in 被赋予默认值0,输出的计算结果就是1;而对于B而言,这个参数对应的是 param_in ,有了指定的值之后默认值就数去了作用,没有被赋值的 param_out 找不到指定参数,抛出了异常。
所以给参数赋默认值的最好考虑下调用情况,尽管安全性堪忧,但或许可以借助default的灵活性实现一些有趣的事情,至于要怎么做,就是靠我们自己去发掘了。
在Oracle中有许多已经定义好了的系统参数,对这块有兴趣的同志可以看看这篇文章:Oracle参数查看方法小结
文中的示例和部分解释参考了网上搜索到的文章,我衷心感谢这些优秀的创作者们所做的贡献,没有他们的资料我将寸步难行。各位如果感兴趣,可以点击下方的链接查看他们的文章:
[1] Oracle存储过程与存储函数-入门
[2] Oracle中connect,resource角色权限
[3] Oracle系统包——dbms_output用法
[4] Oracle中存储过程和函数中IS和AS的区别
[5] (转)Oracle存储过程详解(一)
[6] Oracle存储过程----变量的介绍及使用(PL/SQL)
[7] Oracle参数变量类型
[8] ORACLE中RECORD、VARRAY、TABLE的使用详解
[9] Oracle存储过程in、out、in out 模式参数