PL/SQL开发那点事----->PL/SQL开发过程中异常处理
用户编写的PL/SQL块在执行过程中不可避免地要发生一些错误。
这里涉及的错误并不是由于程序的语法错误引起的,而是因为处理的数据超出了处理的范围而引发的错误。
如果给这样的错误起一个名字,这就是异常。
当PL/SQL块在执行过程中检测到一个错误时,就会抛出相应的异常。
在块中应当处理这样的异常,否则会引起应用程序运行停止。
异常处理程序
异常一般是在PL/SQL程序执行错误时由数据库服务器抛出,也可以在PL/SQL块中由程序员在一定的条件下显式抛出。
无论是哪种形式的异常,都可以在PL/SQL块的异常处理部分编写一段程序进行处理,如果不做任何处理,异常将被传递到调用者,由调用者统一处理。
下面表示两种不同异常的处理方式:
方式一(异常处理):
DECLARE
定义异常
BEGIN
抛出异常
EXCEPTION
捕获并处理异常
END;
方式二(异常传递):
DECLARE
定义异常
BEGIN
抛出异常
EXCEPTION
不处理异常
END;
异常被传递到调用者
如果要在PL/SQL块中对异常进行处理,就需要在异常处理部分编写处理程序。
异常处理程序的形式如下:
EXCEPTION
WHEN 异常1 OR 异常2 THEN
异常处理程序1;
WHEN 异常3 OR 异常4 THEN
异常处理程序2;
WHEN OTHERS THEN
异常处理程序n;
END;
异常处理程序以关键字EXCEPTION开始,结束于关键字END 。
在这部分可以对多个异常分别进行不同的处理,也可以进行相同的处理。
如果没有列出所有异常,可以用关键字OTHERS代替其他的异常,在异常处理程序的最后加上一条WHEN OTHERS子句,用来处理前面没有列出的所有异常。
如果PL/SQL块执行出错,或者遇到显式抛出异常的语句,则程序立即停止执行,转去执行异常处理程序。
异常被处理结束后,整个PL/SQL块的执行便告结束。
所以一旦发生异常,则在PL/SQL块的可执行部分中,从发生异常的地方开始,以后的代码将不再执行。
在PL/SQL块中有三种类型的异常,即预定义的异常、非预定义的异常和用户自定义的异常。
下面分别介绍这几种异常的使用方法。
预定义的异常
Oracle把一些常见的错误定义为有名字的异常,这就是预定义的异常。
Oracle有许多预定义的异常,在进行处理时不需要再定义,只需要编写相应的异常处理程序即可。
当PL/SQL块执行发生错误时,数据库服务器将自动抛出相应的异常,并执行编写的异常处理程序。
下面列出了部分预定义的异常。
NO_DATA_FOUND 用SELECT命令检索数据时,没有发现满足要求的数据
TOO_MANY_ROWS 用SELECT命令检索数据时,得到了多行数据
DUP_VAL_ON_INDEX 在主键列上写入一个重复的值
CURSOR_ALREADY_OPEN 操作游标时,试图打开一个已经打开的游标
INVALID_NUMBER 将字符串转换为数字时,字符串不是数字型的字符串
LOGIN_DENIED 当连接Oracle数据库时被拒绝,可能是因为没有权限
NOT_LOGGED_ON 在没有登录数据库的情况下试图对数据库进行访问
ZERO_DIVIDE 在进行算术运算时,0作为除数
PROGRAM_ERROR PL/SQL块在运行时发生了内部错误
INVALID_CURSOR 视图操作一个无效的游标
VALUE_ERROR 在进行数据运算时发生错误
其中前面的两个异常是最常见的异常。
下面的代码演示如何处理这两个异常。
DECLARE
name emp.ename%type;
BEGIN
SELECT ename INTO name FROM emp WHERE deptno=100; --其实没有编号为100 的部门
EXCEPTION --这里将引发NO_DATA_FOUND异常
WHEN NO_DATA_FOUND THEN
dbms_output.put_line (‘没有满足条件的数据‘);
WHEN TOO_MANY_ROWS THEN
dbms_output.put_line(‘太多的数据‘);
END;
因为编号为100 的部门不存在,所以PL/SQL程序在执行到这条SELECT语句时引发了NO_DATA_FOUND异常。
但是如果对一个存在的部门进行查询,可能返回多行数据。
例如,如果将where子句的条件改为“ deptno= 10”,因为部门10高多个员工,这时将返回多行数据,
从而引发TOO_MANY_ROWS异常。
由此可见,在PL/SQL程序通过传统的SELECT命令只能查询一行数据,如果查询0行或多行数据,都会引发异常。
如果要对0行或多行数据的情况进行处理,就要用到游标了。
在向表的主键列上写入一个重复的值时将引发异常DUP_VAL_ON_INDEX 。
例如,在部门表中列deptno是主键列,这就要求这个列上的值不能重复。
如果已经存在部门10 ,再向这个表插入一行数据,部门编号也为10 ,这时将引发异常DUP_VAL ON_INDEX。
例如:
BEGIN
INSERT INTO dept VALUES(10 , ‘network‘, ‘nowhere‘);
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
dbms_output.put_line (‘主键列上的值重复‘);
END;
在PL/SQL块的异常处理部分,由WHEN 引导的代码即为异常处理程序。
一般在一个PL/SQL块中有多个异常处理程序,分别用于处理不同的异常。
但是一般只可能执行其中一段异常处理程序,因为当发生一个异常时, PL/SQL块的执行立即从可执行部分转入异常处理部分,当处理完异常后PL/SQL块的执行便宣告结束,这时将不会有别的异常出现。
一般针对一个异常可以编写一段单独的异常处理程序,也可以对多个异常编写同一段异常处理程序,如果发生不同的异常,可以进行同样的处理。
这样在WHEN子句中可以指定多个异常的名字,相互之间用OR分隔。
WHEN NO_DATA_FOUND OR TOO_MANY_ROWS THEN
dbms_output.put_line(‘SELECT语句出错‘);
...
PL/SQL提供了两个函数, SQLCODE用于返回发生的错误的代码, SQLERRM用于返回错误的原因。
有了这两个函数,就可以编写通用的异常处理程序,处理所有的异常。
例如,将错误代码和错误信息显示给用户。
例如:
BEGIN
INSERT INTO dept VALUES(10, ‘network‘, ‘nowhere‘);
EXCEPTION
WHEN others THEN
dbms_output.put_line (‘错误代码:‘ || SQLCODE) ;
dbms_output.put_line (‘错误原因:‘ || SQLERRM) ;
END;
在上面的异常处理的例子中,我们仅仅把发生错误的信息显示出来。
如果希望把所有发生的错误记录下来,可以创建一个表,在PL/SQL的异常处理部分把错误的情况写入这个表,生成日志信息。
例如,在数据库中创建表err_ info ,它的结构如下:
列名 类型 为空 描述
err_time DATE NO 错误发生的时间
err_user VARCHAR(30) YES 因执行PL/SQL而引发错误的用户
err_code INTEGER YES 错误代码
err_message VARCHAR( 100) YES 错误原因
这样在处理异常时,就可以直接将异常的情况写入这个表,而不用显示给用户了。
如果要对所有的异常进行相同的处理,那么在异常处理部分就不需要分别列出每个异常,只要用OTHERS代替就可以了。
例如:
DECLARE
name emp.ename%type;
err_code integer;
err_message varchar(100);
BEGIN
SELECT ename INTO name FROM emp WHERE deptno=100;
EXCEPTION --这里将引发NO_DATA_FOUND异常
WHEN OTHERS THEN
err_code:=SQLCODE;
err_message:=SQLERRM;
INSERT INTO err_info VALUES(SYSDATE,USER, err_code, err_message);
COMMIT;
END;
这个块的执行结果是将异常的信息记录在表error_info 中。
引发异常的一个重要原因是处理数时发生错误。
统计表明, SELECT语句、DML语句以及游标操作语句更容易引发异常。
编写PL/SQL块的主要目的是处理数据,而PL/SQL块在逻辑上与数据是分开的,程序员根本无法预料数据的变化。
例如,要查询部门10的员工,程序员根本不知道这个部门中有没有员工,有一个还是有多个员工。
所以在编写程序时,程序员应该考虑各种可能出现的异常,在程序中编写这些异常的处理代码,这样的程序才能经受各种错误的考验。
非预定义异常
在PL/SQL 中还有一类会经常遇到的错误。
每个错误都有相应的错误代码和错误原因,但是由于Oracle没有为这样的错误定义一个名称,因而不能直接进行异常处理。
在一般情况下,只能在PL/SQL块执行出错时查看其出错信息。
编写PL/SQL程序时,应该充分考虑到各种可能出现的异常,并且都作出适当的处理,这样的程序才是健壮的。
对于这类非预定义的异常,由于它也被自动抛出的,因而只需要定义一个异常,把这个异常的名称与错误的代码关联起来,然后就可以像处理预定义异常那样处理这样的异常了。
非预定义异常的处理过程如下所示。
定义异常 把异常与错误代码进行关联 处理异常
PL/SQL声明部分 PL/SQL异常处理部分
非预定义异常的处理过程
异常的定义在PL/SQL块的声明部分进行,定义的格式为:
异常名称 EXCEPTION
其中异常名称是用户自己定义的-个名字,此时它仅仅是一个符号,没有任何意义。
只有把这个名称与某个错误代码关联起来以后,这个异常才代表这个错误。
把异常的名称与错误代码进行关联的格式是:
PRAGMA EXCEPTION_INIT(异常名, 错误代码)
这种关联也是在PL/SQL块的声明部分进行。
这样这个异常的名字就代表这个特定的错误了,当PL/SQL程序在执行的过程中发生这个错误时,这个异常将被自动抛出,这时就可以对其进行处理了。
例如,错误代码-02292的含义是违反了关联完整性。
如果两个表通过主键和外键建立了关联关系,这时要从主表中删除一行,就可能会违反它们之间的关联完整性。
例如员工表emp 与部门表dept通过列deptno建立了关联关系,如果要从部门表dept删除一行,必须保证EMP表中没有这个部门的员工,否则就违反了它们之间的关联完整性。
下面的代码演示了试图删除部门10的数据时发生的错误,因为在表emp中还有部门10的员工。
delete from dept where deptno=10;
ORA-02292: 违反完整约束条件 (SCOTT.FK_DEPTNO) - 已找到子记录
如果这时定义一个异常,把这个异常与错误代码-02292关联起来,就可以在PL/SQL块的异常处理部分对其进行处理了。
下面是处理这个异常的代码。
DECLARE
reference_err EXCEPTION;
PRAGMA EXCEPTION_INIT(reference_err,-02292);
BEGIN
DELETE FROM dept WHERE deptno=10;
EXCEPTION
WHEN reference_err THEN
dbms_output.put_line(‘您所进行的操作违反了关联完整性‘);
END;
用户自定义的异常
除了Oracle定义的两种异常外,在PL/SQL中还可以自定义异常。
程序员可以把一些特定的状态定义为异常。
这样的异常一般由程序员自己决定,在一定的条件下抛出,然后利用PL/SQL的异常机制进行处理。
对于用户自定义的异常,有两种处理方法。
第一种方法是先定义一个异常,并在适当的时候抛出,然后在PL/SQL块的异常处理部分进行处理。
用户自定义的异常一般在一定的条件下抛出,于是这个条件就成为引发这个异常的原因。
第二种方法是向调用者返回一个自定义的错误代码和一条错误信息。
这里先介绍第一种方法。
异常的定义在PL/SQL块的声明部分进行,定义的格式为:
异常名称 EXCEPTION
异常名称这时仅仅是一个符号,仅当在一定条件下抛出时,这个异常才有意义。
抛出异常的命令是RAISE ,异常的抛出在PL/SQL块的可执行部分进行。
RAISE命令的格式为:
RAISE 异常名称
异常一般在一定的条件下抛出,因此RAISE语句通常跟在某个条件判断的后面,这样就把这个异常与这个条件关联起来了。
抛出异常的原因可能是数据出错,也可能是满足了某个自定义的条件,处理自定义异常的方法与处理前两种异常的方法相同。
例如,编写一个PL/SQL程序,求1+2+3 +… 100的值。
在求和的过程中如果发现结果超出了1000 ,则抛出异常,并停止求和。
这个块的代码如下:
DECLARE
out_of_range EXCEPTION; --定义异常
result integer := 0;
BEGIN
for i in 1 .. 100 loop
result := result + i;
if result > 1000 then
RAISE out_of_range; --抛出异常
END if;
END loop;
EXCEPTION
WHEN out_of_range THEN --处理异常
DBMS_OUTPUT.PUT_LINE(‘当前的计算结果为‘||result||‘,已超出范围‘);
END;
用RAISE命令不仅可以抛出一个自定义的异常,也可以抛出一个预定义异常和非预定义异常。
例如,在上面求和的例子中,当计算结果超过1000时可以抛出异常VALUE_ERROR 。
修改后的PL/SQL块代码如下:
DECLARE
result integer := 0;
BEGIN
for i in 1 .. 100 loop
result := result + i;
if result > 1000 then
RAISE value_error; --抛出预定义的异常
END if;
END loop;
EXCEPTION
WHEN value_error THEN --处理异常
DBMS_OUTPUT.PUT_LINE(‘当前的计算结果为‘||result||‘,已超出范围‘);
END;
现在再来介绍自定义异常处理的第二种方法。
当PL/SQL块的执行满足一定的条件时,可以向PL/SQL程序返回一个错误代码和一条错误信息。
错误代码的范围是-20000 到- 20999 ,这个范围的代码是Oracle保留的,本身没有任何意义。
程序如果把一个错误代码与某个条件关联起来,那么在条件满足时系统将引发这样的错误。
当然这是人为制造的一种错误,并不表示程序或数据真正出现了错误。
PL/SQL提供了一个过程,用于向PL/SQL程序返回一个错误代码和一条错误信息。
这个过程是RAISE_APPLICATION_ERROR ,过程的调用格式为:
RAISE_APPLICATION_ERROR(错误代码,错误信息)
例如,对上面求和的例子加以修改,当计算结果大于1000时, PL/SQL程序便得到一个错误代码- 20001 和一条错误信息。
修改后的代码如下:
DECLARE
result integer := 0;
BEGIN
for i in 1 .. 100 loop
result := result + i;
if result > 1000 then
RAISE_APPLICATION_ERROR(-20001, ‘当前的计算结果为‘||result||‘,已超出范围‘);
END if;
END loop;
END;
从程序运行的结果来看,程序的执行过程确实发生了错误,返回了指定的错误代码和错误信息。
在这一点上用户自定义的异常与非预定义异常是相似的。
只不过非预定义异常是由数据库服务器自动抛出的,并且错误代码和错误信息都是由数据库服务器指定的,而用户自定义的异常是由程序员抛出的,错误代码和错误信息都是由程序员指定的。
在处理非预定义异常时,我们为每个错误代码指定了一个异常名称,然后就可以根据这个名称进行异常处理。
既然用户自定义的异常也可以向调用者返回错误代码和错误信息,那么我们也可以采用同样的方法处理这样的异常。
首先定义一个异常,然后把这个异常与某个错误代码关联起来。
这两步都在PL/SQL块的声明部分进行。
然后在PL/SQL程序的可执行部分根据一定的条件,抛出这个异常。
最后在PL/SQL块的异常处理部分捕捉并处理这个命名的异常。
例如,用这种方住重新处理上述求和的例子中的异常,代码如下:
DECLARE
result integer := 0;
out_of_range EXCEPTION;
PRAGMA EXCEPTION_INIT(out_of_range, -20001);
BEGIN
for i in 1 .. 100 loop
result := result + i;
if result > 1000 then
RAISE_APPLICATION_ERROR(-20001, ‘当前的计算结果为‘||result||‘,已超出范围‘);
END if;
END loop;
EXCEPTION
WHEN out_of_range THEN
dbms_output.put_line(‘错误代码:‘||sqlcode);
dbms_output.put_line(‘错误信息:‘||sqlerrm);
END;
从上述PL/SQL块可以看出,我们首先在声明部分定义了一个异常out_of_range ,然后把这个异常与错误代码-20001关联起来,一旦程序在运行过程中发生了这个错误,就是抛出了异常out_of_range 。
在块的可执行部分,如果在累加的过程中变量result的值超过了1000 ,则返回错误代码- 20001 以及相应的错误信息。
这样在异常处理部分就可以捕捉并处理异常out_of_range 了。
在处理用户自定义的异常时,也可以使用函数SQLCODE和SQLERRM ,这两个函数分别用于返回指定的错误代码和错误信息。
从程序的运行结果可以看出,这两个函数确实返回了指定的错误代码和错误信息。
这样的错误代码和错误信息是在可执行部分通过过程RAISE APPLICATION ERROR指定的。
异常的传递
如果PL/SQL程序在执行的过程中发生了错误, 则转去执行相应的异常处理程序,然后结束块的执行。
如果没有定义相应的异常处理程序,那么PL/SQL程序将向调用者返回出错的相关信息,也就是把异常传递到程序的调用者,然后结束程序的执行。
如果这个程序是在SQL*Plus 中执行的,那么异常就会传递到SQL*Plus环境,从而把错误信息显示在屏幕上。
例如,下面的块在检索数据时引发了TOO_MANY _ROWS异常,并把异常传递到SQL*plus 中。
DECLARE
name emp.ename%type;
BEGIN
SELECT ename INTO name FROM emp WHERE deptno=10;
EXCEPTION --这里将引发TOO_MANY_ROWS 异常
when NO_DATA_FOUND then
dbms_output.put_line (‘没有满足条件的数据‘);
END;
从程序的执行结果可以看出,由于在程序中没有处理异常TOO_MANY_ROWS ,所以这个异常被传递到程序的调用者一-SQL*Plus 中。
在PL/SQL块中可以定义过程、函数等形式的子程序,在每个子程序中也可以分别定义异常处理程序。
这样当子程序执行出现错误时,就转去执行相应的异常处理程序。
然后子程序的执行便告结束, PL/SQL块接着从子程序调用处的下一条语句开始执行。
如果子程序对出现的异常进行了处理,就可以认为子程序的执行正常结束。
例如,再来考虑子程序重载的这个例子。
在这个块中定义了两个重载过程increase_salary ,用来对员工增加工资。
第一个过程有两个参数,分别是部门编号和增加的额度,用于对指定的部门的员工增加工资。
第二个过程带有一个参数,即增加的额度,用于对所有员工增加工资。
这里在第一个过程中添加了处理异常NO_DATA_FOUND的程序,还添加了一条SELECT语句。
如果在调用过程时指定了一个不存在的部门,那么在查询该部门信息时将引发NO_DATA_FOUND异常,这个过程的执行流程就会转到异常处理部分。
DECLARE
procedure increase_salary(d_no emp.deptno%type, amount float)
is
d_name dept.dname%type;
BEGIN
SELECT dname INTO d_name FROM dept --其实部门100 不存在
WHERE deptno=d_no;
UPDATE emp set sal=sal+amount WHERE deptno=d_no;
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line (‘这个部门不存在‘);
END;
procedure increase_salary(amount float)
is
BEGIN
UPDATE emp set sal=sal+amount;
END;
BEGIN
increase_salary(100,100.50); --调用第一个increase_salary过程
increase_salary(200); --调用第二个increase_salary过程
END;
从块的执行结果可以看出,当调用第一个重载过程时,因为传递了一个不存在的部门编号,所以引发了NO_DATA_FOUND异常。
这个过程在处理异常后便执行结束, PL/SQL块接着执行第二条调用语句,调用第二个过程。
第一个过程因为处理了出现的异常,所以可以认为是正常结束,它并不会影响块整个程序中其他语句的执行。
对异常的处理应当遵循“不扩散”的原则。
在子程序中发生的错误应该在子程序中进行处理,不要扩散到主程序中。
同样,在PL/SQL块的可执行部分出现的错误应该在块中进行处理,不要扩散到调用该块的SQL*Plus或应用程序中。
如果在子程序中没有处理出现的错误,情况会怎么样呢?
再来考虑上面的例子,取消了第一个increase_salary过程中的异常处理部分。
为了便于测试,在两条调用语句中间添加了一条输出语句。
修改后的代码如下:
DECLARE
procedure increase_salary(d_no emp.deptno%type, amount float)
is
d_name dept.dname%type;
BEGIN
SELECT dname INTO d_name FROM dept --其实部门100 不存在
WHERE deptno=d_no;
UPDATE emp set sal=sal+amount WHERE deptno=d_no;
END;
procedure increase_salary(amount float)
is
BEGIN
UPDATE emp set sal=sal+amount;
END;
BEGIN
increase_salary(100,100.50); --调用第一个increase_salary过程
dbms_output.put_line(‘第一个过程结束了‘);
increase_salary(200); --调用第二个increase_salary过程
END;
在调用第一个increase_salary过程时,由于指定了一个不存在的部门编号,所以引发了异常
NO_DATA_FOUND 。在子程序中没有处理这个异常,所以过程非正常结束。从程序的执行结
果可以看出,我们指定的输出并没有产生,可以断定,第一条调用语句以下的所有语句都没有
得到执行。
如果子程序没有处理出现的错误,那么异常就被传递到它的调用者,即PL/SQL 主程序,
从而在主程序中也会产生错误。所以主程序将在调用子程序的地方停止执行,而去处理这个异
常。但是因为主程序也没有定义异常处理程序,所以这个异常又被传递到块的调用者
SQL*Plus ,从而在屏幕上显示出错的信息。
从子程序中传递到PL/SQL主程序中的异常,能不能在主程序中进行处理呢?答案是肯定
的。如果在主程序中定义了异常处理程序,那么异常被从子程序传递到主程序中后,就像在主
程序中产生的异常一样进行处理。这样我们可以在主程序中编写统一的异常处理程序,无论异
常是在主程序中抛出的,还是在子程序中抛出的,都可以得到同样的处理。这种做陆虽然是可
行的,但是它不符合“不扩散”原则。如果程序出现了异常,不容易确定是什么地方出现了错
误,也无法对程序的不同部分产生的异常进行单独的处理。例如,把上述例子中第一个
increase_salary 过程的异常处理放在PL/SQL块中。如果调用过程时引发了异常,便可以进行处
理。