本章讲解如何处理 pl/sql 编译时警告和 pl/sql 运行时错误。后者被称为异常。
异常(pl/sql 运行时错误)可能由于设计缺陷、编码错误、硬件故障等多种原因而引起。 您无法预见所有可能的异常,但可以编写异常处理程序,使您的程序能够在出现异常时继续运行。
任何 pl/sql 块都可以有异常处理部分,它可以有一个或多个异常处理程序。例如, 异常处理部分可以具有以下语法:
EXCEPTION WHEN ex_name_1 THEN statements_1 -- 异常处理程序 WHEN ex_name_2 OR ex_name_3 THEN statements_2 -- 异常处理程序 WHEN OTHERS THEN statements_3 -- 异常处理程序 END;
当块的可执行部分中引发异常时,可执行部分停止并将控件转移到异常处理部分。 如果引发 ex_name_1,则运行 statements_1。如果引发 ex_name_2 或 ex_name_3, 则运行 statements_2。如果引发其他异常,则运行 statements_3。
在异常处理程序运行后,控制转移到封闭块的下一个语句。 如果没有封闭块,则:
如果异常处理程序在子程序中,则控制返回到调用者,在调用语句之后
如果异常处理程序在匿名块中,则控制转移到主机环境
异常类别包括:
内部定义
运行时系统隐式(自动)引发内部定义的异常
预定义
预定义异常是内部定义的异常,由 pl/sql 赋予名称
用户定义
您可以在任何 pl/sql 匿名块、子程序或包的声明部分中声明自己的异常
使用异常处理程序进行错误处理使程序更易编写和理解,并减少未处理异常的可能性。 如果没有异常处理程序,您必须在可能发生错误的每个地方检查每个可能的错误,然后处理它。 特别是如果错误不能立即检测到,很容易忽略可能的错误或可能发生错误的地方。 错误处理代码分散在整个程序中。 有了异常处理程序,您不需要知道每个可能的错误或可能发生错误的地方。 您只需要在可能发生错误的每个块中包含一个异常处理部分。 在异常处理部分中,您可以包括针对特定和未知错误的异常处理程序。 如果块中的任何位置(包括子块内部)发生错误,则异常处理程序将处理它。 错误处理代码隔离在块的异常处理部分中。
目前 pl/sql 支持预定义错误和用户定义错误。
预定义异常是具有预定义名称的内部定义异常。运行时系统隐式(自动)引发预定义异常。 由于预定义异常具有名称,因此可以专门为它们编写异常处理程序。
目前支持 7 种预定义错误:
INVALID_CURSOR
CASE_NOT_FOUND
DUP_VAL_ON_INDEX
NOT_DATA_FOUND
TOO_MANY_ROWS
ZERO_DIVIDE
LOGIN_DENIED
INVALID_CURSOR 主要针对两种情况:显式游标和游标变量。
使用 CLOSE 语句关闭打开的显式游标,从而允许重用其资源。 关闭游标后,不能从其结果集中获取记录或引用其属性。 如果尝试这样做,则 pl/sql 引发预定义的异常 INVALID_CURSOR。
如果显式游标未打开,则除 %ISOPEN 之外的任何属性引用都会引发预定义的异常 INVALID_CURSOR。
例如,关闭游标变量后,不能从其结果集中获取记录或引用其属性。
--preconditional
create table employees(id int, name varchar2(100), job varchar2(100));
insert into employees values (22091401, 'sunli', 'actress');
insert into employees values (22091402, 'dengchao', 'actor');
insert into employees values (22091403, 'lishizhen', 'doctor');
insert into employees values (22091404, 'zhangsanfeng', 'martial artist');
create or replace function invalid_cursor_f return int as
cursor c1 is select * from employees;
emp_cur employees%rowtype;
begin
open c1;
loop
fetch c1 into emp_cur;
exit when c1%notfound;
close c1;
end loop;
close c1;
EXCEPTION
WHEN invalid_cursor THEN
DBMS_OUTPUT.PUT_LINE('invalid cursor');
return 1;
end;
/
select invalid_cursor_f();
例如,如果显式游标未打开,则除 %ISOPEN 之外的任何属性引用都会引发预定义的异常 INVALID_CURSOR。
create or replace function invalid_cursor_f return int as
cursor c1 is select * from employees;
emp_cur employees%rowtype;
ora_found boolean := false;
begin
ora_found := c1%found;
EXCEPTION
WHEN invalid_cursor THEN
DBMS_OUTPUT.PUT_LINE('invalid cursor');
return 1;
end;
/
select invalid_cursor_f();
使用 pl/sql 进行错误处理,且 lightdb_dblevel_syntax_compatible_type 的值为 oracle。
当你不再需要一个游标变量时,用CLOSE语句关闭它,从而允许其资源被重用。在关闭游标变量之后,你不能从其结果集中获取记录或引用其属性。如果你尝试这样做,pl/sql会引发预定义的异常INVALID_CURSOR。
游标变量具有与显式游标相同的属性。游标变量属性的值语法为cursor_variable_name紧接着attribute(例如,cv%ISOPEN)。如果一个游标变量没有打开,引用除%ISOPEN以外的任何属性都会引发预定义的异常INVALID_CURSOR。
例如,在关闭游标变量之后,你不能从其结果集中获取记录或引用其属性。
create or replace function invalid_cursor_f return int as
c1 sys_refcursor;
emp_cur employees%rowtype;
begin
open c1 for select * from employees;
loop
fetch c1 into emp_cur;
exit when c1%notfound;
close c1;
end loop;
close c1;
EXCEPTION
WHEN invalid_cursor THEN
DBMS_OUTPUT.PUT_LINE('invalid cursor');
return 1;
end;
/
select invalid_cursor_f();
例如,如果一个游标变量没有打开,引用除%ISOPEN以外的任何属性都会引发预定义的异常INVALID_CURSOR。
create or replace function invalid_cursor_f return int as
c1 sys_refcursor;
emp_cur employees%rowtype;
ora_isopen boolean := false;
begin
ora_isopen := c1%isopen;
return 1;
exception
when INVALID_CURSOR then
dbms_output.put_line('this is a normal error');
end;
/
select invalid_cursor_f();
case语句,如果when子句中没有包含必要的条件分支,就会触发CASE_NOT_FOUND异常。简单情况下有simple_case_statement,搜索情况下有search_case_statement。
例如,简单情况下的simple_case_statement。
create or replace function case_not_found_f return int as
grade CHAR(1);
BEGIN
grade := 'G';
CASE grade
WHEN 'A' THEN DBMS_OUTPUT.PUT_LINE('Excellent');
WHEN 'B' THEN DBMS_OUTPUT.PUT_LINE('Very Good');
WHEN 'C' THEN DBMS_OUTPUT.PUT_LINE('Good');
WHEN 'D' THEN DBMS_OUTPUT.PUT_LINE('Fair');
WHEN 'F' THEN DBMS_OUTPUT.PUT_LINE('Poor');
END CASE;
EXCEPTION
WHEN CASE_NOT_FOUND THEN
DBMS_OUTPUT.PUT_LINE('No such grade');
return 1;
END;
/
select case_not_found_f();
例如,搜索情况下的search_case_statement。
create or replace function case_not_found_f return int as
grade CHAR(1);
BEGIN
grade := 'G';
CASE
WHEN grade = 'A' THEN DBMS_OUTPUT.PUT_LINE('Excellent');
WHEN grade = 'B' THEN DBMS_OUTPUT.PUT_LINE('Very Good');
WHEN grade = 'C' THEN DBMS_OUTPUT.PUT_LINE('Good');
WHEN grade = 'D' THEN DBMS_OUTPUT.PUT_LINE('Fair');
WHEN grade = 'F' THEN DBMS_OUTPUT.PUT_LINE('Poor');
END CASE;
EXCEPTION
WHEN CASE_NOT_FOUND THEN
DBMS_OUTPUT.PUT_LINE('No such grade');
return 1;
END;
/
select case_not_found_f();
当向唯一索引对应的列插入重复值时触发。
例如:
--preconditional
CREATE TABLE emp_name AS SELECT id, name FROM employees;
CREATE UNIQUE INDEX empname_ix ON emp_name (id);
create or replace procedure dup_val_on_index_p as
emp_id NUMBER(10);
emp_name VARCHAR2(100);
BEGIN
SELECT id, name INTO emp_id, emp_name FROM employees WHERE id = 22091401;
INSERT INTO emp_name (id, name) VALUES (emp_id, emp_name);
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('Inserts were rolled back');
END;
/
call dup_val_on_index_p();
当执行select into时没有返回任何行时触发。
例如:
-- no_data_found
-- select into
-- number of record selected is zero
create or replace function no_data_found_f return int as
v_emp int;
begin
select id into v_emp from employees where id = 22091501;
EXCEPTION
WHEN no_data_found THEN
DBMS_OUTPUT.PUT_LINE('No data found');
return 1;
end;
/
select no_data_found_f();
当执行select into语句时,如果返回超过一行,则会触发TOO_MANY_ROWS异常。
例如:
create or replace function too_many_rows_f return int as
v_emp int;
begin
select id into v_emp from employees;
EXCEPTION
WHEN no_data_found THEN
DBMS_OUTPUT.PUT_LINE('No data found');
WHEN too_many_rows THEN
DBMS_OUTPUT.PUT_LINE('Too Many Rows');
return 1;
end;
/
select too_many_rows_f();
你可以使用raise来抛出预定义的异常,并在异常后处理问题。(在pl/sql中不会触发,但在LightDB中会触发)
例如:
create or replace function login_denied_f return int as
begin
raise login_denied;
exception
when login_denied then
dbms_output.put_line('this is a login_denied message.. good luck the next time');
return 1;
end;
/
select login_denied_f();
当除数为0时,被除数不为0的数字执行除法的事件就会触发该异常。
例如:
create or replace function zero_divide_f return int as
a int := 10;
b int := 0;
begin
a := a / b;
exception
when zero_divide then
dbms_output.put_line('this is a normal error');
return 1;
end;
/
select zero_divide_f();
1. 错误处理不支持处理问题摘要:
匿名块中的动态SQL包含提交和回滚的匿名块,不支持;
存储过程中的动态SQL包含提交和回滚的匿名块,不支持;
包中的存储过程以及匿名块中的动态SQL包含提交和回滚,不支持。
2. 在包中,函数、存储过程和匿名块的执行顺序有问题;
3. 目前pl/sql不支持保存点。
为了显式抛出异常,请使用RAISE语句或RAISE_APPLICATION_ERROR过程。
RAISE语句不仅支持抛出内置标准异常,而且支持抛出经EXCEPTION_INIT命令初始化过的自定义异常变量。
你只能从存储子程序或方法中调用RAISE_APPLICATION_ERROR过程。
通常,你调用这个过程来抛出一个用户定义的异常,并将它的错误代码和错误消息返回给调用者。
调用RAISE_APPLICATION_ERROR时,请使用以下语法:
RAISE_APPLICATION_ERROR (error_code, message[, {TRUE | FALSE}]);
错误代码是-20000..-20999范围内的整数。如果指定TRUE,PL/oraSQL将不会打印消息。否则,PL/oraSQL将打印消息。默认为FALSE。
DBMS_UTILITY.format_error_backtrace 函数是记录异常时必须调用的关键子程序。 它返回一个字符串,将错误追溯到引发错误的行! 追溯函数总是只追溯到最近引发的异常。 错误允许返回的最大长度为10000。
使用DBMS_UTILITY.format_error_backtrace函数的示例:
--预处理条件
create table test_error_stack(id number primary key);
insert into test_error_stack values (1);
create table t_errlog(operate_error_msg varchar(1000));
create or replace procedure pr_error_code_test1 is
v_backtrace varchar(1000);
v_error varchar(1000);
begin
INSERT INTO test_error_stack values (2);
INSERT INTO test_error_stack values (1);
commit;
exception when others then
insert into t_errlog(operate_error_msg) select to_char(dbms_utility.format_error_backtrace);
commit;
rollback;
end pr_error_code_test1;
/
call pr_error_code_test1();
select * from t_errlog;
--仅使用函数调用
select to_char(dbms_utility.format_error_backtrace)
支持在抛出异常时获取整数错误代码。
非异常块下获取的错误代码为数值0。
该整数的错误代码可以转换为长度为5的字符串错误代码。
转换函数 SQLCODE_TO_SQLSTATE 在 orafce.
长度为5的字符串错误码在 Appendix A.
例如:
declare
a text;
b int;
begin
raise exception 'sqlcode test';
exception
when others then
a := sqlstate;
b := sqlcode;
raise notice 'sqlstate: %, sqlcode: %, sqlcode: %', a, b, sqlcode;
end;
/
select SQLCODE_TO_SQLSTATE(16777248);
支持在抛出异常时获取错误消息。 非异常块下当获取的错误代码为数值0时,错误消息为ORA-0000。