全部產品
Search
文件中心

PolarDB:錯誤和訊息

更新時間:Jul 27, 2024

本文介紹了錯誤和訊息的相關內容。

報告錯誤和訊息

使用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)