|
当我们提交的PL/SQL代码具有很好的一直性、可读性和严密性时,往往会给运维DBA、其他程序员和最终用户一个惊喜。不管我们之前的业务逻辑设计的如何规范、清晰;也不管我们的业务操作流程定义多么明确,请始终坚信:最终用户在决大多数情况下的操作方法都不会符合业务规则的。 所以,我们要想编写出健壮的PL/SQL代码,就必须借助调试技术,可以在关键点上提示最终用户的错误. 很多做运维的DBA都遇到过类似的问题,因为用户的要求,当DBA在做数据维护时,偶尔会清理或修改一些比较重要的关系数据,往往会导致很多的代码执行异常,并且没有任何可明确的提示,DBA不得不打开代码去阅读、分析定位问题。所以,错误处理的细节可以确保迅速定位被破坏的数据或关系。 这里说到Oracle的错误有两种:编译时和运行时错误。 下面我先用2个小的匿名块来对比一下编译时错误和运行时错误的区别。 1)编译时错误 SQL> declare
2 v_num number;
3 begin
4 v_num:=to_number('1A')
5 end;
6 /
ORA-06550: line 5, column 1:
PLS-00103: Encountered the symbol "END" when expecting one of the following:
. ( * % & = - + ; < / > at in is mod remainder not rem
<> or != or ~= >= <= <> and or like LIKE2_
LIKE4_ LIKEC_ between || multiset member SUBMULTISET_
The symbol ";" was substituted for "END" to continue.
SQL>
在这段代码的第4行,我故意少写了一个分号(;),但错误提示却是在line 5,这也是经常困惑大家的地方,在这里需要提醒大家的是,PL/SQL语法分析器指向的通常是出现错误的下一行,所以有时候我们在定位问题时,不要被表象或迷惑。再看一段代码,在下面的这段代码中,我增加了一个统计语句,但实际上non_table这个表在数据库中根本不存在,可以看到,下面的错误提示还是出现在了我故意漏写的标点符号上。 SQL> declare
2 v_num number;
3 begin
4 select count(*) into v_num from non_table; --non_table是一个不存在的表
5 if v_num = 0 then
6 v_num:=to_number('1A')
7 end if;
8 end;
9 /
end if;
*
ERROR at line 7:
ORA-06550: line 7, column 2:
PLS-00103: Encountered the symbol "END" when expecting one of the following:
. ( * % & = - + ; < / > at in is mod remainder not rem
<> or != or ~= >= <= <> and or like LIKE2_
LIKE4_ LIKEC_ between || multiset member SUBMULTISET_
The symbol ";" was substituted for "END" to continue.
SQL>
为什么会出现这个现象,我想大家也可能都很明白:这是因为在调用SQL语法分析器检查内部的SQL语句之前,PL/SQL语法分析器会先对整个PL/SQL程序进行遍历检查。也就是说:PL/SQL总是会先于SQL进行语法检查,只有当PL/SQL语法按照语义上解释是正确的情况下,执行PL/SQL才会显示SQL语法错误(如果有的话) 提醒:在编写PL/SQL代码时,要时刻保持清醒,否则很容易出现编译时错误,编译时错误是不能调试的,只能借助PL/SQL语法分析器去定位编译时错误。 我经常看到有的开发人员一下子蒙头写了几百行PL/SQL代码(还很自诩,觉得自己很了不起,多插一句,往往很多负责开发的manager本身就不是特别懂技术,流程就...,看到team中的人,如此高效的开发能力,颇为赞赏,结果大家一联调测试时才发现,根本不能用,于是又花更多的时间、人力去处理这个早就应该OK的issue,实在可笑,这样的情况,我想国内很多的开发team中屡见不鲜吧),结果发现死活调试不通,最后不得不得花更多的时间去分析、定位他的代码中的问题,甚至有些情况下,不得不重新编写。 2)运行时错误 接着再看一个运行时错误,我还以上面的代码为例,大家对比一下就可以发现不同了: SQL> declare
2 v_num number;
3 begin
4 v_num:=to_number('1A');
5 end;
6 /
declare
*
ERROR at line 1:
ORA-06502: PL/SQL: numeric or value error: character to number conversion error
ORA-06512: at line 4
SQL>
很明显,这段修改过的代码通过了编译,但在运行时却发生了异常:试图将一个带字母的字符串转换成数字。并且,我们可以看到,PL/SQL分析器能准确的找到导致运行时出现异常的那行代码(如上 line 4),运行时异常是由运行时错误引起的。捕获、跟踪、识别并正确处理这些异常,正是我们能写出健壮的PL/SQL代码所关心的。 3)系统预定义的标准异常 下表给出了Oracle提供了一些标准的预定义异常,绝大多数大家都面熟. | 异常 | 错误返回值 | 描述 | ACCESS_INTO_NULL | ORA-06530 | 试图访问一个未初始化的对象 | | CASE_NOT_FOUND | ORA-06592 | 定义了一个case语句却没有else子句与之对应,并且没有一个case语句符合运行时的条件 | | COLLECTION_IS_NULL | ORA-06531 | 试图访问一个未初始化的嵌套表(nested table)或可变数组(varry) | | CURSOR_ALREADY_OPEN | ORA-06511 | 试图打开一个已经open的cursor | | DUP_VAL_ON_INDEX | ORA-00001 | 试图在表中唯一索引对应的列上插入一个重复值 | | INVALID_CURSOR | ORA-01001 | 试图进行一个非发的cursor操作,比如关闭一个已经closed游标等 | | INVALID_NUMBER | ORA-01722 | 非法数字 | | LOGIN_DENIED | ORA-01017 | 试图用非法的用户名或口令登陆 | | NO_DATA_FOUND | ORA-01403 | 使用select - into结构并且返回为null;或访问嵌套表中已经删除的元素;或访问索引表中一个未初始化的元素 | | NOT_LOGGED_ON | ORA-01012 | 程序在非连接状态发出一个数据库调用,一般发生在会话已经断开的情况下 | | PROGRAM_ERROR | ORA-06501 | 通常表示所有Oracle为正式收录的错误。许多数据库的对象特性会产生这种错误 | | ROWTYPE_MISMATCH | ORA-06504 | 游标结构与PL/SQL游标变量不一致;或游标参数的实际内容与形参不符 | | SELF_IS_NULL | ORA-30625 | 在对象类型的一个实例还没有初始化的情况下,就试图调用他的非静态成员函数 | | STORAGE_ERROR | ORA-06500 | SGA运行时内存益处或SGA被破坏 | | SUBSCRIPT_BEYOND_COUNT | ORA-06533 | 分配给嵌套表或可边数组的空间小于下标使用的值 | | SUBSCRIPT_OUTSIDE_LIMIT | ORA-06532 | 使用非法的索引值,来访问嵌套表或可变数组,例如:负数 | | SYS_INVALID_ROWID | ORA-01410 | 试图将一个字符串变为一个非法的ROWID值 | | TIMEOUT_ON_RESOURCE | ORA-00051 | 数据库不能确保对一个资源的锁定 | | TOO_MANY_ROWS | ORA-01422 | 使用select-into语句,但查询结果返回不止一行 | | VALUE_ERROR | ORA-06502 | 试图把变量A赋给B,但变量B由于空间太小而不能保存 | | ZERO_DIVIDE | ORA-01476 | 除数为0 | | OTHERS | Oracle用这个"others"捕捉所有未知的错误.可以使用sqlcode函数,sqlerrm函数在错误处理模块中显示错误 代码和错误信息.而且others处理必须是一个块的最后一个错误处理,否则others处理的优先级比较高,它会捕捉所有错误,包括预定义的Oracle错误和非预定义的Oracle错误 |
4)Oracle的两个内置函数 | 函数 | Oracle预定义的错误 | 用户自定义的错误 | SQLCODE | 异常发生的时候,返回一个与Oracle预定义的异常对应的负值;只有NO_DATA_FOUND异常的返回值为正100 | 如果EXCEPTION_INIT PRAGAM未定义,返回1 如果EXCEPTION_INIT PRAGAM已定义,则返回一个在-20001到-20999之间的值 | SQLERRM | 过载函数。并按照下面的条件执行: 如果没有传递数字参数给函数,则返回触发的异常已经定义好的错误代码和信息 如果传递给函数的是一个正整数或是一个不属于Oracle预定义异常编号的负数,则返回实参的负数值和非Oracle异常的消息 如果传递给函数的参数是属于Oracle预定义的异常编号,则返回实参和Oracle预定义的消息 | 如果是由raise命令触发,则返回1和"User_Defined_Exception"的消息,否则返回一个在-20001和-20999之间的合法整数值和由RAISE_APPLICATION_INFO函数指定的文本消息 |
5)异常管理 PL/SQL代码中的异常捕获和管理也是一个经常让人犯晕的地方,在pl/sql中,除了系统支持的预定义异常外,也允许用户自定义异常,用户自定义的错误也可以有2种方式实现,一种是使用EXCEPTION_INIT指令的预编译程序,另外一种是不使用预编译程序。这部分内容网上的资料很多,也很详细,我就不再唠叨了。 如果要深入理解这部分内容,需要先了解Oracle内部的错误堆栈的管理,但错误堆栈是一块极其复杂的内容,不过我们只要始终牢记:堆栈顶部的程序触发错误,而底部的程序是发生错误的地方,问题就容易理解了。幸运的是,在Oracle 10g中,Oracle对dbms_utility这个程序包做了改进,10g中的dbms_utility.format_error_backtrace过程为记录和跟踪错误序列提供了一定的灵活性,也为Oracle PL/SQL开发者带来了便利。甲骨文公司对Oracle软件的不懈的人性化的更新和发展,是大家无法舍弃她的一个很重要的原因。 下面我们看一个如何在Exception Block中捕获异常发生所在行的例子: 在这篇小技巧中,介绍一下如何在Exception Block中捕获错误发生的所在行,以便于我们准确的定位和解决问题。在实际的pl/sql开发过程中,有的pl/sql代码非常庞大,甚至彼此嵌套了好多层,当然了,Exception部分也就变的复杂起来。 这个时候,我们在日常运维或开发调试的时候,经常会根据Exception Block中抛出的raise去分析是程序的问题还是数据的问题,以便于后期改进和处理。那么有的人就很头疼,一组上K行的pl/sql,分析起来也是很头疼的,他们就希望能在出现异常的时候,知道准确的出错SQL所在行。 以前我们一般就简单的通过SQLCODE、SQLERRM来获取一些错误代码和相关的信息,从Oralce 10g开始,我们可以借助DBMS_UTILITY.FORMAT_ERROR_BACKTRACE() 函数来实现我们的目标。 下面我通过一个简单的DEMO来说一下如何调用这个函数: conn study/study;
create table zrp (no int);
set serveroutput on
declare
v_no number;
begin
select no into v_no from zrp;
exception
when others then
dbms_output.put_line('SQL CODE:'||sqlcode||chr(10)||
sqlerrm||chr(10)||
dbms_utility.format_error_backtrace());
end;
/测试一下看看: SQL> declare
2 v_no number;
3 begin
4 select no into v_no from zrp;
5 exception
6 when others then
7 dbms_output.put_line('SQL CODE:'||sqlcode||chr(10)||
8 sqlerrm||chr(10)||
9 dbms_utility.format_error_backtrace());
10 end;
11 /
SQL CODE:100
ORA-01403: no data found
ORA-06512: at line 4
PL/SQL procedure successfully completed
SQL> 是不是很好用?
|