Oracle 存储过程详解(上)

目录

  • 一、存储过程与存储函数的定义
  • 二、创建 / 执行存储过程所需的权限
    • 1、resource权限
    • 2、create、execute procedure权限
  • 三、创建 / 执行存储过程
  • 四、变量与参数
    • 1、变量
      • 1) 标量类型
      • 2) 复合变量类型
        • a) 复合记录类型
        • b) 复合表类型(关联数组)
        • c) 变长数组类型
      • 3) 参照类型
    • 2、参数
      • 1) in
      • 2) out
      • 3) in out
      • 4) 设置参数默认值
      • 5) 系统参数
  • 附录:

一、存储过程与存储函数的定义

       所谓的存储过程(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]

1、resource权限

       开发的时候,我们可以直接授予开发人员以管理员权限(dba)或是开发者权限(resource),给予相关人员最多的权限内容,让他们能做更多的事情(当然开发完成后就得反过来,给予外部人员最少的权限以降低安全性问题发生的风险):

grant resource to [ user_name ];
-- 或者是 grant dba to [ user_name ];

2、create、execute procedure权限

       从我个人看法而言,我们应该尽可能的只给予用户工作所需的最低权限,用于确保系统的安全性。比如开发人员只需要做存储过程设计,那我就只给他相关的权限:

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;
/
  • 运用此语法我们创建了一个名为“procedure_name”的存储过程。首行的 replace 表示替换,对于Oracle的存储过程而言,我们只能创建 (create)、删除 (drop) 或替换 (replace) 它,没有类似于SQL Server的修改 (Alter) 操作。
  • 第二行的 as 与SQL Server中的Declare作用相似,但as不可省略,是固定语法,之后讲到参数的时候,我们就要在as和begin之间插入参数列表。
  • beginend 表示PL-SQL语句块的开始和结束,所有需要执行的语句都写在此处。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 存储过程详解(上)_第1张图片

关于Oracle的表命名规范可以参考下面这篇文章,尽管严格来说,表命名并没有太多的强制要求,但规范的命名习惯有利于我们日后的维护:Oracle命名规范。
dbms_output有很多的用途,想知道其细节的话可查阅本文末的相关链接。

四、变量与参数

       在设计存储过程的时候,我们必然会用到变量与参数,它们可以扩展代码的灵活性,让我们做到更多事情。在Oracle中,参数与变量有着截然不同的语法。
       开讲之前有个小细节我想和大家提一下,我相信诸位在查找相关资料的时候一定有看到“as”和“is”这两种不同的写法,严格来说在存储过程中二者没有什么显著的差别,它们是同义词,但使用as的情况居多。值得注意的是,在创建视图的时候我们只能用as,而在声明游标的时候只能用is。[4]

1、变量

       首先让我们看看声明变量的语法[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的变量命名遵循系统命名规则,在此我们不做赘述,但变量类型则有多种不同的分类:标量类型复合变量类型参照类型大型数据对象

1) 标量类型

       标量类型既包括了系统中的标准数据类型,诸如varcharnumber等;亦包括了一些比较少用的类型,比如BINARY_INTEGERboolean等。这些类型使用广泛、声明简单,是变量类型中的基础。下面这个例子会创建一个名为“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;

Oracle 存储过程详解(上)_第2张图片
       这个例子仅仅只是用来演示变量效果的,实际情况中我们肯定不会干这种在存储过程中只塞一个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;
-- 效果与前者一致

2) 复合变量类型

       复合变量类型要比标量类型更加复杂,在这里我只做一些简单的解释。它包含以下几种类型:

a) 复合记录类型

       就我个人看法而言我觉得它无论是看起来还是用起来都很像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;

b) 复合表类型(关联数组)

       索引表(关联数组)是一种更为复杂的记录类型,尽管在声明的时候我们会用到 “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 是索引号的类型,它可以是标量类型,也可以是我们自己声明的记录类型,声明索引号的时,除非使用的是有固定大小或有默认大小的类型(如numberBINARY_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这个位置上。索引表保存的实际上是数据的逻辑地址,这与数组的保存原理一脉相承。

c) 变长数组类型

       变长数组(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;

       输出结果如下:
Oracle 存储过程详解(上)_第3张图片
除了直接使用下标读取之外,我们也可以通过循环遍历数组输出:

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;

3) 参照类型

       参照类型主要是用在游标上,我会在 “游标” 里具体介绍这部分内容。

2、参数

       参数是编写存储过程中的另一个重要组成元素。在前面的演示中,我们经常能看见这个方法被调用:dbms_output.put_line(),细心的同志们在PL/SQL Developer中敲出来的时候就会注意到,put_line其实也是一个存储过程,dbms_output是它所属的包:
在这里插入图片描述
       这就意味着,我们先前在括号中输入的其实就是参数,在这里我们不去研究这个过程的内部细节,我们先看看参数分别有哪些类型。

1) in

       对于一个参数而言,如果它被标记成 “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 ,这很好理解,直接通过参数名就可以调用参数了。但是请注意:

  • 输入参数不能被赋值,给输入参数赋值会引发异常
  • 输入参数只能用于给存储过程传递外部的值,不能执行其他功能
  • 在输入IN参数的类型的时候,不需要给参数指定范围
    (比如 param IN varchar2(20),这就是种错误的写法,应写成 param IN varchar2

2) out

       OUT参数即是输出参数,被标记为OUT的参数在程序段执行完毕后会将该参数的最终值赋给对应的“实参变量”[9]。注意:

  • 输出参数可以被 “:=” 符号赋值
  • 在存储过程中调用输出参数的时候会忽略传递进来的值
  • 只有变量可以被传递进程序段中做输出参数的
  • 和IN参数一样,在输入OUT参数的类型的时候,不需要指定范围
-- 创建存储过程,定义输入输出变量
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;
/

3) in out

       将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;
/

4) 设置参数默认值

       编写存储过程的时候,我们可以通过 “default” 字段声明参数的初始值。注意:

  • 只有IN参数可以声明初始值,OUT参数和IN OUT参数均不可声明
  • 需要声明初始值的参数最好放在参数列表的最后

       在这里我要解释下第二点,对于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的灵活性实现一些有趣的事情,至于要怎么做,就是靠我们自己去发掘了。

5) 系统参数

在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 模式参数

你可能感兴趣的:(学习笔记)