📄 第七章 控制pl-sql错误 - pl-sql用户指南与参考 - whatiswhat.htm
字号:
<TD>发生算术、转换、截位或长度约束错误。例如,当我们的程序把一个字段的值放到一个字符变量中时,如果值的长度大于变量的长度,PL/SQL就会终止赋值操作并抛出异常VALUE_ERROR。在过程化语句中,如果字符串向数字转换失败,异常VALUE_ERROR就会被抛出。(在SQL语句中,异常
INVALID_NUMBER会被抛出。)</TD></TR>
<TR>
<TD vAlign=top>ZERO_DIVIDE</TD>
<TD>程序尝试除以0。</TD></TR></TBODY></TABLE>
<P class=title1>四、自定义PL/SQL异常</P>
<P>PL/SQL允许我们定义自己的异常。与预定义异常不同的是,用户自定义异常必须声明,并且需要用RAISE语句显式地抛出。
</P>
<P class=title2>1、声明PL/SQL异常</P>
<P>异常只能在PL/SQL块、子程序或包的声明部分声明。下例中,我们声明一个名为past_due的异常:
</P>
<BLOCKQUOTE>
<TABLE>
<TBODY>
<TR>
<TD
noWrap><STRONG>DECLARE</STRONG><BR> past_due <STRONG>EXCEPTION</STRONG>;
</TD></TR></TBODY></TABLE></BLOCKQUOTE>
<P>异常和变量的声明是相似的。但是要记住,异常是一种错误情况(error
condition),而不是数据项。与变量不同的是,异常不能出现在赋值语句或是SQL语句中。但是,变量的作用域规则也适用于异常。
</P>
<P class=title2>2、PL/SQL异常的作用域规则</P>
<P>在同一个块内,异常不能声明两次。但可以在不同的块声明相同的异常。 </P>
<P>块中声明的异常对于当前块来说是本地的,但对于当前块的所有子块来说是全局的。因为块只能引用本地或全局的异常,所以封闭块不能引用声明在子块中的异常。
</P>
<P>如果我们在子块中重新声明了一个全局的异常,本地声明的异常的优先级是要高于全局的。所以,子块就不能引用全局的异常,除非全局异常在它的所在块中用标签作了标记,这种情况下可以使用下面的语法来引用全局异常:
</P>
<BLOCKQUOTE>
<TABLE>
<TBODY>
<TR>
<TD noWrap>block_label.exception_name
</TD></TR></TBODY></TABLE></BLOCKQUOTE>
<P>下例中演示了作用范围规则: </P>
<BLOCKQUOTE>
<TABLE>
<TBODY>
<TR>
<TD
noWrap><STRONG>DECLARE</STRONG><BR> past_due <STRONG>EXCEPTION</STRONG>;<BR> acct_num <STRONG>NUMBER</STRONG>;<BR><STRONG>BEGIN</STRONG><BR> <STRONG>DECLARE</STRONG> <EM>-- sub-block begins</EM><BR> past_due <STRONG>EXCEPTION</STRONG>; <EM>-- this declaration prevails</EM><BR> acct_num <STRONG>NUMBER</STRONG>;<BR> <STRONG>BEGIN</STRONG><BR> ...<BR> <STRONG>IF</STRONG> ... <STRONG>THEN</STRONG><BR> <STRONG>RAISE</STRONG> past_due; <EM>-- this is not handled</EM><BR> <STRONG>END</STRONG> <STRONG>IF</STRONG>;<BR> <STRONG>END</STRONG>; <EM>-- sub-block ends</EM><BR><STRONG>EXCEPTION</STRONG><BR> <STRONG>WHEN</STRONG> past_due <STRONG>THEN</STRONG> <EM>-- does not handle RAISEd exception</EM><BR> ...<BR><STRONG>END</STRONG>;
</TD></TR></TBODY></TABLE></BLOCKQUOTE>
<P>上例中的封闭块并不能捕获抛出来的异常,因为在子块中声明的past_due优先级要高于封闭块声明的异常。虽然它们的名字相同,但实际上是两个不同的past_due异常,就像两个acct_num变量只是共享着相同的名字一样,实际上它们是完全不同的两个变量。因此,RAISE语句和WHEN子句所引用的是不同的异常。如果想让封闭块能捕获到子块中的past_due异常,我们就必须从子块中删除声明,或是在封闭块中添加OTHERS处理器。
</P>
<P class=title2>3、把PL/SQL异常与编号关联:编译指示EXCEPTION_INIT</P>
<P>要想控制没有预定义名称的错误(通常为 ORA-
消息),我们就必须使用OTHERS处理器或编译指示EXCEPTION_INIT。编译指示就是能在编译期而非运行时进行处理的编译指令。
</P>
<P>在PL/SQL中,编译指示EXCPTION_INIT能告诉编译器把异常名称和错误编号关联起来。这就能让我们按名称来引用所有的内部异常,并为它编写特定的处理程序。在我们看到的错误栈或是错误消息序列中,最顶层的就是我们能捕获和处理的信息。
</P>
<P>我们可以把编译指示EXCEPTION_INIT写在PL/SQL块、子程序或包的声明部分,语法如下:
</P>
<BLOCKQUOTE>
<TABLE>
<TBODY>
<TR>
<TD
noWrap><STRONG>PRAGMA</STRONG> EXCEPTION_INIT(exception_name, -Oracle_error_number);
</TD></TR></TBODY></TABLE></BLOCKQUOTE>
<P>其中exception_name是已经声明过的异常名称,Oracle_error_number是Oracle错误编号。编译指示必须和异常声明处于同一个声明中,并且只能在异常声明之后出现。如下例所示:
</P>
<BLOCKQUOTE>
<TABLE>
<TBODY>
<TR>
<TD
noWrap><STRONG>DECLARE</STRONG><BR> deadlock_detected <STRONG>EXCEPTION</STRONG>;<BR> <STRONG>PRAGMA</STRONG> EXCEPTION_INIT (deadlock_detected, -60);<BR><STRONG>BEGIN</STRONG><BR> ... <EM>-- Some operation that causes an ORA-00060 error</EM><BR><STRONG>EXCEPTION</STRONG><BR> <STRONG>WHEN</STRONG> deadlock_detected <STRONG>THEN</STRONG><BR> <EM>-- handle the error</EM><BR> ...<BR><STRONG>END</STRONG>;
</TD></TR></TBODY></TABLE></BLOCKQUOTE>
<P
class=title2>4、自定我们自己的错误消息:过程RAISE_APPLICATION_ERROR</P>
<P>过程RAISE_APPLICATION_ERROR能帮助我们从存储子程序中抛出用户自定义的错误消息。这样,我们就能把错误消息报告给应用程序而避免返回未捕获异常。
</P>
<P>调用RAISE_APPLICATION_ERROR的语法如下: </P>
<BLOCKQUOTE>
<TABLE>
<TBODY>
<TR>
<TD
noWrap>raise_application_error(error_number, message[, {<STRONG>TRUE</STRONG> | <STRONG>FALSE</STRONG>}]);
</TD></TR></TBODY></TABLE></BLOCKQUOTE>
<P>error_number是一个范围在-20000至-20999之间的负整数,message是最大长度为2048字节的字符串。如果第三个可选参数为TRUE的话,错误就会被放到前面错误的栈顶。如果为FALSE(默认值),错误就会替代前面所有的错误。
</P>
<P>RAISE_APPLICATION_ERROR是包DBMS_STANDARD的一部分,所以,我们对它的引用不需要添加限定修饰词。
</P>
<P>应用程序只能从一个正在执行的存储子程序或方法中调用raise_application_error。在调用时,
raise_application_error会结束子程序并把用户定义的错误编号和消息返回给应用程序。错误编号和消息可以像其它的Oracle错误一样被捕获。
</P>
<P>在下面的例子中,我们在雇员工资栏的内容为空的情况下调用raise_application_error:
</P>
<BLOCKQUOTE>
<TABLE>
<TBODY>
<TR>
<TD
noWrap><STRONG>CREATE</STRONG> <STRONG>PROCEDURE</STRONG> raise_salary (emp_id <STRONG>NUMBER</STRONG>, amount <STRONG>NUMBER</STRONG>) <STRONG>AS</STRONG><BR> curr_sal <STRONG>NUMBER</STRONG>;<BR><STRONG>BEGIN</STRONG><BR> <STRONG>SELECT</STRONG> sal<BR> <STRONG>INTO</STRONG> curr_sal<BR> <STRONG>FROM</STRONG> emp<BR> <STRONG>WHERE</STRONG> empno = emp_id;<BR> <STRONG>IF</STRONG> curr_sal <STRONG>IS</STRONG> <STRONG>NULL</STRONG> <STRONG>THEN</STRONG><BR> /* Issue user-defined error message. */<BR> raise_application_error (-20101, <EM>'Salary is missing'</EM>);<BR> <STRONG>ELSE</STRONG><BR> <STRONG>UPDATE</STRONG> emp<BR> <STRONG>SET</STRONG> sal = curr_sal + amount<BR> <STRONG>WHERE</STRONG> empno = emp_id;<BR> <STRONG>END</STRONG> <STRONG>IF</STRONG>;<BR><STRONG>END</STRONG> raise_salary;
</TD></TR></TBODY></TABLE></BLOCKQUOTE>
<P>调用程序会得到一个PL/SQL异常,它能在OTHERS处理器中使用错误报告函数SQLCODE和SQLERRM来进行处理。同样,我们也可以使用编译指示EXCEPTION_INIT把raise_application_error返回的错误编号映射到异常本身。如下面的Pro*C例子所示:
</P>
<BLOCKQUOTE>
<TABLE>
<TBODY>
<TR>
<TD
noWrap>EXEC <STRONG>SQL</STRONG> <STRONG>EXECUTE</STRONG><BR> /* Execute embedded PL/<STRONG>SQL</STRONG> block using host<BR> variables my_emp_id and my_amount, which were<BR> assigned values in the host environment. */<BR><BR><STRONG>DECLARE</STRONG><BR> null_salary <STRONG>EXCEPTION</STRONG>;<BR> /* Map error number returned by raise_application_error<BR> to user-defined exception. */<BR> <STRONG>PRAGMA</STRONG> EXCEPTION_INIT (null_salary, -20101);<BR><STRONG>BEGIN</STRONG><BR> raise_salary (:my_emp_id, :my_amount);<BR><STRONG>EXCEPTION</STRONG><BR> <STRONG>WHEN</STRONG> null_salary <STRONG>THEN</STRONG><BR> <STRONG>INSERT</STRONG> <STRONG>INTO</STRONG> emp_audit<BR> <STRONG>VALUES</STRONG> (:my_emp_id, ...);<BR><STRONG>END</STRONG>;<BR><BR><STRONG>END</STRONG>-EXEC;
</TD></TR></TBODY></TABLE></BLOCKQUOTE>
<P>这项技术能让调用程序在特定的异常处理程序中控制错误。 </P>
<P class=title2>5、重新声明预定义异常</P>
<P>请记住,PL/SQL把预定义的异常作为全局内容声明在包STANDARD中,所以,我们没有必要重新声明它们。重新声明预定义异常是错误的做法,因为我们的本地声明会覆盖掉全局声明。例如,如果我们声明了一个invalid_number,当PL/SQL抛出预定义异常
INVALID_NUMBER时,我们为异常INVALID_NUMBER编写的异常控制程序就无法正确地捕获到它了。这种情况下,我们必须像下面这样使用点标志来指定预定义异常:
</P>
<BLOCKQUOTE>
<TABLE>
<TBODY>
<TR>
<TD
noWrap><STRONG>EXCEPTION</STRONG><BR> <STRONG>WHEN</STRONG> INVALID_NUMBER <STRONG>OR</STRONG> STANDARD.INVALID_NUMBER <STRONG>THEN</STRONG><BR> <EM>-- handle the error</EM><BR><STRONG>END</STRONG>;
</TD></TR></TBODY></TABLE></BLOCKQUOTE>
<P class=title1>五、如何抛出PL/SQL异常</P>
<P>内部异常会由运行时系统隐式地抛出,其中也包括使用编译指示EXCEPTION_INIT与Oracle错误编号关联起来的用户自定义异常。但是,用户自定义的异常就必须显式地用RAISE语句抛出。
</P>
<P class=title2>1、使用RAISE语句抛出异常</P>
<P>PL/SQL块和子程序应该只在错误发生或无法完成正常程序处理的时候才抛出异常。下例中,我们用RAISE语句抛出一个用户自定义的out_of_stack异常:
</P>
<BLOCKQUOTE>
<TABLE>
<TBODY>
<TR>
<TD
noWrap><STRONG>DECLARE</STRONG><BR> out_of_stock <STRONG>EXCEPTION</STRONG>;<BR> number_on_hand <STRONG>NUMBER</STRONG> (4);<BR><STRONG>BEGIN</STRONG><BR> ...<BR> <STRONG>IF</STRONG> number_on_hand < 1 <STRONG>THEN</STRONG><BR> <STRONG>RAISE</STRONG> out_of_stock;<BR> <STRONG>END</STRONG> <STRONG>IF</STRONG>;<BR><STRONG>EXCEPTION</STRONG><BR> <STRONG>WHEN</STRONG> out_of_stock <STRONG>THEN</STRONG><BR> <EM>-- handle the error</EM><BR><STRONG>END</STRONG>;
</TD></TR></TBODY></TABLE></BLOCKQUOTE>
<P>我们也可以显式地抛出预定义异常。这样,为预定义异常编写的处理程序也就能够处理其它错误了,示例如下:</P>
<BLOCKQUOTE>
<TABLE>
<TBODY>
<TR>
<TD
noWrap><STRONG>DECLARE</STRONG><BR> acct_type <STRONG>INTEGER</STRONG> := 7;<BR><STRONG>BEGIN</STRONG><BR> <STRONG>IF</STRONG> acct_type <STRONG>NOT</STRONG> <STRONG>IN</STRONG> (1, 2, 3) <STRONG>THEN</STRONG><BR> <STRONG>RAISE</STRONG> INVALID_NUMBER; <EM>-- raise predefined exception</EM><BR> <STRONG>END</STRONG> <STRONG>IF</STRONG>;<BR><STRONG>EXCEPTION</STRONG><BR> <STRONG>WHEN</STRONG> INVALID_NUMBER <STRONG>THEN</STRONG><BR> <STRONG>ROLLBACK</STRONG>;<BR><STRONG>END</STRONG>;
</TD></TR></TBODY></TABLE></BLOCKQUOTE>
<P class=title1>六、PL/SQL异常的传递</P>
<P>异常被抛出时,如果PL/SQL在当前块或子程序中没有找到对应的异常控制程序,异常就会被继续向上一级传递。也就是说异常会把它自身传递到后继的封闭块直到找到异常处理程序或是再也没有可以搜索到的块为止。在后一种情况下,PL/SQL会向主环境抛出一个未捕获异常。
</P>
<P>但是,异常是不能通过远程过程调用(RPC)来传递的。因此,PL/SQL块不能捕获由远程子程序抛出的异常。下面三幅图演示了异常基本的传递规则。
</P><IMG alt=""
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -