错误和消息

本文介绍了错误和消息的相关内容。

报告错误和消息

使用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选项指定了错误的严重性。允许的级别有DEBUGLOGINFONOTICE, WARNING以及EXCEPTION,默认级别是EXCEPTIONEXCEPTION会抛出一个错误(通常会中止当前事务)。其他级别仅仅是产生不同优先级的消息。不管一个特定优先级的消息是被报告给客户端、还是写到服务器日志、亦或是二者同时都做,这都由log_min_messagesclient_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)