在编写 PL/SQL 程序时,避免不了会发生一些错误,可能是程序设计人员造成的,也可能是操作系统或硬件环境出错,比如出现除数为零、磁盘 I/O 错误等情况。对于出现的错误,Oracle 采用异常机制来处理,处理代码通常放在 PL/SQL 的EXCEPTION代码块中。根据异常产生的机制和原理,可将 Oracle 系统异常分为以下两大类:
(1)预定义异常:Oracle 系统自身为用户提供了大量的、可在 PL/SQL 中使用的预定义异常,以便检查用户代码失败的原因。他们都定义在 Oracle 的核心 PL/SQL 库中,用户可以在自己的 PL/SQL 异常处理部分使用名称对其进行标识。对这种异常情况的处理,用户无须再程序中定义,由 Oracle 自动引发。
(2)自定义异常:有时候可能会出现操作系统错误或机器硬件故障,这些错误 Oracle 系统自身无法知晓,也不能控制。例如,操作系统因病毒破坏而产生故障、磁盘村坏、网络突然中断等。另外,因业务的实际需求,程序设计人员需要自定义一些错误的业务逻辑,而PL/SQL 程序在运行过程中就可能会触发到这些错误的业务逻辑。那么,对于以上这些异常情况的处理,就需要用户在程序中自定义异常,然后由 Oracle 自动引发。
7.3.1预定义异常
当 PL/SQL 程序违反了 Oracle 系统内部规定的设计规范时,就会自动引发一个预定义的异常。例如,当除数为零时,就会引发ZERO_DIVIDE异常。Oracle 系统常见的预定义异常及其说明见表7-1。
表7-1 预定义异常
错误代码 命名的系统异常 产生原因
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在等待资源时超时
【例7-4】建立职工表,包括员工编号、姓名、薪水、电话号码,然后插入测试数据。写一个匿名块,当查询职工表数据出现异常情况时,提示异常信息。
具体代码如下:
第七章\ccgs.sql
create table SM_EMP
( empid varchar2(30),
name varchar2(30),
salary NUMBER(6,2),
telno CHAR(8)
);
—插入测试记录
INSERT INTO sm_emp VALUES(‘0000000001’,‘张飞飞’,5500,'6678562 ');
INSERT INTO sm_emp VALUES(‘0000000002’,‘关庭’,4500,‘87825626’);
INSERT INTO sm_emp VALUES(‘0000000003’,‘孙海’,6200,‘87783617’);
Commit;
-写匿名块,出现数据异常做提示
declare
v_emp sm_emp%rowtype;
begin
select * into v_emp from sm_emp; --该处会捕捉到异常,然后转到异常处理部分
exception
when too_many_rows then --too_many_rows 是Oracle预定义的异常名称,该异常的异常号为-01422
dbms_output.put_line(sqlcode||sqlerrm);
when others then
dbms_output.put_line(sqlcode||sqlerrm);
end;
执行该匿名块后,结果如图7-5所示。
图7-5显示异常信息
7.3.2自定义异常
Oracle 系统内部的预定义异常仅仅 20 个左右,而实际程序过程中可能会产生几千个异常情况,为此 Oracle 经常使用错误编号和相关描述输出异常信息。另外,程序设计人员可能会根据实际的业务需求定义一些特殊异常,这样 Oracle 的自定义异常就可以分为错误代码异常和业务逻辑异常两种。
在声明部分声明异常情况,语法如下:
异常情况(异常名) EXCEPTION;
通过raise语句触发,手动抛出异常。语法如下:
raise 异常情况(异常名);
在程序块的异常处理部分对异常做相应处理。异常抛出后没有在EXCEPTION中处理,会报用户自定义的异常错误未得到处理的错误。
异常处理的格式如下:
exception
when exception1 [or exception2…] then --异常列表
statement… --异常处理语句
when exception3 [or exception4…] then
statement…
when others then
statement…
Exception:表示定义异常块部分,是异常处理部分开始标志。
when :异常列表。
then :异常处理语句,也就是发生异常后执行的指定语句。
when others then:异常处理最后部分,如果抛出的异常和前面的都不匹配时,则执行的语句。
raise_application_error():抛出异常消息,可将异常传给客户端的应用程序。前台收到的消息包含异常代码和异常说明。包含三个参数:
第一个参数(error_number)自定义异常的代码。
第二个参数(error_message)为异常的说明,长度最大为2k字节,超过2k就截取。
第三个参数(keep_errors)为可选参数,true表示将新错误添加到已经引发的错误列表中,false(缺省)表示新错误替换当前的错误列表,即值为true时打印整个异常列表。
【例7-5】写一个匿名块,当更新职工表数据出现异常情况时,显示异常信息。
建立匿名块代码如下:
第七章\ccgs.sql
DECLARE
v_empno SM_EMP.empid%TYPE :=&empno;
no_result EXCEPTION;
BEGIN
UPDATE SM_EMP SET salary = salary+100 WHERE empid = v_empno;
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(SQLCODE||‘—’||SQLERRM);
END;
输入职工号001,如图7-6所示。
图7-6输入职工编号
执行该匿名块后,结果如图7-7所示。
图7-7更新语句失败
【例7-6】写一个函数,输入部门代码。出现异常情况时,显示异常信息。
建立函数,代码如下:
第七章\ccgs.sql
CREATE OR REPLACE FUNCTION get_salary(p_deptno NUMBER)
RETURN NUMBER
AS
v_sal NUMBER;
BEGIN
IF p_deptno IS NULL THEN
RAISE_APPLICATION_ERROR(-20991, ‘部门代码为空’);
ELSIF p_deptno<0 THEN
RAISE_APPLICATION_ERROR(-20992, ‘无效的部门代码’);
ELSE
SELECT SUM(salary) INTO v_sal FROM SM_EMP
WHERE SM_EMP.empid=p_deptno;
RETURN v_sal;
END IF;
End;
如果部门代码为空,则执行后的结果如图7-8所示。
图7-8部门代码为空
如果输入部门代码为-216,则执行后的结果如图7-9所示。
图7-9部门代码为空
7.3.3 业务逻辑异常
在实际的应用中,例如,自己的程序会要求用户输入学生的 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块的异常情况处理部分对异常情况做出相应的处理。
【例7-7】写一段 PL/SQL匿名块儿,提示用户输入年龄,默认合法年龄是0到130之间,如果用户输入的数字超出范围,则触发异常。
匿名块代码如下:
第七章\ccgs.sql
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;
输入年龄为25,如图7-10所示。
图7-10输入年龄为25
输出结果如图7-11所示。
图7-11输出结果
输入年龄为135,如图7-12所示。
图7-12输入年龄135
输出结果如图7-13所示。
图7-13年龄超过合理值提示错误信息
【例7-8】写一个匿名块,输入员工编号。根据输入的员工编号,更新员工工资,工资增加100。如果输入的员工编号编号不存在,提示错误。
建立职工表,然后插入测试记录,代码如下:
第七章\ccgs.sql
create table EMP
( empid varchar2(30),
name varchar2(30),
sal NUMBER(6,2),
telno CHAR(8)
);
INSERT INTO emp VALUES(‘0000000001’,‘张飞飞’,5500,'6678562 ');
INSERT INTO emp VALUES(‘0000000002’,‘关庭’,4500,‘87825626’);
INSERT INTO emp VALUES(‘0000000003’,‘孙海’,6200,‘87783617’);
Commit;
查询职工表,代码如下:
select * from emp t
查询表数据如图7-14所示。
图7-14查询职工表数据
匿名块代码如下:
declare
v_empno emp.empid%type := &empid ;–将用户输入的员工编号,存储在变量v_empno中
no_result exception;–声明异常
begin
update emp set sal = sal+100 where empid = v_empno ;–更新emp表中对应员工编号的工资
dbms_output.put_line(‘数据更新成功’);
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一行数据时,如果没有找到,就可以作相应操作。
输入年龄职工号0000000001,如图7-15所示。
图7-15输入正确的职工号
查询职工表数据,代码如下:
select * from emp t
执行后如图7-16所示。
图7-16查询表数据
输入年龄职工号788,如图7-17所示。
图7-17输入错误的职工号
执行匿名块后,如图7-18所示。
图7-18提示更新数据失败
7.3.4 异常作用范围
【例7-9】写一个匿名块,把数字1234赋值给字符型变量从而产生错误(超出允许长度),则触发异常。
匿名块代码如下:
第七章\ccgs.sql
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;
执行后如图7-19所示。
图7-19提示更新数据失败
示例中,使用了嵌套语句块结构。在内层块中,把数字1234赋值给字符型变量从而产生错误(超出允许长度)。于是该错误被内层块中异常处理语句捕捉并处理。执行结果:
【例7-10】对7-7例子稍作改动,如果内层语句块中没有异常处理语句,并且错误发生在内层语句块, 查看异常作用范围。
匿名块代码如下:
第七章\ccgs.sql
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;
执行后如图7-20所示。
图7-20提示外部错误
从执行结果可以看出:由于内层语句块出现错误,但是内层语句块没有异常处理语句,所以,该错误被外层语句块的异常处理语句捕获,并处理。
总结:如果在语句块中定义一个异常处理语句,那么该异常处理语句只捕获处理当前所在的语句块中出现的错误。在嵌套语句块结构中,内层语句块无法捕捉的错误,可以被外层语句块的异常处理语句捕捉并处理。
7.3.5 异常传播
当 PL/SQL 语句块的可执行部分出现某个运行时错误时,会抛出不同类型的异常。但是运行时错误也可能发生在语句块的声明部分或者异常处理部分。控制在这些环境下异常抛出方式的规则被称为异常传播。
通过例7-8来理解,当语句块的声明部分发生运行时错误时,异常是如何传播的。
【例7-11】写一个匿名块,声明一个变量v_tes,给其赋一个错误值。内层语句块中没有异常处理语句,并且错误发生在内层语句块,查看异常作用范围。
匿名块代码如下:
第七章\ccgs.sql
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 ;
执行后,如图7-21所示。
图7-21提示外部错误
执行结果中,并没有输出异常发生,而是系统提示出错。说明语句块中定义了异常处理语句,可当声明部分出现异常时,并没有触发其本身定义的异常处理语句,而是交给了系统处理。结论:当 PL/SQL 语句块的声明部分出现运行时错误时,该语句块的异常处理部分不能捕获此项错误。