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块中。
如果调用过程时引发了异常,便可以进行处理。
修改后的代码如下(这里去掉了第二个过程):

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
WHERE deptno=d_no;
UPDATE emp SET sal=sal + amount WHERE deptno = d_no;
END;
BEGIN
increase_salary(100,100.50);
EXCEPTION
WHEN NO_DATA_FOUND then
dbms_output.put_line ('这是在过程中产生的异常');
END;

从上述执行结果可以看出,从子程序中传递到主程序的异常确实可以在主程序中进行处理。
但是在主程序中也可能产生同样的异常,这时如果输出同样的信息就不合适了。
从上面的例子可以看出,在主程序、各个子程序之中可能会因为不同的原因引发同一个异常。
为了方便地确定异常产生的原因,应该在PL/SQL程序的每部分都定义异常处理程序。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值