本文介绍了错误和消息的相关内容。
报告错误和消息
使用RAISE
语句报告消息以及抛出错误。
RAISE [ level ] 'format' [, expression [, ... ]] [ USING option = expression [, ... ] ];
RAISE [ level ] condition_name [ USING option = expression [, ... ] ];
RAISE [ level ] SQLSTATE 'sqlstate' [ USING option = expression [, ... ] ];
RAISE [ level ] USING option = expression [, ... ];
RAISE ;
level
选项指定了错误的严重性。允许的级别有DEBUG
、LOG
、INFO
、NOTICE
, WARNING
以及EXCEPTION
,默认级别是EXCEPTION
。EXCEPTION
会抛出一个错误(通常会中止当前事务)。其他级别仅仅是产生不同优先级的消息。不管一个特定优先级的消息是被报告给客户端、还是写到服务器日志、亦或是二者同时都做,这都由log_min_messages
和client_min_messages
配置变量控制。
如果有level
, 在它后面可以写一个format
( 它必须是一个简单字符串而不是表达式)。该格式字符串指定要被报告的错误消息文本。在格式字符串后面可以跟上可选的要被插入到该消息的参数表达式。在格式字符串中,%
会被下一个可选参数的值所替换。写%%
可以发出一个字面的 %
。参数的数量必须匹配格式字符串中%
占位符的数量,否则在函数编译期间就会发生错误。
在这个例子中,v_job_id
的值将替换字符串中的%
:
RAISE NOTICE 'Calling cs_create_job(%)', v_job_id;
通过写一个后面跟着option
= expression
项的USING
,可以为错误报告附加一些额外信息。每一个expression
可以是任意字符串值的表达式。允许的option
关键词是:
MESSAGE
设置错误消息文本。这个选项可以被用于在USING
之前包括一个格式字符串的RAISE
形式。
DETAIL
提供一个错误的细节消息。
HINT
提供一个提示消息。
ERRCODE
指定要报告的错误代码(SQLSTATE)。
COLUMN
CONSTRAINT
DATATYPE
TABLE
SCHEMA
提供一个相关对象的名称。
这个例子将用给定的错误消息和提示中止事务:
RAISE EXCEPTION 'Nonexistent ID --> %', user_id
USING HINT = 'Please check your user ID';
这两个例子展示了设置 SQLSTATE 的两种等价的方法:
RAISE 'Duplicate user ID: %', user_id USING ERRCODE = 'unique_violation';
RAISE 'Duplicate user ID: %', user_id USING ERRCODE = '23505';
还有第二种RAISE
语法,在其中主要参数是要被报告的条件名或 SQLSTATE,例如:
RAISE division_by_zero;
RAISE SQLSTATE '22012';
在这种语法中,USING
能被用来提供一个自定义的错误消息、细节或提示。另一种做前面的例子的方法是
RAISE unique_violation USING MESSAGE = 'Duplicate user ID: ' || user_id;
仍有另一种变体是写RAISE USING
或者RAISE ``level`` USING
并且把所有其他东西都放在USING
列表中。
RAISE
的最后一种变体根本没有参数。这种形式只能被用在一个BEGIN
块的EXCEPTION
子句中,它导致当前正在被处理的错误被重新抛出。
在之前数据库中,没有参数的RAISE
被解释为重新抛出来自包含活动异常处理器的块的错误。因此一个嵌套在那个处理器中的EXCEPTION
子句无法捕捉它,即使RAISE
位于嵌套EXCEPTION
子句的块中也是这样。这种行为很奇怪,也并不兼容 Oracle 的 PL/SQL。
如果在一个RAISE EXCEPTION
命令中没有指定条件名以及 SQLSTATE,默认是使用ERRCODE_RAISE_EXCEPTION
(P0001
)。如果没有指定消息文本,默认是使用条件名或 SQLSTATE 作为消息文本。
当用 SQLSTATE 代码指定一个错误代码时,你不会受到预定义错误代码的限制,而是可以选择任何由五位以及大写 ASCII 字母构成的错误代码,只有00000
不能使用。我们推荐尽量避免抛出以三个零结尾的错误代码,因为这些是分类代码并且只能用来捕获整个类别。
检查断言
ASSERT
语句是一种向 PL/SQL函数中插入调试检查的方便方法。
ASSERT condition [ , message ];
condition
是一个布尔表达式,它被期望总是计算为真。如果确实如此, ASSERT
语句不会再做什么。但如果结果是假或者空,那么将发生一个ASSERT_FAILURE
异常(如果在计算 condition
时发生错误, 它会被报告为一个普通错误)。
如果提供了可选的message
, 它是一个结果(如果非空)被用来替换默认错误消息文本 “assertion failed”的表达式(如果 condition
失败)。 message
表达式在断言成功的普通情况下不会被计算。
通过配置参数plpgsql.check_asserts
可以启用或者禁用断言测试, 这个参数接受布尔值且默认为on
。如果这个参数为off
, 则ASSERT
语句什么也不做。
ASSERT
是为了检测程序的 bug,而不是报告普通的错误情况。如果要报告普通错误,请使用前面介绍的 RAISE
语句。
异常
简介
异常(PL/SQL 运行时错误)可能由设计错误、编码错误、硬件故障和许多其他原因引起。您无法预见所有可能的异常,但您可以编写异常处理程序,让您的程序在出现异常时继续运行。任何PL/SQL块都可以有一个异常处理部分,该部分可以有一个或多个异常处理程序。例如,异常处理部分可以使用以下语法,其中ex_name_n是异常的名称,statements_n是一个或多个语句。
DECLARE
...
BEGIN
...
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;
当PL块的可执行部分(BEGIN 块)发生异常时,可执行部分停止,并将控制权转移到异常处理部分。在内部,PolarDB通过异常码控制异常条件的引发,每一个ex_name_n在内部都对应了一个唯一的异常码。如果ex_name_1被引发,则statements_1运行。如果引发了ex_name_2或ex_name_3,则statements_2运行。如果引发任何其他异常,则statements_3运行。OTHERS常作为异常捕获的兜底判断来使用,如果没有OTHERS,当发生了不被任何ex_name_n捕获的异常时,该异常将会被抛出,并由调用该PL块的调用方的EXCEPTION段继续处理该异常。如果没有任何PL块能处理,最终将异常抛给调用程序。
使用异常处理程序进行错误处理可以使程序更易于编写和理解,并降低出现未处理异常的可能性。如果没有异常处理程序,您必须在可能发生异常的地方检查每个可能的错误,然后进行处理。人们很容易忽视可能的错误或可能发生错误的位置,特别是如果错误无法立即检测到(例如,错误的数据可能无法检测到,直到您在计算中使用它为止)。
使用异常处理程序,您不需要知道每个可能的错误或可能发生的任何地方,而只需要在可能发生错误的每个块中包含异常处理部分。在异常处理部分中,可以包含特定错误和未知错误的异常处理程序。如果块中的任何位置(包括子块内部)发生错误,则异常处理程序将处理它。错误处理代码被隔离在块的异常处理部分中。
内部异常
内部异常是系统运行时引发的内部定义的异常。在PolarDB中,每一个内部异常都通过一个唯一的五位字符来表示,并且它们中的大多数都具有一个唯一的异常名称来标识。
常见的示例有除零异常('22012', division_by_zero ),以及数组下标错误异常( '2202E',array_subscript_error)。可以使用以下两种方式来表达ex_name_n。
EXCEPTION
WHEN sqlstate '22012' THEN
...
WHEN array_subscript_error THEN
...
同时,这些异常可以通过RAISE主动抛出。
DECLARE
BEGIN
RAISE division_by_zero; -- 主动抛出除零异常
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'catch exception!';
RAISE; -- 再次抛出!
END;
在EXCEPTION块有一个特殊语法RAISE,这种语法只允许在EXCEPTION中使用,可以直接继续抛出当前触发的异常。
预定义异常
在PolarDB中,内部异常都有一个对应的异常名称,因此,可以通过这些异常名称直接捕获对应的异常。此外,PolarDB提供了一些Oracle兼容的异常名称以及和PolarDB的原生异常名称的对应关系,如下表所示:
Oracle错误码 | Oracle错误码名称 | PolarDB错误码 | PolarDB错误码名称 |
-6592 | case_not_found | 20000 | case_not_found |
-6531 | collection_is_null | 2203G | collection_is_null |
-6511 | cursor_already_open | 42P03 | duplicate_cursor |
-1 | dup_val_on_index | 23505 | unique_violation |
-6533 | subscript_beyond_count | 2203H | subscript_beyond_count |
-6532 | subscript_outside_limit | 2202E | array_subscript_error |
-1422 | too_many_rows | P0003 | too_many_rows |
-1476 | zero_divide | 22012 | division_by_zero |
您可以使用这些Oracle风格的异常名称来完成上述操作。
自定义异常
可以通过以下语法声明一个自定义异常,抛出并捕获它:
DECLARE
exception_name EXCEPTION;
BEGIN
RAISE exception_name;
EXCEPTION
WHEN exception_name THEN
RAISE NOTICE 'catch user-defined exception!';
RAISE NOTICE '% : %', SQLCODE, SQLERRM;
END;
未绑定异常码的自定义异常关联着一个内部的异常码表示,对于用户来说,通过SQLCODE获取到的异常码为1,SQLERRM获取到的异常信息为'User-Defined Exception'
。
如果希望绑定一个特定的异常码,并在抛出自定义异常的时候关联一个特定的异常信息,可以使用以下语法:
DECLARE
my_exception EXCEPTION;
PRAGMA EXCEPTION_INIT (my_exception, -20001); -- 绑定异常码
BEGIN
RAISE_APPLICATION_ERROR(-20001, 'raise a special division by zero exception!'); -- 抛出特定异常码的异常,并为其绑定一个异常信息
EXCEPTION
WHEN my_exception THEN -- WHEN zero_divide/division_by_zero THEN
RAISE NOTICE '% : %', SQLCODE, SQLERRM;
END;
结果显示如下:
NOTICE: -20001 : raise a special division by zero exception!
您可以通过以下语法来为自定义异常绑定一个异常号。这个异常号必须是个Oracle风格的负数。
PRAGMA EXCEPTION_INIT (exception_name, error_code);
然后通过过程RAISE_APPLICATION_ERROR来抛出一个指定了异常信息和异常号的异常。此处的异常号取值范围为:-20000~-20999。
RAISE_APPLICATION_ERROR(error_code, 'raise a special exception!');
如果您创建了一个与预定义异常名称相同的自定义异常,那么您定义的异常变量将会覆盖预定义异常。但是PolarDB并不建议您这样做。
DECLARE
zero_divide EXCEPTION;
BEGIN
RAISE zero_divide;
EXCEPTION
WHEN zero_divide THEN
RAISE NOTICE '% %', sqlcode, sqlerrm;
END;
结果显示如下:
NOTICE: 1 User-Defined Exception
主动抛出异常
除了因为系统出错导致自动抛出内部异常外,您可以采用以下三种方式主动抛出异常。示例如下:
DECLARE
my_exception EXCEPTION;
BEGIN
-- 嵌套块
DECLARE
BEGIN
RAISE my_exception; -- 1. 抛出指定异常
EXCEPTION
WHEN my_exception THEN
RAISE NOTICE 'catch inner exception!';
RAISE; -- 2. 抛出当前异常
END;
EXCEPTION
WHEN my_exception THEN
RAISE_APPLICATION_ERROR(-20000, 'raise a special exception!'); -- 3. 通过过程 RAISE_APPLICATION_ERROR 抛出特定异常号和特定异常信息
END;
显示结果如下:
NOTICE: catch inner exception!
ERROR: raise a special exception!
异常传播
如果在没有异常处理程序的块中引发异常,则该异常会向外抛出,直到有一个块具有其处理程序为止。如果没有处理程序处理异常,则PL/SQL会向调用程序或主机环境返回未处理的异常错误,由它来决定结果。如果异常被某一个块处理,那将在处理结束后继续正常执行外层块的下一条语句(或是在没有外层块的情况下直接正常返回到调用程序或主机环境)。
获取异常号和异常信息
如果异常没有被捕获,您将直接在控制台或其他调用方中看到异常信息,但无法看到异常号。可以在捕获异常后通过 SQLCODE
获取异常号,通过 SQLERRM
获取异常信息,如下所示:
DECLARE
...
BEGIN
...
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '% %', sqlcode, sqlerrm;
END;
事务行为
PL/SQL程序总是运行在一个事务中。当具有EXCEPTION块的时候,PolarDB会隐式的创建子事务来执行BEGIN块。当BEGIN块发生异常时,子事务将会被回滚,然后重新开一个子事务继续执行EXCEPTION块里的语句。如果EXCEPTION块的语句正常执行,那么该事务可以被正常提交,如下所示:
-- 准备测试表
CREATE TABLE test(id INT);
-- BEGIN 块抛出异常, EXCEPTION 块正常执行
DECLARE
my_exception EXCEPTION;
BEGIN
INSERT INTO test VALUES(1);
RAISE my_exception;
EXCEPTION
WHEN my_exception THEN
RAISE NOTICE 'catch!';
INSERT INTO test VALUES(2);
END;
-- 查表
SELECT id FROM test;
显示结果如下:
NOTICE: catch!
DO
postgres=# select * from t;
id
----
2
(1 row)
如果你希望保留BEGIN块中完成的部分,您需要进行显式提交,或是开启语句级事务。当显式提交的时候,子事务和父事务将会一并提交,然后重新开启一个事务和子事务来执行后续语句。
PL/SQL中的事务控制语句仅在顶层过程的PL/SQL块和匿名块中被允许使用。
-- 清空测试表
DELETE FROM test;
DECLARE
my_exception EXCEPTION;
BEGIN
INSERT INTO test VALUES(1);
COMMIT; -- 提交
RAISE my_exception;
EXCEPTION
WHEN my_exception THEN
RAISE NOTICE 'catch!';
INSERT INTO test VALUES(2);
END;
-- 查表
SELECT id FROM test;
结果显示如下:
NOTICE: catch!
DO
postgres=# select * from t;
id
----
1
2
(2 rows)