本章讲解如何处理 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。