如果 PLSQL发生了错误, 无论是系统错误还是应用错误, 都会抛出一个异常, 当前 PL/SQL 块中执行单元会暂停处理, 如果当前块有一个异常处理单元的话, 控制会转移到当前块的异常处理单元来处理异常, 完成了异常处理后就不能再返回到当前块, 相反, 控制会转移到外层包伟块, 如果有的话.
在Pl/sql 中, 任何类型的错误都按程序异常处理:
- 系统产生错误( 内存溢出, 索引重复等等 )
- 用户动作导致的错误
- 应用程序发出的警告
种类: 系统异常 和 程序员定义的异常
抛出: 数据库本身可以抛出异常, 自己写指令 RAISE_APPLICATION_ERROR 命令也可以抛出异常.
传播: 一个异常如果没有在当前块内被处理了, 而是从当前块传给包围它的外层块的过程.
如果是没有名字的异常, 那么可以使用 ( ORA-01843)
如果 pl/sql 发生了一个错误, 无论是系统错误还是应用错误, 都会抛出一个异常, 当前 pl/sql 块中执行单元就会暂停处理, 如果当前块有一个异常处理单元的话, 控制会转移到当前块的异常处理单元来处理异常, 完成了异常处理后就不能再返回到当前块, 相反, 控制会转移到外层包围块, 如果有的话.
被命名的系统异常
声明有名异常
pl/sql 在 standard 包中声明的异常(还有其他内置包中的异常)已经覆盖了内部或系统生成的错误. 但是用户在应用程序中遇到的问题很多都是应用程序特有的, 即应用程序的逻辑错误, 比如 账户余额为负数, 或者赎回日期不能是过去日期 等等, 这种逻辑错误的异常, 你可以定义并自己抛出, 注意: 这不是系统错误, 系统不会因此而抛出异常.
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = –1843 THEN
这个代码很难理解, 可以使用 EXCEPTION_INIT, 作用是将自己定义的名称 和 错误代码相联系
EXCEPTION_INIT 是一个在编译时刻执行的命令或者叫编译命令.
PROCEDURE my_procedure
IS
invalid_month EXCEPTION; -- 定义一个异常
PRAGMA EXCEPTION_INIT ( invalid_month, –1843); -- 建立异常名称与错误代码之间的联系
BEGIN
…
EXCEPTION
WHEN invalid_month THEN
异常的作用范围
被命名的系统异常: 例如 no_data_found, 全局可用的异常
被命名的程序定义的异常: 这种异常只在声明它的块(以及所有嵌套块)的执行单元, 异常处理单元被抛出或处理. 对于包规范部分定义的异常, 那么对于那些对这个包具有 execute 权限的用户的程序, 这个异常都是可见的.
匿名系统异常: 这些异常在任何 pl/sql 块的异常处理单元 when others 部分处理. 如果这些已经被指定了名字, 这个名称的作用范围是和被命名的自定义异常时一样的, 注意: 是这个名称的范围.
匿名自定义异常: 这些异常只是在调用 raise_application_error 时才定义, 并返回到调用程序.
抛出异常的方式
RAISE exception_name;
RAISE package_name.exception_name; ( 如果异常在包中, 且不是 standard 包, 那么就要先使用包名 )
RAISE;
RAISE_APPLICATION_ERROR( 也是抛出异常, 也是在DBMS_STANDARD 这个包中, 这个命令有个好处是, 可以准许你写一些错误信息 )
PROCEDURE RAISE_APPLICATION_ERROR (
num binary_integer, -- 错误号, –20999 ~ -20000
msg varchar2, -- 错误消息
keeperrorstack boolean default false ); -- 表明是否想把这个错误添加到栈中(true)还是替换现存的错误(false)
捕获异常
EXCEPTION
WHEN exception_name
THEN
executable statements
…
WHEN OTHERS
THEN
executable statements
异常例子1, 直接使用 raise
procedure my_procedure
is
invalid_month exception;
begin
raise invalid_month;
exception
when invalid_month then
dbms_output.put_line('invalid_month error');
end;
-- chap06_02.sql
procedure my_procedure
is
invalid_month exception;
pragma exception_init(invalid_month, -20188);
begin
raise_application_error(-20188, 'You get an exception');
-- 注意此处的抛出异常的message, 如果此异常被捕获了, 那么这个message是不会被显示出来的, 而直接走捕获异常的程序 exception
when invalid_month then
dbms_output.put_line('invalid_month error');
end;
内置错误函数
SQLCODE, 返回一个错误代码, 如果没有错误, 返回 0
SQLERRM, 可以返回错误代码对应的官方的错误信息.
要想查看某个错误号对应的提示信息, 可以使用如下:
begin
dbms_output.put_line(SQLERRM(-1403));
end;
/
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE, 10g 以后开始引入, 返回一个格式化文本串, 能够回退到错误最初发生代码行的行号.
未处理的异常
如果抛出的异常没有被捕获, 那么就是一个未处理异常, PLSQL会把这个未处理的异常作为错误代码一直返回到运行PLSQL的应用环境, 如果是 sql*plus, 所有顶层块逻辑中的DML都会自动的执行 ROLLBACK.
防止未处理异常: ( 设计最外层块或程序 )
- 捕获任何可能传播的异常
- 为错误记录日志, 从而能够分析出了什么情况出现的异常
- 返回一个状态码, 描述或其他信息, 以帮助宿主环境决定采取适当的措施.
异常会逐层向外传播
如果想抛出异常以后, 程序还能继续运行, 就需要将各个模块独立出来, 针对自己的独立的模块, 进行异常处理, 这样并不影响其他模块. 如果是嵌套类型, 并且捕获异常程序在最外层的话, 那么只要内层有一个错误, 将导致所有的操作全部ROLLBACK.
如何使用 WHEN OTHERS
when others 可以捕获所有未捕获的异常, 因为你没有明确指出对某些特殊异常的处理, 你可能要利用内置函数 SQLCODE 等来确认到底抛出的是什么异常.
个人感觉, 首先想好逻辑关系, 如果使用 when others, 那么就要打印出逻辑号, 或将其保存起来. 尽量少用 when others
procedure add_company(
id_in IN company.id%type,
name_in IN company.name%type,
type_id_in IN company.type_id%type)
is
begin
dbms.output_putline('show others exceptions');
exception
when others
then
/*
|| 位于异常中的匿名块,
|| 我可以声明本地变量来放错误号和错误信息
*/
declare
l_errcode PLS_INTEGER := sqlcode;
begin
case l_errcode
when -1 then
dbms_output.put_line('error 1');
raise; -- 这里的 raise 意思是虽然捕获了异常, 但是你只要知道是哪个异常, 但是还是要抛出这个异常
-- 为的就是, 程序本身要抛出来, 而如果你不加这个 raise, 那么即便有异常, 程序也会显示成功
-- 因为你已经捕获了异常, 而实际上, 你是用 when others 捕获的, 并非真正捕获.
when -2291 then
dbms_output.put_line('error 2');
raise;
else
raise;
end case;
end;
end add_company;
设计异常的框架
- 当代码发生错误时, 要尽可能多的获得错误发生的环境信息
- 尽量避免用像 when error then Null (或更差的 when others then null) 这样掩盖错误
- 如果可能, 尽量使用 pl/sql 的缺省错误处理机制. 要避免程序向外层调用块或者宿主环境返回状态代码.
异常的分类( 人为分类 )
deliberate: 代码本身可能需要依赖异常, 必须设计有异常放生
例如, utl_file.get_line, 这是一个读取文件的函数, 每次读取一行, 但是每次读取超过最后一样, 就会抛出 no_data_found, 这就是它本身的工作方式
procedure read_file_and_do_stuff (
dir_in in varchar2, file_in in varchar2 )
is
l_file utl_file.file_type;
l_line varchar2(32767);
begin
l_file := utl_file.fopen (dir_in, file_in, 'R', max_linesize => 32767); loop
utl_file.get_line(l_file, l_line);
do_stuff;
end loop;
exception
-- 作为一个正常的处理过程来处理, 意义上并不是异常
when no_data_found
then
utl_file.fclose(l_file);
more_stuff_here;
end;
/
show errors;
针对这种异常: 你要预先为这种异常写好代码, 最佳实践就是避免在异常处理单元包含逻辑. 异常处理单元应该只包含处理错误需要的代码, 把错误记录日志, 重新抛出异常等.
unfortunate: 这是一个错误, 但是属于在意料之中并不意味着有问题, 例如: select into 语句发生了 no_data_found 异常.
针对这种异常: 这种异常是, 并不把这种情况看做是错误, 也没有按未处理异常传播, 这时, 通过返回一个值或者返回状态标志的方法来只是发生了异常, 应该让程序的用户自行去决定是否应该为这个错误终止程序.
unexcepted: 这是一个真正的错误, 应用程序出了问题. 一个例子就是 select into 应该返回一条记录, 但是却抛出了 too_many_rows异常.
针对这种异常: 尽可能的记录下应用程序的上下文信息, 以便帮助找到发生原因, 程序本身也应该因为抛出未处理异常而终止.