赞
踩
本章解释如何处理 PL/SQL 运行时错误,即异常处理。
另请参阅
" 触发器中的异常处理 " "FORALL 语句完成后处理 FORALL 异常 "
异常(PL/SQL 运行时错误)可能来自设计错误、编码错误、硬件故障或其他来源。 您无法预先处理所有可能触发的异常,但您可以编写异常处理程序,让您的程序在触发异常的情况下继续运行。
任何 PL/SQL 块都可以有一个异常处理部分,它可以处理一个或多个异常。 例如:
EXCEPTION WHEN ex_name_1 THEN statements_1 -- Exception handler WHEN ex_name_2 OR ex_name_3 THEN statements_2 -- Exception handler WHEN OTHERS THEN statements_3 -- Exception handler END;
在上面的语法示例中,ex_name_n 是异常的名称, statements_n 是一个或多个语句。(有关完整的语法和语义,请参阅 异常处理程序 。)
当 PL/SQL 块的可执行部分触发异常时,可执行部分会停止执行并将控制权转移到异常处理部分。如果抛出异常 ex_name_1,则运行语句 statements_1 。如果抛出异常 ex_name_2 或 ex_name_3,则运行语句 statements_2 。如果抛出其他异常,则运行语句 statements_3。
异常处理程序运行后,控制权转移到封闭块的下一条语句。如果没有封闭块,则:
如果异常处理程序在子程序中,则将控制权返回给调用者调用之后的语句处。
如果异常处理程序位于匿名块中,则控制权转移到主机环境(例如,ksql)
如果在没有异常处理程序的 PL/SQL 块中触发异常,则异常会传播。也就是说,异常会在连续的封闭块中向上抛出,直到一个 PL/SQL 块有一个异常处理程序或没有封闭块为止(有关更多信息,请参阅 异常传播 )。如果没有异常处理程序,那么 PL/SQL 会向调用者或主机环境返回一个未处理的异常,这将决定最终的返回结果(有关更多信息,请参阅 未处理的异常 )。
KES 异常的种类分为:
系统预定义异常
系统预定义异常是 PL/SQL 已命名的异常,这些异常都有一个错误代码,且会在系统运行出错时隐式(自动)触发。
有关详细信息,请参阅 系统预定义异常。
用户自定义异常
您可以在任何 PL/SQL 匿名块、子程序或包的声明部分中声明自己的异常。例如,您可以声明一个名为 invalid_number 的异常标记一个无效数字。
用户自定义异常必须显示的触发。
有关详细信息,请参阅 用户自定义异常。
表 异常差异 总结了两种异常之间的差别。
表 12.1.1 异常差异
异常种类 | 定义 | 错误代码 | 名字 | 隐式触发 | 显示触发 |
---|---|---|---|---|---|
系统预定义异常 | 系统 | 总有 | 总有 | 是 | 可选 |
用户自定义异常 | 用户 | 用户分配 | 总有 | 否 | 总是 |
更多详细信息,请参阅 使用 RAISE 语句触发系统预定义的异常 。
对于命名异常,您可以编写特定的异常处理程序,而不是使用 OTHERS 异常处理程序来处理它。 特定的异常处理程序比 OTHERS 异常处理程序更有效,因为后者必须调用一个函数来确定它正在处理哪个异常。有关详细信息,请参阅 检索异常信息 。
使用异常处理程序进行异常处理使程序更易于编写和理解,并降低了未处理异常的可能性。
如果没有异常处理程序,您必须检查所有可能触发的异常,并处理它。但这样很容易忽略可能出现的异常,尤其是在无法立即检测到异常的情况下(例如,使用了错误数据在计算运行之前可能无法检测到)。异常处理代码可以分散在整个程序中。
使用异常处理程序,您不需要预先知道每个可能触发的异常或它可能发生的位置。您只需在可能发生错误的每个块中包含一个异常处理模块。在异常处理模块中,您可以编写处理特定错误或未知错误的异常处理程序。如果块中的任何地方(包括子块内)发生错误,则异常处理程序都会捕获并处理它。错误处理的相关信息则会被隔离在块的异常处理部分,不会再影响程序后续的执行。
在 示例 12-1_ 中,一个存储过程使用单个异常处理程序来处理预定义异常 NO_DATA_FOUND,该异常可以出现在两个 SELECT INTO 语句中的任何一个中。
如果多个语句使用相同的异常处理程序,并且您想知道哪个语句触发了异常,则可以使用变量辅助定位,如 示例 12-2_ 所示。
如果您可以确定您需要处理哪个异常,您可以为特定异常设置一个异常处理程序。您还可以通过将语句放入具有自己的异常处理程序的块中来检查单个语句中的异常。
示例 12-1 在单个异常处理程序中处理多个异常
set serveroutput on \set SQLTERM / CREATE OR REPLACE PROCEDURE select_item ( t_column VARCHAR2, t_name VARCHAR2 ) AUTHID DEFINER IS temp VARCHAR2(30); BEGIN temp := t_column; -- For error message if next SELECT fails -- Fails if table t_name does not have column t_column: SELECT COLUMN_NAME INTO temp FROM USER_TAB_COLS WHERE TABLE_NAME = UPPER(t_name) AND COLUMN_NAME = UPPER(t_column); temp := t_name; -- For error message if next SELECT fails -- Fails if there is no table named t_name: SELECT OBJECT_NAME INTO temp FROM USER_OBJECTS WHERE OBJECT_NAME = UPPER(t_name) AND OBJECT_TYPE = 'TABLE'; EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE ('No Data found for SELECT on ' || temp); WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE ('Unexpected error'); RAISE; END; / \set SQLTERM ;
调用过程(有一个 SYS_PROC 表,但没有 NAME 列):
\set SQLTERM / BEGIN select_item('sys_proc', 'name'); END; / \set SQLTERM ;
结果:
No Data found for SELECT on sys_proc
调用过程(没有 EMP 表):
\set SQLTERM / BEGIN select_item('emp', 'name'); END; / \set SQLTERM ;
结果:
No Data found for SELECT on emp
示例 12-2 在共享异常处理程序中使用定位变量
set serveroutput on \set SQLTERM / CREATE OR REPLACE PROCEDURE loc_var AUTHID DEFINER IS stmt_no POSITIVE; name_ VARCHAR2(100); BEGIN stmt_no := 1; SELECT table_name INTO name_ FROM user_tables WHERE table_name LIKE 'SYS%' LIMIT 1; stmt_no := 2; SELECT table_name INTO name_ FROM user_tables WHERE table_name LIKE 'XYZ%'; EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE ('Table name not found in query ' || stmt_no); END; / \set SQLTERM ; CALL loc_var();
结果:
Table name not found in query 2
为了使您的程序尽可能可靠和安全:
使错误检索代码和异常处理程序。
在输入错误的数据可能导致出错的地方使用错误检查代码。比如不正确或为空的参数和不返回行或返回的行数超出预期的查询语句。可以在程序编写过程中输入错误数据的不同组合来测试您的代码,以查看可能出现的错误。
有时您可以使用错误检查代码来避免引发异常,如 示例 12-4_ 所示。
在可能发生错误的地方添加异常处理程序。
在数字计算、字符串操作和数据库操作期间尤其容易出错。错误也可能来自与您的代码无关的问题,例如,磁盘存储或内存硬件故障,但您的代码仍然需要异常处理程序来处理他们。
设计您的程序以在数据库未处于您期望的状态时继续工作。
例如,您查询的表可能添加或删除了列,或者它们的类型已经更改。您可以通过使用 % TYPE 限定符声明变量或者使用 % ROWTYPE 限定符声明记录变量来保存查询结果来避免出错。
尽可能使用命名异常编写异常处理程序,而不是使用 OTHERS 异常处理程序。
了解预定义异常的名称和原因。如果您知道您的数据库操作可能会引发特定的系统预定义异常,请专门为它们编写异常处理程序。
让您的异常处理程序输出调试信息。
如果您将调试信息存储在单独的表中,请使用自治事务程序执行此操作,这样即使您回滚主程序所做的工作,您也可以提交调试信息。有关自治事务程序的信息,请参阅 自治事务 。
对于每个异常处理程序,仔细决定是让它进行事物的提交、回滚还是继续执行。
无论错误的严重程度如何,都需要使数据库保持正常状态并避免存储错误数据。
通过在每个 PL/SQL 程序的顶层包含一个 OTHERS 异常处理程序来避免漏处理某些异常。
将 OTHERS 异常处理程序中的最后一条语句设为 RAISE, 有关 RAISE 或调用 RAISE_APPLICATION_ERROR 的信息,请参阅 显式触发异常。
针对许多比较常见的异常以及系统运行时会触发的异常, PL/SQL 内部为其预定义了一个名称。例如:除零错误,对应的预定义异常名称为 DIVISION_BY_ZERO。当错误发生时,系统隐式(自动)抛出该异常。
KingbaseES 常见的系统预定义异常详见 PL/SQL 预定义异常表 。
表 12.2.1 PL/SQL 预定义异常
KingbaseES 异常名称 | Oracle 对应异常名称 | 异常说明 |
---|---|---|
|
| CASE 语句中没有任何 WHEN 子句满足条件,且没有 ELSE 子句。 |
|
| 调用一个未初始化的嵌套表或者可变数组的方法 (不包含 EXISTS),或为一个未初始化的嵌套表或者可变数组的元素赋值。 |
|
| 打开一个已经打开的游标。 |
|
| 给一个有唯一约束条件的数据字段保存相同的值。 |
|
| 操作一个不合法的游标。例如关闭一个未打开的游标。 |
|
| 出现运算、转换、截位或长度的约束错误。 |
|
| 未获取到数据。 |
|
| PL/SQL 内部错误。 |
|
| 调用一个为空对象的 MEMBER 方法。 |
|
| 内存溢出。 |
|
| 调用嵌套表或者可变数组时,使用的下标索引超出对应元素的总个数。 |
|
| 调用嵌套表或者可变数组时,使用的下标索引不在合法范围内,如 (-1)。 |
|
| 返回太多的结果行。 |
|
| 数值类型超过定义域。 |
|
| 除零错误。 |
更多预定义异常信息可通过数据库系统函数 SYS_GET_PREDEFINED_EXCEPTION_DETAIL 进行查询,详见 SYS_GET_PREDEFINED_EXCEPTION_DETAIL 函数 。
示例 12-3_ 计算了一家公司的市盈率。如果公司的收益为零,则除法操作会引发预定义的异常 DIVISION_BY_ZERO,并且块的可执行部分将控制权转移到异常处理部分。
示例 12-4_ 使用错误检查代码来避免 示例 12-3_ 处理的异常。
另请参阅
使用 RAISE 语句触发预定义的异常_
示例 12-3 匿名块中处理 DIVISION_BY_ZERO
set serverout on \set SQLTERM / DECLARE stock_price NUMBER := 9.73; net_earnings NUMBER := 0; pe_ratio NUMBER; BEGIN pe_ratio := stock_price / net_earnings; -- raises ZERO_DIVIDE exception DBMS_OUTPUT.PUT_LINE('Price/earnings ratio = ' || pe_ratio); EXCEPTION WHEN ZERO_DIVIDE THEN DBMS_OUTPUT.PUT_LINE('Company had zero earnings.'); pe_ratio := NULL; END; / \set SQLTERM ;
结果:
Company had zero earnings.
示例 12-4 匿名块中避免 DIVISION_BY_ZERO
\set SQLTERM / DECLARE stock_price NUMBER := 9.73; net_earnings NUMBER := 0; pe_ratio NUMBER; BEGIN pe_ratio := CASE net_earnings WHEN 0 THEN NULL ELSE stock_price / net_earnings END; END; / \set SQLTERM ;
您可以在任何 PL/SQL 块,函数、存储过程或者包中声明一个异常。
语法格式:
exception_name EXCEPTION ;
用户自定义异常必须被显示触发,有关异常触发的详细信息,详见 异常触发 。
用户自定义异常可以与一个错误码进行绑定,具体语法为:
PRAGMA EXCEPTION_INIT (exception, error_code) ;
其中 exception 是用户自定义的异常, error_code 是大于 -1000000 且小于 0 的整数,error_code 可以是系统预定义异常的错误码。
注意
EXCEPTION_INIT
仅可为当前声明块中声明的自定义异常进行错误码绑定。
当为包声明中声明的自定义异常进行错误码绑定时,无法使用包名对异常名称进行修饰。即如下语法是错误的:
PRAGMA EXCEPTION_INIT (package.exception, error_code) ;
建议不要重新声明系统预定义的异常,即声明一个用户定义的异常名称,该名称是系统预定义的异常名称。(有关系统预定义异常名称的列表,请参见 系统预定义异常 。)
如果您重新声明了系统预定义的异常,您的本地声明将覆盖 STANDARD 包中的全局声明。为全局声明的异常编写的异常处理程序将无法处理它,除非您使用包名 STANDARD 来限定它的名称。
示例 12-5_ 对此进行了说明
示例 12-5 重新声明预定义异常
DROP TABLE IF EXISTS t CASCADE; CREATE TABLE t (c NUMBER(2,1));
在下面的块中,INSERT 语句隐式引发了 VALUE_ERROR 异常,异常处理程序捕获并处理了这个异常。
set serverout on \set SQLTERM / DECLARE default_number NUMBER := 0; BEGIN INSERT INTO t VALUES(TO_NUMBER('100.001', '9G999')); EXCEPTION WHEN VALUE_ERROR THEN DBMS_OUTPUT.PUT_LINE('Substituting default value for invalid number.'); INSERT INTO t VALUES(default_number); END; / \set SQLTERM ;
结果:
Substituting default value for invalid number.
以下块重新声明了预定义的异常 VALUE_ERROR 当 INSERT 语句隐式触发预定义异常 VALUE_ERROR 时,异常处理程序不会处理它。
set serverout on \set SQLTERM / DECLARE default_number NUMBER := 0; value_error EXCEPTION; -- redeclare predefined exception BEGIN INSERT INTO t VALUES(TO_NUMBER('100.001', '9G999')); EXCEPTION WHEN VALUE_ERROR THEN DBMS_OUTPUT.PUT_LINE('Substituting default value for invalid number.'); INSERT INTO t VALUES(default_number); END; / \set SQLTERM ;
结果:
ERROR: numeric field overflow DETAIL: A field with precision 2, scale 1 must round to an absolute value less than 10^1. CONTEXT: SQL statement "INSERT INTO t VALUES(TO_NUMBER('100.001', '9G999'))" PL/SQL function inline_code_block line 5 at SQL statement
如果您在异常处理程序中使用包名 STANDARD 限定异常名称,则上述块中的异常处理程序将处理预定义的异常 VALUE_ERROR:
set serverout on \set SQLTERM / DECLARE default_number NUMBER := 0; value_error EXCEPTION; -- redeclare predefined exception BEGIN INSERT INTO t VALUES(TO_NUMBER('100.001', '9G999')); EXCEPTION WHEN STANDARD.VALUE_ERROR THEN DBMS_OUTPUT.PUT_LINE('Substituting default value for invalid number.'); INSERT INTO t VALUES(default_number); END; / \set SQLTERM ;
结果:
Substituting default value for invalid number.
要显式触发常,请使用 RAISE 语句或 存储过程 RAISE_APPLICATION_ERROR。
RAISE 语句可以显示的触发一个异常。在异常处理程序之外,RAISE 语句必须指定异常名称。如果在异常处理程序内部,且省略了异常名称,那么该 RAISE 语句将重新引发当前正在处理的异常。
语法格式:
RAISE [ exception ];
其中 exception 可以是已定义的用户自定义异常,也可以是系统预定义异常。
省略 exception 的 RAISE 子句仅可在异常处理模块中使用。
关于 RAISE 子句的更多用法,详见 打印语句 。
12.5.1.1. 使用 RAISE 语句触发用户自定义的异常
在 示例 12-6_ 中,存储过程声明了一个名为 past_due 的异常,使用 RAISE 语句显式触发它,并使用异常处理程序处理它。
示例 12-6 声明、触发和处理用户定义的异常
\set SQLTERM / CREATE PROCEDURE account_status ( due_date DATE, today DATE) AUTHID DEFINER IS past_due EXCEPTION; -- declare exception BEGIN IF due_date < today THEN RAISE past_due; -- explicitly raise exception END IF; EXCEPTION WHEN past_due THEN -- handle exception RAISE NOTICE 'Account past due.'; END; / \set SQLTERM ;
调用存储过程:
\set SQLTERM / BEGIN account_status (TO_DATE('01-JUL-2010', 'DD-MON-YYYY'), TO_DATE('09-JUL-2010', 'DD-MON-YYYY')); END; / \set SQLTERM ;
输出结果:
NOTICE:: Account past due.
12.5.1.2. 使用 RAISE 语句触发系统预定义的异常
系统预定义异常通常由系统运行时隐式触发,但也可以使用 RAISE 语句显示的触发它们。当一个预定义常拥有对应的异常处理程序时,无论是显式触发还是隐式触发,都会触发异常处理程序对相应的异常进行处理。
在 示例 12-7_ 中,存储过程不论显式或是隐式触发预定义异常 VALUE_ERROR,异常处理程序始终都会处理它。
示例 12-7 触发系统预定义异常
DROP TABLE IF EXISTS t CASCADE; CREATE TABLE t (c NUMBER(2,1)); \set SQLTERM / CREATE PROCEDURE p (n NUMBER) AUTHID DEFINER IS default_number NUMBER := 0; BEGIN IF n < 0 THEN RAISE VALUE_ERROR; -- 显式触发 ELSE INSERT INTO t VALUES(TO_NUMBER('100.001', '9G999')); -- 隐式触发 END IF; EXCEPTION WHEN VALUE_ERROR THEN RAISE NOTICE 'Substituting default value for invalid number.'; INSERT INTO t VALUES(default_number); END; / \set SQLTERM ;
调用存储过程:
\set SQLTERM / BEGIN p(-1); END; / \set SQLTERM ;
输出结果:
NOTICE:: Substituting default value for invalid number.
再次调用存储过程:
\set SQLTERM / BEGIN p(1); END; / \set SQLTERM ;
输出结果:
NOTICE: Substituting default value for invalid number.
12.5.1.3. 使用 RAISE 语句重新触发当前异常
在异常处理程序中,可以使用 RAISE 语句重新引发当前正在处理的异常。重新引发的异常会将异常传递给当前块中的的异常处理程序块,若当前块中无相应的异常处理块,则该异常会传播,详见 异常传播 。在重新引发异常时,可以省略异常名称。
在 示例 12-8_ 中,异常处理从内部块开始,到外部块结束。外部块声明异常,因此异常对两个块中都可见,并且每个块都有一个专门针对该异常的异常处理程序。内部块触发异常,其异常处理程序进行初步处理,然后重新引发异常,将其传递给外部块以进行进一步处理。
示例 12-8 重新触发异常
\set SQLTERM / DECLARE salary_too_high EXCEPTION; current_salary NUMBER := 20000; max_salary NUMBER := 10000; erroneous_salary NUMBER; BEGIN BEGIN IF current_salary > max_salary THEN RAISE salary_too_high; -- raise exception END IF; EXCEPTION WHEN salary_too_high THEN -- start handling exception erroneous_salary := current_salary; RAISE NOTICE 'Salary % is out of range.', erroneous_salary; RAISE NOTICE 'Maximum salary is %.', max_salary; RAISE; -- reraise current exception (exception name is optional) END; EXCEPTION WHEN salary_too_high THEN -- finish handling exception current_salary := max_salary; RAISE NOTICE 'Revising salary from % to %.', erroneous_salary, current_salary; END; / \set SQLTERM ;
输出结果:
NOTICE: Salary 20000 is out of range. NOTICE: Maximum salary is 10000. NOTICE: Revising salary from 20000 to 10000.
存储过程 RAISE_APPLICATION_ERROR 通常用来抛出一个用户自定义异常,并将错误码和错误信息传播给调用者。
要调用 RAISE_APPLICATION_ERROR,请使用以下语法:
RAISE_APPLICATION_ERROR (error_code, message);
其中 error_code 是 -20999 .. -20000 之间的整数, message 是长度为 2048 字节的字符串,大于该长度的字符会被自动截断。
使用 RAISE_APPLICATION_ERROR 抛出的用户定义的异常必须已使用 PRAGMA EXCEPTION_INIT 分配 error_code。
通过存储过程 RAISE_APPLICATION_ERROR 抛出的异常可以用 OTHERS 进行捕获处理,也可以通过绑定了相同 error_code 的用户自定义异常进行捕获处理,详见 用户自定义异常 。
在 示例 12-9_ 中,一个匿名块中声明了一个名为 past_due 的异常,并为其分配了错误代码 -20000。然后调用了一个存储过程,存储过程调用带有错误代码 -20000 和消息的存储过程 RAISE_APPLICATION_ERROR ,然后控制权返回到匿名块异常处理部分。 为了检索与异常关联的消息,匿名块中的异常处理程序调用 SQLERRM 函数,详见 检索异常信息 。
示例 12-9 使用 RAISE_APPLICATION_ERROR 引发用户定义的异常
\set SQLTERM / CREATE PROCEDURE account_status ( due_date DATE, today DATE ) AUTHID DEFINER IS BEGIN IF due_date < today THEN -- explicitly raise exception RAISE_APPLICATION_ERROR(-20000, 'Account past due.'); END IF; END; / \set SQLTERM ;
调用存储过程:
\set SQLTERM / DECLARE past_due EXCEPTION; -- declare exception PRAGMA EXCEPTION_INIT (past_due, -20000); -- assign error code to exception BEGIN account_status (TO_DATE('01-JUL-2010', 'DD-MON-YYYY'), TO_DATE('09-JUL-2010', 'DD-MON-YYYY')); -- invoke procedure EXCEPTION WHEN past_due THEN -- handle exception RAISE NOTICE '%', SQLERRM(-20000); END; / \set SQLTERM ;
输出结果:
NOTICE: ERRCODE-20000: Account past due.
如果在一个没有异常处理程序的块中抛出异常,则该异常会进行传播。即该异常会在连续的封闭块中重现,直到被某个具有对应异常处理程序的块捕获处理或者没有封闭块为止。若一直没有对应的异常处理程序,那么 PL/SQL 会将未处理的异常返回给调用者或者返回到主机环境中,该异常也会成为当前程序执行的结果。
用户自定义的异常可以传播到超出其声明的范围(即超出声明它的块),但由于其名称在其超出的范围内不存在。因此,超出其范围后,用户自定义的异常只能使用 OTHERS 来捕获处理。
在 示例 12-10_ 中,内部块声明了一个名为 past_due 的异常,它没有异常处理程序。 当内部块引发 past_due 时,异常传播到外部块,其中不存在名称 past_due。 外部块使用 OTHERS 异常处理程序处理异常。 如果外部块不处理用户定义的异常,则会发生错误,如 示例 12-11_ 所示。
示例 12-10 处理超出范围传播的异常
\set SQLTERM / CREATE OR REPLACE PROCEDURE p AUTHID DEFINER AS BEGIN DECLARE past_due EXCEPTION; due_date DATE := trunc(SYSDATE) - 1; todays_date DATE := trunc(SYSDATE); BEGIN IF due_date < todays_date THEN RAISE past_due; END IF; END; EXCEPTION WHEN OTHERS THEN NULL; END; / \set SQLTERM ;
示例 12-11 未处理传播超出范围的异常
\set SQLTERM / BEGIN DECLARE past_due EXCEPTION; due_date DATE := trunc(SYSDATE) - 1; todays_date DATE := trunc(SYSDATE); BEGIN IF due_date < todays_date THEN RAISE past_due; END IF; END; END; / \set SQLTERM ;
结果:
ERROR: unhandled exception "past_due" CONTEXT: PL/SQL function inline_code_block line 8 at RAISE
异常在块中被触发并捕获处理,未发生传播,如 示例 12-12_ 所示。
示例 12-12 未传播的异常
\set SQLTERM / CREATE OR REPLACE PROCEDURE p AS BEGIN DECLARE past_due EXCEPTION; BEGIN RAISE past_due; EXCEPTION WHEN past_due THEN RAISE NOTICE 'Exception Does Not Propagate'; END; END; / \set SQLTERM ;
调用存储过程:
CALL P();
输出结果:
NOTICE: Exception Does Not Propagate
异常在内部块中被触发,但未被捕获,传播到上层块中被捕获处理,如 示例 12-13_ 所示。
示例 12-13 从内部块传播到外部块的异常
\set SQLTERM / CREATE OR REPLACE PROCEDURE p AS BEGIN DECLARE past_due EXCEPTION; BEGIN RAISE past_due; END; EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'Exception Propagates from Inner Block to Outer Block'; END; / \set SQLTERM ;
调用存储过程:
CALL P();
输出结果:
NOTICE: Exception Propagates from Inner Block to Outer Block
当前块中声明阶段抛出的异常会直接传播到上层块或者主机环境,而不是当前块。因此,声明阶段的异常处理程序必须在调用块中,而不是在声明的当前块中。
在 示例 12-14_ 中,VALUE_ERROR 异常处理程序与引发 VALUE_ERROR 的声明位于同一块中。 因为异常会立即传播到主机环境,所以异常处理程序不会处理它。
示例 12-15_ 与 示例 12-14_ 类似,只是上层块中处理了内部块中的声明中引发的 VALUE_ERROR 异常。
示例 12-14 声明中触发的异常未处理
\set SQLTERM / DECLARE credit_limit CONSTANT NUMBER(3) := 5000; BEGIN NULL; EXCEPTION WHEN VALUE_ERROR THEN RAISE NOTICE 'Exception raised in declaration.'; END; / \set SQLTERM ;
输出结果:
ERROR: numeric field overflow DETAIL: A field with precision 3, scale 0 must round to an absolute value less than 10^3. CONTEXT: PL/SQL function inline_code_block line 3 during statement block local variable initialization
示例 12-15 声明中触发的异常由上层块处理
\set SQLTERM / BEGIN DECLARE credit_limit CONSTANT NUMBER(3) := 5000; BEGIN NULL; END; EXCEPTION WHEN VALUE_ERROR THEN RAISE NOTICE 'Exception raised in declaration.'; END; / \set SQLTERM ;
输出结果:
NOTICE: Exception raised in declaration.
异常处理阶段抛出的异常与声明阶段抛出的异常一样,会直接传播到上层块或者主机环境中。因此,异常处理程序必须位于上层块或者调用块中。
在 示例 12-16_ 中,当 n 为零时,计算 1/n 会引发预定义的异常 ZERO_DIVIDE,并且控制权转移到同一块中的 ZERO_DIVIDE 异常处理程序。当异常处理程序触发 ZERO_DIVIDE 时,异常会立即传播到调用程序。调用者不处理异常,所以 PL/SQL 向主机环境返回一个未处理的异常错误。
示例 12-17_ 与 示例 12-16_ 类似,只是当存储过程向调用者返回未处理的异常错误时,调用者会处理它。
示例 12-18_ 与 示例 12-17_ 类似,只是由上层块处理内部块中的异常处理程序引发的异常。
在 示例 12-19_ 中,存储过程中的异常处理部分具有用于处理用户自定义异常 i_is_one 和预定义异常 ZERO_DIVIDE 的异常处理程序。当 i_is_one 异常处理程序引发 ZERO_DIVIDE 时,异常会立即传播到调用程序中(因此,ZERO_DIVIDE 异常处理程序不会处理它)。调用者不处理异常,所以 PL/SQL 向主机环境返回一个未处理的异常错误。
示例 12-20_ 与 示例 12-19_ 类似,只是上层中块处理了 i_is_one 异常处理程序引发的 ZERO_DIVIDE 异常。
示例 12-16 异常处理程序中触发的异常未处理
\set SQLTERM / CREATE OR REPLACE PROCEDURE print_reciprocal (n NUMBER) AUTHID DEFINER IS BEGIN RAISE NOTICE '%', 1/n; EXCEPTION WHEN ZERO_DIVIDE THEN RAISE NOTICE 'Error:'; RAISE NOTICE '% is undefined', 1/n; END; / \set SQLTERM ;
调用存储过程:
\set SQLTERM / BEGIN -- invoking block print_reciprocal(0); END; / \set SQLTERM ;
输出结果:
NOTICE: Error: ERROR: division by zero CONTEXT: PL/SQL function print_reciprocal(numeric) line 7 at RAISE SQL statement "CALL print_reciprocal(0)" PL/SQL function inline_code_block line 2 at CALL
示例 12-17 异常处理程序中引发的异常由调用程序处理
\set SQLTERM / CREATE OR REPLACE PROCEDURE print_reciprocal (n NUMBER) AUTHID DEFINER IS BEGIN RAISE NOTICE '%', 1/n; EXCEPTION WHEN ZERO_DIVIDE THEN RAISE NOTICE 'Error:'; RAISE NOTICE '% is undefined', 1/n; END; / \set SQLTERM ;
调用存储过程:
\set SQLTERM / BEGIN -- invoking block print_reciprocal(0); EXCEPTION WHEN ZERO_DIVIDE THEN -- handles exception raised in exception handler RAISE NOTICE '1/0 is undefined.'; END; / \set SQLTERM ;
输出结果:
NOTICE: Error: NOTICE: 1/0 is undefined.
示例 12-18 异常处理程序中引发的异常由上层块处理
set serverout on \set SQLTERM / CREATE OR REPLACE PROCEDURE print_reciprocal (n NUMBER) AUTHID DEFINER IS BEGIN BEGIN DBMS_OUTPUT.PUT_LINE(1/n); EXCEPTION WHEN ZERO_DIVIDE THEN DBMS_OUTPUT.PUT_LINE('Error in inner block:'); DBMS_OUTPUT.PUT_LINE(1/n || ' is undefined.'); END; EXCEPTION WHEN ZERO_DIVIDE THEN -- handles exception raised in exception handler DBMS_OUTPUT.PUT('Error in outer block: '); DBMS_OUTPUT.PUT_LINE('1/0 is undefined.'); END; / \set SQLTERM ;
调用存储过程:
\set SQLTERM / BEGIN print_reciprocal(0); END; / \set SQLTERM ;
输出结果:
Error in inner block: Error in outer block: 1/0 is undefined.
示例 12-19 未处理异常处理程序中引发的异常
set serverout on \set SQLTERM / CREATE OR REPLACE PROCEDURE descending_reciprocals (n INTEGER) AUTHID DEFINER IS i INTEGER; i_is_one EXCEPTION; BEGIN i := n; LOOP IF i = 1 THEN RAISE i_is_one; ELSE DBMS_OUTPUT.PUT_LINE('Reciprocal of ' || i || ' is ' || 1/i); END IF; i := i - 1; END LOOP; EXCEPTION WHEN i_is_one THEN DBMS_OUTPUT.PUT_LINE('1 is its own reciprocal.'); DBMS_OUTPUT.PUT_LINE('Reciprocal of ' || TO_CHAR(i-1) || ' is ' || TO_CHAR(1/(i-1))); WHEN ZERO_DIVIDE THEN DBMS_OUTPUT.PUT_LINE('Error:'); DBMS_OUTPUT.PUT_LINE(1/n || ' is undefined'); END; / \set SQLTERM ;
调用存储过程:
\set SQLTERM / BEGIN descending_reciprocals(3); END; / \set SQLTERM ;
输出结果:
ERROR: division by zero CONTEXT: SQL statement "CALL DBMS_OUTPUT.PUT_LINE('Reciprocal of ' || TO_CHAR(i-1) || ' is ' || TO_CHAR(1/(i-1)))" PL/SQL function descending_reciprocals(integer) line 17 at CALL SQL statement "CALL descending_reciprocals(3)" PL/SQL function inline_code_block line 2 at CALL Reciprocal of 3 is 0 Reciprocal of 2 is 0 1 is its own reciprocal.
示例 12-20 异常处理程序中引发的异常由上层块处理
set serverout on \set SQLTERM / CREATE OR REPLACE PROCEDURE descending_reciprocals (n INTEGER) AUTHID DEFINER IS i INTEGER; i_is_one EXCEPTION; BEGIN BEGIN i := n; LOOP IF i = 1 THEN RAISE i_is_one; ELSE DBMS_OUTPUT.PUT_LINE('Reciprocal of ' || i || ' is ' || 1/i); END IF; i := i - 1; END LOOP; EXCEPTION WHEN i_is_one THEN DBMS_OUTPUT.PUT_LINE('1 is its own reciprocal.'); DBMS_OUTPUT.PUT_LINE('Reciprocal of ' || TO_CHAR(i-1) || ' is ' || TO_CHAR(1/(i-1))); WHEN ZERO_DIVIDE THEN DBMS_OUTPUT.PUT_LINE('Error:'); DBMS_OUTPUT.PUT_LINE(1/n || ' is undefined'); END; EXCEPTION WHEN ZERO_DIVIDE THEN -- handles exception raised in exception handler DBMS_OUTPUT.PUT_LINE('Error:'); DBMS_OUTPUT.PUT_LINE('1/0 is undefined'); END; / \set SQLTERM ;
调用存储过程:
\set SQLTERM / BEGIN descending_reciprocals(3); END; / \set SQLTERM ;
输出结果:
Reciprocal of 3 is 0 Reciprocal of 2 is 0 1 is its own reciprocal. Error: 1/0 is undefined
异常在内部块中被触发,但未被捕获,且传播到外层块中。仍未被捕获,最终返回到主机环境中。
在打开单语句回滚的情况下,如果程序因为未处理的异常退出,PL/SQL 不会回滚程序所做的数据库更改。
提示
通过在每个 PL/SQL 程序的顶层包含一个 OTHERS 异常处理程序来避免未处理的异常。
示例 12-21 未处理异常
\set SQLTERM / CREATE OR REPLACE PROCEDURE p AS BEGIN DECLARE past_due EXCEPTION; PRAGMA EXCEPTION_INIT (past_due, -4910); BEGIN RAISE past_due; END; END; / \set SQLTERM ;
调用存储过程:
CALL P();
输出结果:
ERROR: ERRCODE-4910: non-KingbaseES exception CONTEXT: PL/SQL function p() line 7 at RAISE
在异常处理程序中,对于正在处理的异常:
您可以使用 SQLCODE 函数 中描述的 PL/SQL 函数 SQLCODE 检索错误代码。
您可以使用以下任一方法检索错误消息:
PL/SQL 函数 SQLERRM,详见 SQLERRM 函数 。
该函数最多返回 512 字节,这是数据库错误消息的最大长度(包括错误代码、详细的错误信息等)。
PL/SQL 系统包 DBMS_UTILITY 中的包函数 DBMS_UTILITY.FORMAT_ERROR_STACK
此函数返回完整的错误堆栈,最多 2000 个字节。
KES 数据库提供的其它异常信息检索方式。
另请参阅
PL/SQL 系统包中有关 DBMS_UTILITY.FORMAT_ERROR_BACKTRACE 函数的信息,该函数返回引发异常时的调用堆栈,即使该程序是从上层块的异常处理程序中被调用的
PL/SQL 系统包中有关 UTL_CALL_STACK 包的信息,其子程序提供有关当前正在执行的子程序的信息,包括子程序名称
异常发生时,调用 SQLSTATE 变量返回当前异常对应的 SQL 标准错误码(五位字符串)。
异常发生时,调用 SQLERRM 变量返回当前异常相应的错误信息。
注意
变量 SQLSTATE 和 SQLERRM 仅在异常处理模块中可用。
语法:
INT SQLCODE () ;
说明:
检索异常兼容 ORACLE 的错误码(负整数)。在异常处理程序之外,SQLCODE 函数始终返回 0, 表示程序正常编译执行。
对于系统预定义的异常,错误码是关联相关异常错误的编号,该数字通常为一个负整数( NO_DATA_FOUND 除外,其错误码为 +100)。
对于用户自定义的异常,数字代码为 +1(默认值)或者通过 EXCEPTION_INIT 方法绑定的错误码。
语法:
TEXT SQLERRM ( [ error_code ] ) ;
说明:
检索异常错误信息。
其中 error_code 为数据库异常对应的错误码,省略该参数时返回当前正在处理的异常错误信息。当 error_code 不是一个标准的错误码或者不存在时,都会进行相应的错误提示。
语法:
INT ERROR_LINE () ;
说明:
返回抛出异常的函数(存储过程)中抛出异常的位置行号,若没有在异常处理块中,则返回 NULL。
语法:
VARCHAR(128) ERROR_PROCEDURE () ;
说明:
返回抛出异常的函数(存储过程)或触发器的名称,若没有在异常处理块中,则返回 NULL。
语法:
INT ERROR_NUMBER () ;
说明:
返回抛出异常的错误号,与 SQLCODE 相同。若没有在异常处理块中,则返回 NULL。
语法:
VARCHAR(4000) ERROR_MESSAGE () ;
说明:
返回异常消息的完整文本,与 SQLERRM 相同。若没有在异常处理块中,则返回 NULL。
语法:
INT ERROR_STATE () ;
说明:
返回异常状态号,在异常处理块中始终为 1。若没有在异常处理块中,则返回 NULL。
语法:
INT ERROR_SEVERITY () ;
说明:
返回异常的严重级别,在异常处理块中始终为 20。若没有在异常处理块中,则返回 NULL。
语法:
SETOF RECORD SYS_GET_PREDEFINED_EXCEPTION_DETAIL ( OUT exception_name TEXT, OUT sqlstate TEXT, OUT sqlcode INTEGER) ;
说明:
返回所有的预定义异常的相关信息, exception_name 为预定义异常名称, sqlstate 为预定义异常的 SQL 标准错误码, sqlcode 为预定义异常的 ORACLE 兼容错误码, 如 示例 12-23_ 所示。
示例 12-22 SQLCODE 函数和 SQLERRM 函数
set serverout on \set SQLTERM / CREATE OR REPLACE PROCEDURE p AUTHID DEFINER AS i INT; v_code NUMBER; v_errm VARCHAR2(64); BEGIN i := 1/0; EXCEPTION WHEN OTHERS THEN v_code := SQLCODE; v_errm := SUBSTR(SQLERRM, 1, 64); DBMS_OUTPUT.PUT_LINE ('Error code ' || v_code || ': ' || v_errm); END; / \set SQLTERM ;
调用存储过程:
CALL P();
输出结果:
Error code -1476: division by zero
示例 12-23 SYS_GET_PREDEFINED_EXCEPTION_DETAIL 函数
SELECT * FROM SYS_GET_PREDEFINED_EXCEPTION_DETAIL();
输出结果:
exception_name | sqlstate | sqlcode ------------------------------------------------------+----------+--------- sql_statement_not_yet_complete | 03000 | -10000 connection_exception | 08000 | -10001 connection_does_not_exist | 08003 | -10002 ... tablespace_is_readonly | F1001 | -10243 tablespace_is_write | F1002 | -10244 tablespace_operation_notsupport | F1003 | -10245
异常捕获是异常处理程序的一部分,通常通过 EXCEPTION 子句对异常进行捕获操作。
通常情况下,一个语句引发异常,将导致当前语句退出执行,与该语句处于同一个事务内的语句也会被回滚。为了处理异常,PL/SQL 块中可以通过 EXCEPTION 子句来捕获异常,进行相应处理,这时,处于同一个事务块中已经执行的的语句不会被回滚,但当前语句被回滚。
KingbaseES 的 PL/SQL 中,当一条语句执行后,控制权将移交到下一条语句,但是当异常触发后,KingbaseES 的 PL/SQL 将立即捕获、处理异常。
语法格式:
[ DECLARE [ < VariableDeclaration > ] [ < CursorDeclaration > ] [ < UserDefinedExceptionDeclaration > ] ] BEGIN < Statements > EXCEPTION WHEN ExceptionName [ OR ExceptionName... ] THEN < HandlerStatements >; [ WHEN ExceptionName[ OR ExceptionName... ] THEN < HandlerStatements >; ... ] END;
功能:
捕获异常。
使用说明:
ExceptionName,异常的名称,系统预先定义或用户自定义异常,直接使用即可,异常名称大小写均可,如 division_by_zero ,表示发生 "除零" 错误。异常名是与大小写无关的,一个异常名也可以通过 SQLSTATE 代码指定,例如以下是等价的:
WHEN division_by_zero THEN ... WHEN SQLSTATE '22012' THEN ...
ExceptionName 可以使用关键字 OTHERS ,用于处理在 OTHERS 之前没有显示指定处理的异常。
如果没有发生异常,这种形式的块只是简单地执行所有 Statements , 然后转到 END 之后的下一个语句。但是如果在 Statements 内发生了一个错误,则会放弃对 Statements 的进一步处理,然后转到 EXCEPTION 子句。系统会在异常条件列表中匹配当前触发的异常。如果匹配成功,则执行对应的 HandlerStatements ,执行完成后转到 END 之后的下一个语句。如果匹配失败,则该异常就会传播出去,就像没有 EXCEPTION 子句一样。异常可以被外层闭合块中的 EXCEPTION 子句捕获,如果没有 EXCEPTION 则中止该程序的执行。
如果在选中的 HandlerStatements 内触发新的异常,那么它不能被当前这个 EXCEPTION 子句捕获,而是被传播出去。由外层的 EXCEPTION 子句捕获它。
当一个异常被 EXCEPTION 子句捕获时,PL/SQL 函数的局部变量会保持异常触发时的值,但是该块中所有对数据库状态的改变都会被回滚。如 示例 12-24_ 所示:
如果 KES 数据库打开了单语句回滚的参数(ora_statement_level_rollback),触发异常的语句不会回滚之前已经执行完成的操作。如 示例 12-25_ 所示:
进入和退出一个包含 EXCEPTION 子句的块要比不包含 EXCEPTION 的块开销大的多。因此,尽量在必要的时候使用 EXCEPTION 子句。
示例 12-24 异常触发导致 PL/SQL 函数内已完成的操作被回滚
DROP TABLE IF EXISTS mytab CASCADE; CREATE TABLE mytab(firstname TEXT, lastname TEXT); INSERT INTO mytab(firstname, lastname) VALUES('Tom', 'Jones'); \set SQLTERM / CREATE OR REPLACE PROCEDURE PROC() AS x int := 1; BEGIN UPDATE mytab SET firstname = 'Joe' WHERE lastname = 'Jones'; x := x / 0; EXCEPTION WHEN division_by_zero THEN RAISE NOTICE 'caught division_by_zero'; END; / \set SQLTERM ;
调用函数:
CALL PROC(); NOTICE: caught division_by_zero
查询更改:
SELECT * FROM mytab; firstname | lastname -----------+---------- Tom | Jones (1 row)
当函数执行到对 y 赋值的地方时,它会触发一个 division_by_zero 异常,同时这个异常将被 EXCEPTION 子句捕获。而在 RETURN 语句中返回的值将是 x 执行加法过后的值。不过,在该块之前的 INSERT 将不会被回滚,因此最终的结果是数据库包含 'Tom Jones' 但不包含 'Joe Jones' 。
示例 12-25 异常触发未影响 PL/SQL 函数内已完成的操作
set ora_statement_level_rollback to on; DROP TABLE IF EXISTS mytab CASCADE; CREATE TABLE mytab(firstname TEXT, lastname TEXT); INSERT INTO mytab(firstname, lastname) VALUES('Tom', 'Jones'); \set SQLTERM / CREATE OR REPLACE PROCEDURE PROC() AS x int := 1; BEGIN UPDATE mytab SET firstname = 'Joe' WHERE lastname = 'Jones'; x := x / 0; EXCEPTION WHEN division_by_zero THEN RAISE NOTICE 'caught division_by_zero'; END; / \set SQLTERM ;
调用函数:
CALL PROC(); NOTICE: caught division_by_zero
查询更改:
SELECT * FROM mytab; firstname | lastname -----------+---------- Joe | Jones (1 row)
在异常处理模块中,除系统变量和系统函数外,KingbaseES 也支持使用 GET STACKED DIAGNOSTICS 命令获取有关当前异常的状态信息,该命令的语法为:
GET STACKED DIAGNOSTICS variable { = | := } item [ , ... ];
使用说明:
每个 item 都是一个异常相关状态项。下表中显示了当前可用的状态项。
表 12.10.1 异常状态项
名称 | 类型 | 描述 |
---|---|---|
|
| 该异常的 SQLSTATE 错误代码 |
|
| 与异常相关的列名 |
|
| 与异常相关的约束名 |
|
| 与异常相关的数据类型名 |
|
| 该异常的主要消息的文本 |
|
| 与异常相关的表名 |
|
| 与异常相关的模式名 |
|
| 该异常的详细消息文本 |
|
| 该异常的提示消息文本 |
|
| 描述产生异常时调用栈 的文本行 |
如果触发的异常无法为某个状态项设置值,将对应的状态项将返回一个空字符串。
DECLARE text_var1 TEXT; text_var2 TEXT; text_var3 TEXT; BEGIN -- 某些可能导致异常的处理 ... EXCEPTION WHEN OTHERS THEN GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT, text_var2 = SYS_EXCEPTION_DETAIL, text_var3 = SYS_EXCEPTION_HINT; END;
GET DIAGNOSTICS (见 获得结果状态 中描述)命令可获取有关当前执行状态的信息。该命令的 SYS_CONTEXT 状态项可用于记录当前执行位置。状态项 SYS_CONTEXT 会返回一个文本字符串,该字符串为一个描述该调用栈的多行文本。第一行会指向当前函数以及当前正在执行 GET DIAGNOSTICS 命令的位置。第二行及其后的行表示调用栈中更上层的调用位置信息。
语法格式:
GET DIAGNOSTICS stack = SYS_CONTEXT;
示例 12-26 获取执行位置信息 .. code:
\set SQLTERM / CREATE OR REPLACE FUNCTION outer_func RETURN integer AS BEGIN RETURN inner_func(); END; / \set SQLTERM ; \set SQLTERM / CREATE OR REPLACE FUNCTION inner_func RETURN integer AS stack text; BEGIN GET DIAGNOSTICS stack = SYS_CONTEXT; RAISE NOTICE E'--- Call Stack ---\n%', stack; RETURN 1; END; / \set SQLTERM ; SELECT outer_func(); NOTICE: --- Call Stack --- PL/SQL function INNER_FUNC() line 5 at GET DIAGNOSTICS PL/SQL function OUTER_FUNC() line 3 at RETURN OUTER_FUNC ------------ 1 (1 row)
GET STACKED DIAGNOSTICS ... SYS_EXCEPTION_CONTEXT 语句返回类似的栈跟踪,但是 SYS_EXCEPTION_CONTEXT 记录的是检测到异常的位置而不是当前位置。
ASSERT
语句是一种向 PL/SQL 函数中插入调试检查的方法。
语法格式:
ASSERT condition [ , message ];
使用说明:
condition 是一个布尔表达式,该表达式的计算结果被断言为真。如果结果为真, ASSERT 语句不会对程序产生任何影响。但如果结果是假或者空,那么将发生一个 ASSERT_FAILURE 异常(如果在计算 condition 时发生错误,会抛出一个对应的普通异常)。
如果 message 参数非空, 则在断言失败时, message 会被用来替换默认的错误消息文本 ("assertion failed") 。 message 表达式在断言成功的情况下不会对程序产生任何影响。
通过配置参数 plsql.check_asserts 可以启用或者禁用断言测试, 这个参数为布尔值且默认为 on 。如果这个参数值被设置为 off ,则 ASSERT 语句什么也不做。
ASSERT 是为了检测程序的 bug,而不是报告普通的错误情况。如果要报告普通错误,请使用前面介绍的 打印语句 。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。