知识点:了解异常的处理、异常的作用域,以及异常的传播。学习异常如何捕获 PL/SQL 语句块的声明部分、可执行部分或者异常处理部分所发生的运行时错误。也学习如何定义自己的异常以及如何重新抛出异常。
1、理解异常
在编写 PL/SQL 程序时,避免不了会发生一些错误,可能是程序设计人员自己造成的,也可能是操作系统或硬件环境出错,比如出现除数为零、磁盘 I/O 错误等情况。对于出现的这次错误,Oracle 采用异常机制来处理,以长阳处理代码通常放在 PL/SQL 的 EXCEPTION代码块中。根据异常产生的机制和原理,可将 Oracle 系统异常分为以下两大类:
预定义异常:Oracle 系统自身为用户提供了大量的、可在 PL/SQL 中使用的预定义异常,以便检查用户代码失败的一般原因。他们都定义在 Oracle 的核心 PL/SQL 库中,用户可以在自己的 PL/SQL 异常处理部分使用名称对其进行标识。对这种异常情况的处理,用户无须再程序中定义,他们有 Oracle 自动引发。
自定义异常:有时候可能会出现操作系统错误或机器硬件故障,这些错误 Oracle 系统自身无法知晓,也不能控制。例如,操作系统因病毒破坏而产生故障、磁盘村坏、网络突然中断等。另外,因业务的实际需求,程序设计人员需要自定义一些错误的业务逻辑,而PL/SQL 程序在运行过程中就可能会触发到这些错误的业务逻辑。那么,对于以上这些异常情况的处理,就需要用户在程序中自定义异常,然后由 Oracle 自动引发。
1.1 预定义异常
当 PL/SQL 程序违反了 Oracle 系统内部规定的设计规范时,就会自动引发一个预定义的异常。例如,当除数为零时,就会引发“ZERO_DIVIDE”异常。Oracle 系统常见的预定义异常及其说明如下表:
错误代码 | 命名的系统异常 | 产生原因 |
ORA-6530 | ACCESS_INTO_NULL | 未定义对象 |
ORA-6531 | COLLECTION_IS_NULL | 集合元素未初始化 |
ORA-6511 | CURSER_ALREADY_OPEN | 游标已经打开 |
ORA-0001 | DUP_VAL_ON_INDEX | 唯一索引对应的列上有重复的值 |
ORA-1001 | INVALID_CURSOR | 在不合法的游标上进行操作 |
ORA-1722 | INVALID_NUMBER | 内嵌的SQL语句不能将字符转换为数字 |
ORA-1403 | NO_DATA_FOUND | 使用select into未返回行,或应用索引表未初始化的元素时 |
ORA-1422 | TOO_MANY_ROWS | 执行select into时,结果集超过一行 |
ORA-1476 | ZERO_DIVIDE | 除数为0 |
ORA-6533 | SUBSCRIPT_BEYOND_COUNT | 元素下标超过嵌套表或VARRAY的最大值 |
ORA-6532 | SUBSCRIPT_OUTSIDE_LIMIT | 使用嵌套表或VARRAY时,将下标指定为负数 |
ORA-6502 | VALUE_ERROR | 赋值时,变量长度不足以容纳实际数据 |
ORA-1017 | LOGIN_DENIED | PL/SQL应用程序连接到oracle数据库时,提供了不正确的用户名或密码 |
ORA-1012 | NOT_LOGGED_ON | PL/SQL应用程序在没有连接oralce数据库的情况下访问数据 |
ORA-6501 | PROGRAM_ERROR | PL/SQL内部问题,可能需要重装数据字典&PL/SQL系统包 |
ORA-6504 | ROWTYPE_MISMATCH | 宿主游标变量与PL/SQL游标变量的返回类型不兼容 |
ORA-6500 | STORAGE_ERROR | 运行PL/SQL时,超出内存空间 |
ORA-0051 | TIMEOUT_ON_RESOURCE | Oracle在等待资源时超时 |
示例练习1:程序尝试除以0,从而引发系统异常。
declare
v_i int := 12;
begin
v_i := v_i / 0;
dbms_output.put_line(v_i);
end;
思考:遇到这样的异常,如何处理?
declare
v_i int := 12;
begin
v_i := v_i / 0;
dbms_output.put_line(v_i);
exception
when zero_divide then
dbms_output.put_line('除数不能为0');
end;
输出结果:“除数不能为0”。
1.2 自定义异常
Oracle 系统内部的预定义异常仅仅 20 个左右,而实际程序过程中可能会产生几千个异常情况,为此 Oracle 经常使用错误编号和相关描述输出异常信息。另外,程序设计人员可能会根据实际的业务需求定义一些特殊异常,这样 Oracle 的自定义异常就可以分为错误代码异常和业务逻辑异常两种。
1.2.1 错误编号异常(非预定义异常)
错误编号异常,通常称为非预定义异常,是指在 Oracle 系统发生错误时,系统会显示错误编号和相关描述信息的异常。虽然可以直接使用错误编号来处理异常,但错误编号为一串数字,较为抽象,不易于记忆和理解。于是用户可以首先在 PL/SQL 块的声明部分( declare 部分)使用 exception 类型定义一个异常变量名,然后使用语句pragma exception_init 为“错误编号”关联这个异常变量名。 具体步骤如下:
1. 在 PL/SQL 块的定义部分定义异常情况:
<异常情况名> EXCEPTION;
2. 将其定义好的异常情况,与标准的 ORACLE 错误联系起来,使用 EXCEPTION_INIT
语句:
PRAGMA EXCEPTION_INIT(<异常情况名>, <错误代码>);
3. 在 PL/SQL 块的异常情况处理部分对异常情况做出相应的处理。
示例练习2:非预定义异常的练习
要求:在 scott 模式下,向 emp 表中插入一行新记录,新记录员工编号与表中已有员工 编号重复,引起“违反唯一条件约束”异常。
实现步骤:
第一步:使用Scott用户登录PL/SQL Developer,新建SQL窗口,并使用“select * from emp; ”语句,查看一下emp表的基本信息。
第二步:在插入数据的时候,使用异常处理。
第三步:查看输出结果。
总结:异常处理部分一般放在PL/SQL程序体的后半部
结构为:
exception
when 异常1 then 执行语句1 ;
when 异常2 then 执行语句2 ;
……
when others then 执行语句 ;
end;
说明:异常处理可以按任意次序排列,但 others 必须放在最后。
示例代码:
declare
e_exception exception;
pragma exception_init(e_exception,-0001);
begin
insert into emp values(7369,'zhangsan','boss',7839,sysdate,10000,500,20);
exception
when e_exception then
dbms_output.put_line('编号不能重复');
end;
1.2.1 业务逻辑异常
在实际的应用中,例如,自己的程序会要求用户输入学生的 ID 值。然后,这个值被赋予程序后面会使用的变量 v_id。通常,希望学生 ID 的值是正值。但可能,用户在操作时输入了一个负数。在编写的程序逻辑中这种情况是不被允许的。由于变量 v_id 被定义为数值类型,而且负数也是一个合法数值,所以程序不会给出任何错误信息。因此,开发人员需要根据这一具体的业务逻辑规则自定义一个业务逻辑异常来防止该错误的发生并提示用户。
无论是预定义异常,还是错误编号异常,都是 Oracle 系统判断的错误,但业务逻辑异常是 Oracle 系统本身无法知道的,这样就需要有一个引发异常的机制,引发业务逻辑异常通常使用 raise 语句来实现。当引发一个异常时,控制就会转到exception 异常处理部分执行异常处理语句。业务逻辑异常首先在 declare 部分使用 exception 类型声明一个异常变量,然后在 begin 部分根据一定的业务逻辑规则执行 raise 语句(在 raise 关键字后面跟着异常变量名),最后在 exception 部分编写异常处理语句。
具体步骤如下:
(1)在 PL/SQL 块的定义部分定义异常情况:
<异常情况名> EXCEPTION;
(2)RAISE <异常情况>;
(3)在 PL/SQL 块的异常情况处理部分对异常情况做出相应的处理。
示例练习3:业务逻辑异常练习
要求:提示用户输入年龄,默认合法年龄是0—130之间,如果用户输入的数字超出范围,则触发异常
示例完整代码:
--业务逻辑异常处理
declare
v_age number(3,0) := &age ;--将用户输入的数字,存储在变量v_age中
ageSmall_exception exception ;--声明一个异常,当用户输入数字过小时,触发
ageLarge_exception exception ;--声明一个异常,当用户输入数字过大时,触发
begin
if (v_age < 0 ) then
raise ageSmall_exception;--raise关键字,触发对应的异常
elsif (v_age > 130) then
raise ageLarge_exception;
end if;
dbms_output.put_line('您输入的年龄是:' || v_age);
exception
when ageSmall_exception then
dbms_output.put_line('年龄不能小于0');--当ageSmall_exception异常被触发时,执行语句
when ageLarge_exception then
dbms_output.put_line('年龄不能大于130');
end;
示例练习4:业务逻辑异常练习—更新emp表中指定员工的工资
要求:在scott模式下,将用户输入的员工编号,工资增加100
--2、提示用户输入员工编号,并给该员工的工资增加100元
declare
v_empno emp.empno%type := &empno ;--将用户输入的员工编号,存储在变量v_empno中
no_result exception;--声明异常
begin
update emp set sal = sal+100 where empno = v_empno ;--更新emp表中对应员工编号的工资
if sql%notfound then
raise no_result;
end if;
exception
when no_result then
dbms_output.put_line('数据更新语句失败');
when others then
dbms_output.put_line('发生其他错误');
end;
代码分析:sql%notfound 是隐式游标 SQL 的 notfound 属性, 返回一个布尔值,表示与它最近的一条 SQL 语句(update,insert,delete,select)是否得到结果。当最近的一条sql语句没有涉及任何行的时候,则返回true。否则返回false。这样的语句在实际应用中,是非常有用的。例如要update一行数据时,如果没有找到,就可以作相应操作。
2、 异常作用范围
通过一个案例来探讨异常处理的作用范围。
--异常作用范围理解,案例1
declare
v_test char(3) ;--声明一个变量v_test,数据类型是char,长度是3
begin
<<inner_block>>--内层语句块
begin
v_test := 1234 ;
dbms_output.put_line('这只是一个测试');
exception
when invalid_number or value_error then
dbms_output.put_line('内层捕获错误');
end inner_block;
exception
when invalid_number or value_error then
dbms_output.put_line('外层捕获错误');
end;
示例中,我们使用了嵌套语句块结构。在内层块中,我们把数字1234赋值给字符型变量从而产生错误(超出允许长度)。于是该错误被内层块中异常处理语句捕捉并处理。
执行结果:
对上一个例子我们稍作改动,如果内层语句块中没有异常处理语句,并且错误发生在内层语句块,那么该错误由谁来处理呢?
--异常作用范围理解,案例2
declare
v_test char(3) ;--声明一个变量v_test,数据类型是char,长度是3
begin
<<inner_block>>
begin
v_test := 1234;
dbms_output.put_line('这只是一个测试');
end inner_block;--内层没有异常处理语句,直接结束内层语句块
exception
when invalid_number or value_error then
dbms_output.put_line('外层捕获错误');
end;
执行结果:
示例中,由于内层语句块出现错误,但是内层语句块没有异常处理语句,所以,该错误被外层语句块的异常处理语句捕获,并处理。
总结:如果在语句块中定义一个异常处理语句,那么该异常处理语句只捕获处理当前所在的语句块中出现的错误。在嵌套语句块结构中,内层语句块无法捕捉的错误,可以被外层语句块的异常处理语句捕捉并处理。
3、 异常传播
当 PL/SQL 语句块的可执行部分出现某个运行时错误时,会抛出不同类型的异常。但是,运行时错误也可能发生在语句块的声明部分或者异常处理部分。控制在这些环境下异常抛出方式的规则被称为异常传播。
3.1 声明部分异常
通过案例来理解,当语句块的声明部分发生运行时错误时,异常是如何传播的。
(1)例如:声明一个变量,给其赋一个错误值。
--案例1、声明部分异常
declare
v_test char(3) := 'ABCDE' ;--声明一个长度是3的字符型变量,同时赋值,赋值超出长度
begin
dbms_output.put_line('这只是一个测试');
exception
when invalid_number or value_error then
dbms_output.put_line('异常发生');
end ;
输出结果:
执行结果中,并没有输出‘异常发生’,而是系统提示出错。
语法说明:语句块中定义了异常处理语句,可当声明部分出现异常时,并没有触发其本身定义的异常处理语句,而是交给了系统处理。我们可以下一个结论:当 PL/SQL 语句块的声明部分出现运行时错误时,该语句块的异常处理部分不能捕获此项错误。
(2)将上个例子稍作修改:
本示例在上个示例语句块的外层嵌套了一个外层语句块,并定义了 EXCEPTION 结构。 于是本示例的执行结果为:
因此,我们可以得出结论:当内部语句块的声明部分发生运行时错误时,该异常会立即传播到外部语句块。
3.2异常处理部分异常
通过分析知道,当声明部分出现错误时,与声明部分同级的异常处理部分不会处理该错误,而是将该错误传播到外层语句块的异常处理部分处理。那么如果异常处理部分内部又产生了错误,那么该错误又该如何处理呢?
我们来看个例子:
--案例2、异常处理部分异常
declare
v_test char(3) := 'ABC' ;
begin
v_test := 1234 ;--把数字赋值给字符型变量,且超出长度
dbms_output.put_line('这只是一个测试');
exception
when invalid_number or value_error then
v_test := 'ABCDE' ; --赋值超出长度
dbms_output.put_line('异常发生');
end;
执行结果中,并没有输出‘异常发生’,而是系统提示出错。
案例分析:在执行部分,将 1234 赋值给一个字符变量,会出现错误,于是被本语句块中的 EXCEPTION 部分捕捉,可是进行异常处理时,将‘ABCDE’给变量赋值,超出了变量类型的范围,又产生了错误。最终结果,异常处理部分并没有同时处理这两次错误,而是抛给了系统处理。我们可以下一个结论:当 PL/SQL 语句块的异常处理部分发生运行时错误时,该语句块的异常处理部分不能防止这个错误。
如果我们使用嵌套 PL/SQL 语句块:
输出结果:
本案例完整代码:
--案例2-1、异常处理部分异常(嵌套)
begin
<<inner_block>>
declare
v_test char(3) := 'ABC' ;
begin
v_test := 1234 ;--把数字赋值给字符型变量,且超出长度
dbms_output.put_line('这只是一个测试');
exception
when invalid_number or value_error then
v_test := 'ABCDE' ; --赋值超出长度
dbms_output.put_line('异常发生--内层错误被捕获');
end inner_block;
exception
when invalid_number or value_error then
dbms_output.put_line('异常发生--外层错误被捕获');
end;
通过本示例,我们可以得出结论:当内部语句块的异常处理部分发生运行时错误时,该异常会立即传播到外部语句块。
3.3显式抛出异常
前面两种异常处理方式属于隐式抛出异常,即程序自动向外层抛出异常。那么也可以根据需要,使用 RAISE 语句,人为地控制异常的抛出,我们称之为显式抛出异常。
例如:
--案例3、显示抛出异常
declare
e_exception1 exception;
e_exception2 exception;
begin
<<inner_block>>
begin
raise e_exception1;
exception
when e_exception1 then
raise e_exception2;
when e_exception2 then
dbms_output.put_line('内层捕获e_exception2错误');
end inner_block;
exception
when e_exception2 then
dbms_output.put_line('外层捕获e_exception2错误');
end;
输出结果:
示例中,声明了两个异常:e_exception1 和 e_exception2 。在内部语句块中,使用 RAISE 语句抛出 e_exception1 异常。在该语句块中异常处理部分,异常 e_exception1 会尝试抛出 e_exception2 异常。尽管内部语句块中存在异常 e_exception2 的异常处理程序,但是执行 权却转到了外层语句块。之所以发生这种情况,原因在于,异常处理部分一次只会处理其中一个异常,不能一次处理多个异常。
3.4再次抛出异常
在有些情况下,当发生特定类型的错误时,也许希望内部语句块异常处理语句对该错误不做任何处理,而是希望将错误抛给外层语句块去处理,那么这个过程,我们称之为再次抛出异常。
例如:
--案例4、再次抛出异常
declare
e_exception exception;
begin
<<inner_block>>
begin
raise e_exception;
exception
when e_exception then
raise;
end inner_block;
exception
when e_exception then
dbms_output.put_line('捕获再次抛出的异常');
end;
输出结果:
在这个示例中,异常 e_execption 被声明在外部语句块中。然后,在内部语句块中抛出它。这样的话,执行权会转到内部语句块的异常处理部分。该语句块的异常处理部分的 RAISE 语句会导致该异常传播到外部语句块的异常处理部分。
==========这里是结束分割线=============