Oracle-PL/SQL錯誤處理

 

控制PL/SQL錯誤 EXCEPTION,SQLCODE,SQLERRM --瀏覽時可通過查找功能跳躍式的尋找自己要查看的內容!

一、錯誤控制一覽

在PL/SQL中,警告或錯誤被稱爲異常。異常可以是內部(運行時系統)定義的或是用戶定義的。內部定義的案例包括除零操作和內存溢出等。一些常見的內部異常都有一個預定義的名字,如ZERO_DIVIDE和STORAGE_ERROR等。對於其它的內部異常,我們可以手動爲它們命名。

我們可以在PL/SQL塊、子程序或包的聲明部分自定義異常。例如,我們可以定義一個名爲insufficient_funds的異常來標示帳戶透支的情況。與內部異常不同的是,用戶自定義異常必須有一個名字。

錯誤發生時,異常就會被拋出。也就是說,正常的執行語句會被終止,控制權被轉到PL/SQL塊的異常控制部分或子程序的異常控制部分。內部異常會由運行時系統隱式地拋出,而用戶定義異常必須顯式地用RAISE語句拋出,RAISE語句也可以拋出預定義異常。

爲了控制被拋出的異常,我們需要單獨編寫被稱爲"exception handler"的異常控制程序。異常控制程序運行後,當前塊就會停止執行,封閉塊繼續執行下一條語句。如果沒有封閉塊,控制權會直接交給主環境。

下例中,我們爲一家股票代碼(Ticker Symbol)爲XYZ的公司計算並保存市盈率(price-to-earning)。如果公司的收入爲零,預定義異常ZERO_DIVIDE就會被拋出。這將導致正常的執行被終止,控制權被交給異常控制程序。可選的OTHERS處理器可以捕獲所有的未命名異常。

DECLARE
   pe_ratio   NUMBER (3, 1);
BEGIN
SELECT price / earnings
    INTO pe_ratio
    FROM stocks
   WHERE symbol = 'XYZ';   -- might cause division-by-zero error
INSERT INTO stats (symbol, ratio)
       VALUES ('XYZ', pe_ratio);
COMMIT;
EXCEPTION   -- exception handlers begin
WHEN ZERO_DIVIDE THEN   -- handles 'division by zero' error
    INSERT INTO stats (symbol, ratio)
         VALUES ('XYZ', NULL);
    COMMIT;
     ...
WHEN OTHERS THEN   -- handles all other errors
    ROLLBACK;
END;   -- exception handlers and block end here

上面的例子演示了異常控制,但對於INSERT語句的使用就有些低效了。使用下面的語句就要好一些:

INSERT INTO stats (symbol, ratio)
SELECT symbol, DECODE (earnings, 0, NULL, price / earnings)
    FROM stocks
   WHERE symbol = 'XYZ';

在下面這個例子中,子查詢爲INSERT語句提供了數據。如果earnings是零的話,函數DECODE就會返回空,否則DECODE就會返回price與earnings的比值。

二、異常的優點

使用異常來控制錯誤有幾個優點。如果沒有異常控制的話,每次執行一條語句,我們都必須進行錯誤檢查:

BEGIN
SELECT ...
    -- check for ’no data found’ error
SELECT ...
    -- check for ’no data found’ error
SELECT ...
    -- check for ’no data found’ error

錯誤處理和正常的處理內容界限不明顯,導致代碼混亂。如果我們不編寫錯誤檢查代碼,一個錯誤就可能引起其它錯誤,有時還可能是一些無關錯誤。

但有了異常後,我們就能很方便的控制錯誤,而且不需要編寫多個檢查代碼:

BEGIN
SELECT ...
SELECT ...
SELECT ...
   ...
EXCEPTION
WHEN NO_DATA_FOUND THEN -- catches all 'no data found' errors

異常能把錯誤控制程序單獨分離出來,改善可讀性,主要的算法不會受到錯誤恢復算法影響。異常還可以提高可靠性。我們不需要在每一個可能出現錯誤的地方編寫錯誤檢查代碼了,只要在PL/SQL塊中添加一個異常控制代碼即可。這樣,如果有異常被拋出,我們就可以確保它能夠被捕獲並處理。

三、預定義PL/SQL異常

當我們的PL/SQL程序與Oracle規則相沖突或超過系統相關(system-dependent)的限制時,內部異常就會被拋出。每個Oracle錯誤都有一個錯誤編號,但異常只能按名稱捕獲,然後被處理。所以,PL/SQL把一些常見Oracle錯誤定義爲異常。例如,如果SELECT INTO語句查詢不到數據時,PL/SQL就會拋出預定義異常NO_DATA_FOUND。

要控制其它Oracle異常,我們可以使用OTHERS處理器。函數SQLCODE和SQLERRM在OTHERS處理器中特別有用,因爲它們能返回Oracle錯誤編號和消息。另外,我們還可以使用編譯指示(pragma)EXCEPTION_INIT把一個異常名稱和一個Oracle錯誤編號關聯起來。PL/SQL在STANDARD包中聲明瞭全局預定義異常。所以,我們不需要自己聲明它們。我們可以爲下面列表中命名的預定義異常編寫處理程序:

異常 Oracle錯誤號 SQLCODE值
ACCESS_INTO_NULL ORA-06530 -6530
CASE_NOT_FOUND ORA-06592 -6592
COLLECTION_IS_NULL ORA-06531 -6531
CURSOR_ALREADY_OPEN ORA-06511 -6511
DUP_VAL_ON_INDEX ORA-00001 -1
INVALID_CURSOR ORA-01001 -1001
INVALID_NUMBER ORA-01722 -1722
LOGIN_DENIED ORA-01017 -1017
NO_DATA_FOUND ORA-01403 100
NOT_LOGGED_ON ORA-01012 -1012
PROGRAM_ERROR ORA-06501 -6501
ROWTYPE_MISMATCH ORA-06504 -6504
SELF_IS_NULL ORA-30625 -30625
STORAGE_ERROR ORA-06500 -6500
SUBSCRIPT_BEYOND_COUNT ORA-06533 -6533
SUBSCRIPT_OUTSIDE_LIMIT ORA-06532 -6532
SYS_INVALID_ROWID ORA-01410 -1410
TIMEOUT_ON_RESOURCE ORA-00051 -51
TOO_MANY_ROWS ORA-01422 -1422
VALUE_ERROR ORA-06502 -6502
ZERO_DIVIDE ORA-01476 -1476

預定義異常的簡要描述:

異常 拋出時機
ACCESS_INTO_NULL 程序嘗試爲一個未初始化(自動賦爲null)對象的屬性賦值。
CASE_NOT_FOUND CASE語句中沒有任何WHEN子句滿足條件,並且沒有編寫ELSE子句。
COLLECTION_IS_NULL 程序嘗試調用一個未初始化(自動賦爲null)嵌套表或變長數組的集合方法(不包括EXISTS),或者是程序嘗試爲一個未初始化嵌套表或變長數組的元素賦值。
CURSOR_ALREADY_OPEN 程序嘗試打開一個已經打開的遊標。一個遊標在重新打開之前必須關閉。一個遊標FOR循環會自動打開它所引用的遊標。所以,我們的程序不能在循環內部打開遊標。
DUP_VAL_ON_INDEX 程序嘗試向一個有着唯一約束條件的數據庫字段中保存重複值。
INVALID_CURSOR 程序嘗試操作一個不合法的遊標,例如關閉一個未打開的遊標。
INVALID_NUMBER 在一個SQL語句中,由於字符串並不代表一個有效的數字,導致字符串向數字轉換時會發生錯誤。(在過程化語句中,會拋出異常VALUE_ERROR。)當FETCH語句的LIMIT子句表達式後面不是一個正數時,這個異常也會被拋出。
LOGIN_DENIED 程序嘗試使用無效的用戶名和/或密碼來登錄Oracle。
NO_DATA_FOUND SELECT INTO語句沒有返回數據,或者是我們的程序引用了一個嵌套表中被刪除了的元素或是索引表中未初始化的元素。SQL聚合函數,如AVG和SUM,總是能返回一個值或空。所以,一個調用聚合函數的SELECT INTO語句從來不會拋出NO_DATA_FOUND異常。FETCH語句最終會取不到數據,當這種情況發生時,不會有異常拋出的。
NOT_LOGGED_ON 程序沒有連接到Oracle就要調用數據庫。
PROGRAM_ERROR PL/SQL程序發生內部錯誤。
ROWTYPE_MISMATCH 賦值語句中使用的主遊標變量和PL/SQL遊標變量的類型不兼容。例如,當一個打開的主遊標變量傳遞到一個存儲子程序時,實參的返回類型和形參的必須一致。
SELF_IS_NULL 程序嘗試調用一個空實例的MEMBER方法。也就是內置參數SELF(它總是第一個傳遞到MEMBER方法的參數)是空。
STORAGE_ERROR PL/SQL運行時內存溢出或內存不足。
SUBSCRIPT_BEYOND_COUNT 程序引用一個嵌套表或變長數組元素,但使用的下標索引超過嵌套表或變長數組元素總個數。
SUBSCRIPT_OUTSIDE_LIMIT 程序引用一個嵌套表或變長數組,但使用的下標索引不在合法的範圍內(如-1)。
SYS_INVALID_ROWID 從字符串向ROWID轉換髮生錯誤,因爲字符串並不代表一個有效的ROWID。
TIMEOUT_ON_RESOURCE 當Oracle等待資源時,發生超時現象。
TOO_MANY_ROWS SELECT INTO語句返回多行數據。
VALUE_ERROR 發生算術、轉換、截位或長度約束錯誤。例如,當我們的程序把一個字段的值放到一個字符變量中時,如果值的長度大於變量的長度,PL/SQL就會終止賦值操作並拋出異常VALUE_ERROR。在過程化語句中,如果字符串向數字轉換失敗,異常VALUE_ERROR就會被拋出。(在SQL語句中,異常INVALID_NUMBER會被拋出。)
ZERO_DIVIDE 程序嘗試除以0。

四、自定義PL/SQL異常

PL/SQL允許我們定義自己的異常。與預定義異常不同的是,用戶自定義異常必須聲明,並且需要用RAISE語句顯式地拋出。

1、聲明PL/SQL異常

異常只能在PL/SQL塊、子程序或包的聲明部分聲明。下例中,我們聲明一個名爲past_due的異常:

DECLARE
   past_due EXCEPTION;

異常和變量的聲明是相似的。但是要記住,異常是一種錯誤情況(error condition),而不是數據項。與變量不同的是,異常不能出現在賦值語句或是SQL語句中。但是,變量的作用域規則也適用於異常。

2、PL/SQL異常的作用域規則

在同一個塊內,異常不能聲明兩次。但可以在不同的塊聲明相同的異常。

塊中聲明的異常對於當前塊來說是本地的,但對於當前塊的所有子塊來說是全局的。因爲塊只能引用本地或全局的異常,所以封閉塊不能引用聲明在子塊中的異常。

如果我們在子塊中重新聲明瞭一個全局的異常,本地聲明的異常的優先級是要高於全局的。所以,子塊就不能引用全局的異常,除非全局異常在它的所在塊中用標籤作了標記,這種情況下可以使用下面的語法來引用全局異常:

block_label.exception_name

下例中演示了作用範圍規則:

DECLARE
   past_due   EXCEPTION;
   acct_num   NUMBER;
BEGIN
DECLARE   -- sub-block begins
     past_due   EXCEPTION;   -- this declaration prevails
     acct_num   NUMBER;
BEGIN
     ...
    IF ... THEN
      RAISE past_due;   -- this is not handled
    END IF;
END;   -- sub-block ends
EXCEPTION
WHEN past_due THEN   -- does not handle RAISEd exception
     ...
END;

上例中的封閉塊並不能捕獲拋出來的異常,因爲在子塊中聲明的past_due優先級要高於封閉塊聲明的異常。雖然它們的名字相同,但實際上是兩個不同的past_due異常,就像兩個acct_num變量只是共享着相同的名字一樣,實際上它們是完全不同的兩個變量。因此,RAISE語句和WHEN子句所引用的是不同的異常。如果想讓封閉塊能捕獲到子塊中的past_due異常,我們就必須從子塊中刪除聲明,或是在封閉塊中添加OTHERS處理器。

3、把PL/SQL異常與編號關聯:編譯指示EXCEPTION_INIT

要想控制沒有預定義名稱的錯誤(通常爲 ORA- 消息),我們就必須使用OTHERS處理器或編譯指示EXCEPTION_INIT。編譯指示就是能在編譯期而非運行時進行處理的編譯指令。

在PL/SQL中,編譯指示EXCPTION_INIT能告訴編譯器把異常名稱和錯誤編號關聯起來。這就能讓我們按名稱來引用所有的內部異常,併爲它編寫特定的處理程序。在我們看到的錯誤棧或是錯誤消息序列中,最頂層的就是我們能捕獲和處理的信息。

我們可以把編譯指示EXCEPTION_INIT寫在PL/SQL塊、子程序或包的聲明部分,語法如下:

PRAGMA EXCEPTION_INIT(exception_name, -Oracle_error_number);

其中exception_name是已經聲明過的異常名稱,Oracle_error_number是Oracle錯誤編號。編譯指示必須和異常聲明處於同一個聲明中,並且只能在異常聲明之後出現。如下例所示:

DECLARE
   deadlock_detected   EXCEPTION;
PRAGMA EXCEPTION_INIT (deadlock_detected, -60);
BEGIN
   ...   -- Some operation that causes an ORA-00060 error
EXCEPTION
WHEN deadlock_detected THEN
    -- handle the error
     ...
END;

4、自定我們自己的錯誤消息:過程RAISE_APPLICATION_ERROR

過程RAISE_APPLICATION_ERROR能幫助我們從存儲子程序中拋出用戶自定義的錯誤消息。這樣,我們就能把錯誤消息報告給應用程序而避免返回未捕獲異常。

調用RAISE_APPLICATION_ERROR的語法如下:

raise_application_error(error_number, message[, {TRUE | FALSE}]);

error_number是一個範圍在-20000至-20999之間的負整數,message是最大長度爲2048字節的字符串。如果第三個可選參數爲TRUE的話,錯誤就會被放到前面錯誤的棧頂。如果爲FALSE(默認值),錯誤就會替代前面所有的錯誤。

RAISE_APPLICATION_ERROR是包DBMS_STANDARD的一部分,所以,我們對它的引用不需要添加限定修飾詞。

應用程序只能從一個正在執行的存儲子程序或方法中調用raise_application_error。在調用時,raise_application_error會結束子程序並把用戶定義的錯誤編號和消息返回給應用程序。錯誤編號和消息可以像其它的Oracle錯誤一樣被捕獲。

在下面的例子中,我們在僱員工資欄的內容爲空的情況下調用raise_application_error:

CREATE PROCEDURE raise_salary (emp_id NUMBER, amount NUMBER) AS
   curr_sal   NUMBER;
BEGIN
SELECT sal
    INTO curr_sal
    FROM emp
   WHERE empno = emp_id;
IF curr_sal IS NULL THEN
    
     raise_application_error (-20101, 'Salary is missing');
ELSE
    UPDATE emp
       SET sal = curr_sal + amount
     WHERE empno = emp_id;
END IF;
END raise_salary;

調用程序會得到一個PL/SQL異常,它能在OTHERS處理器中使用錯誤報告函數SQLCODE和SQLERRM來進行處理。同樣,我們也可以使用編譯指示EXCEPTION_INIT把raise_application_error返回的錯誤編號映射到異常本身。如下面的Pro*C例子所示:

EXEC SQL EXECUTE
  

DECLARE
   null_salary   EXCEPTION;
  
PRAGMA EXCEPTION_INIT (null_salary, -20101);
BEGIN
   raise_salary (:my_emp_id, :my_amount);
EXCEPTION
WHEN null_salary THEN
    INSERT INTO emp_audit
         VALUES (:my_emp_id, ...);
END;

END-EXEC;

這項技術能讓調用程序在特定的異常處理程序中控制錯誤。

5、重新聲明預定義異常

請記住,PL/SQL把預定義的異常作爲全局內容聲明在包STANDARD中,所以,我們沒有必要重新聲明它們。重新聲明預定義異常是錯誤的做法,因爲我們的本地聲明會覆蓋掉全局聲明。例如,如果我們聲明瞭一個invalid_number,當PL/SQL拋出預定義異常INVALID_NUMBER時,我們爲異常INVALID_NUMBER編寫的異常控制程序就無法正確地捕獲到它了。這種情況下,我們必須像下面這樣使用點標誌來指定預定義異常:

EXCEPTION
WHEN INVALID_NUMBER OR STANDARD.INVALID_NUMBER THEN
    -- handle the error
END;

五、如何拋出PL/SQL異常

內部異常會由運行時系統隱式地拋出,其中也包括使用編譯指示EXCEPTION_INIT與Oracle錯誤編號關聯起來的用戶自定義異常。但是,用戶自定義的異常就必須顯式地用RAISE語句拋出。

1、使用RAISE語句拋出異常

PL/SQL塊和子程序應該只在錯誤發生或無法完成正常程序處理的時候才拋出異常。下例中,我們用RAISE語句拋出一個用戶自定義的out_of_stack異常:

DECLARE
   out_of_stock     EXCEPTION;
   number_on_hand   NUMBER (4);
BEGIN
   ...
IF number_on_hand < 1 THEN
    RAISE out_of_stock;
END IF;
EXCEPTION
WHEN out_of_stock THEN
    -- handle the error
END;

我們也可以顯式地拋出預定義異常。這樣,爲預定義異常編寫的處理程序也就能夠處理其它錯誤了,示例如下:

DECLARE
   acct_type   INTEGER := 7;
BEGIN
IF acct_type NOT IN (1, 2, 3) THEN
    RAISE INVALID_NUMBER;   -- raise predefined exception
END IF;
EXCEPTION
WHEN INVALID_NUMBER THEN
    ROLLBACK;
END;
六、PL/SQL異常的傳遞

異常被拋出時,如果PL/SQL在當前塊或子程序中沒有找到對應的異常控制程序,異常就會被繼續向上一級傳遞。也就是說異常會把它自身傳遞到後繼的封閉塊直到找到異常處理程序或是再也沒有可以搜索到的塊爲止。在後一種情況下,PL/SQL會向主環境拋出一個未捕獲異常。

但是,異常是不能通過遠程過程調用(RPC)來傳遞的。因此,PL/SQL塊不能捕獲由遠程子程序拋出的異常。
異常可以跨作用域傳遞,也就是說,它能夠超越聲明它的塊的範圍而存在。如下例所示:

BEGIN
   ...
DECLARE   -- sub-block begins
     past_due   EXCEPTION;
BEGIN
     ...
    IF ... THEN
      RAISE past_due;
    END IF;
END;   -- sub-block ends
EXCEPTION
   ...
WHEN OTHERS THEN
    ROLLBACK;
END;

因爲異常past_due所在的塊並沒有專門針對它的處理程序,所以異常就被傳遞到封閉塊。但是,按照作用域規則,封閉塊是不能引用子塊聲明的異常。所以,只有OTHERS處理器才能捕獲到這個異常。如果沒有用戶定義異常的處理程序,調用這個程序就會得到下面的錯誤:

ORA-06510: PL/SQL: unhandled user-defined exception

七、重新拋出PL/SQL異常

有時我們需要重新拋出捕獲到異常,也就是說,我們想在本地處理之後再把它傳遞到封閉塊。比如,在異常發生的時候,我們可能需要回滾事務,然後在封閉塊中寫下錯誤日誌。

要重新拋出異常,只要在本地處理程序中放置一個RAISE語句即可,示例如下:

DECLARE
   out_of_balance   EXCEPTION;
BEGIN
   ...
BEGIN   -- sub-block begins
     ...
    IF ... THEN
      RAISE out_of_balance;   -- raise the exception
    END IF;
EXCEPTION
    WHEN out_of_balance THEN
      -- handle the error
      RAISE;   -- reraise the current exception
END;   -- sub-block ends
EXCEPTION
WHEN out_of_balance THEN
    -- handle the error differently
     ...
END;

如果在RAISE語句中省略了異常名稱——只允許在異常處理程序中這樣做——程序就會把當前的異常重新拋出。

八、處理PL/SQL異常

異常拋出時,PL/SQL塊或子程序的正常執行就會停止,控制權轉到塊或子程序的異常處理部分,語法如下:

EXCEPTION
WHEN exception_name1 THEN   -- handler
     sequence_of_statements1
WHEN exception_name2 THEN   -- another handler
     sequence_of_statements2
     ...
WHEN OTHERS THEN   -- optional handler
     sequence_of_statements3
END;

爲捕獲拋出的異常,我們需要編寫異常處理程序。每個處理程序都由一個WHEN子句和語句序列組成。這些語句執行完畢後,塊或子程序就會結束,控制權不再返回異常被拋起的地方。換句話說,也就是我們不能再次返回異常發生的地方繼續執行我們的程序。

可選的OTHERS處理器總是塊或子程序的最後一個處理程序,它可以用於捕獲所有的未命名異常。因此,塊或子程序只能有一個OTHERS處理器。如下例所示,OTHERS處理器能夠保證所有的異常都會被控制:

EXCEPTION
WHEN ... THEN
    -- handle the error
WHEN ... THEN
    -- handle the error
WHEN OTHERS THEN
    -- handle all other errors
END;

如果我們想讓兩個或更多的異常執行同樣的語句序列,只需把異常名稱用關鍵字OR隔開,放在同一個WHEN子句中即可,如下例所示:

EXCEPTION
WHEN over_limit OR under_limit OR VALUE_ERROR THEN
-- handle the error

只要在WHEN子句的異常列表中有一項與被拋出異常相匹配,相關的語句序列就會被執行。關鍵字OTHERS不能出現在異常名稱列表中;它只能單獨使用。我們可以有任意數量的異常處理程序,而且每個處理程序都與一個異常列表及其對應的語句序列相關聯。但是,異常名稱只能在塊或子程序的異常處理部分出現一次。

變量作用範圍的規則在這裏也同樣適用,所以我們可以在異常處理程序中引用本地或全局變量。但是,當遊標FOR循環中有異常拋出時,遊標就會在異常處理程序調用之前被隱式地關閉。因此,顯式遊標的屬性值在異常處理程序中就不再可用了。

1、聲明中控制異常

如果在聲明時使用了錯誤的初始化表達式也有可能引發異常。例如,下面的聲明就是因常量credit_limit不能存儲超過999的數字而拋出了異常:

DECLARE
   credit_limit CONSTANT NUMBER(3) := 5000;   -- raises an exception
BEGIN
   ...
EXCEPTION
WHEN OTHERS THEN   -- cannot catch the exception
   ...
END;

當前塊中的處理程序並不能捕獲到拋出的異常,這是因爲聲明時拋出的異常會被立即傳遞到最近的封閉塊中去。

2、異常句柄中控制異常

在一個塊或子程序中,一次只能有一個異常被激活。所以,一個被異常處理程序拋出的異常會被立即傳遞到封閉塊,在那兒,封閉塊會爲它查找新的處理程序。從那一刻起,異常傳遞纔開始正常化。參考下面的例子:

EXCEPTION
WHEN INVALID_NUMBER THEN
    INSERT INTO ...   -- might raise DUP_VAL_ON_INDEX
WHEN DUP_VAL_ON_INDEX THEN ...   -- cannot catch the exception
END;

3、異常分支

GOTO語句不能跳轉到異常控制程序。同樣,GOTO語句也不能從異常控制程序跳轉到當前塊。例如,下面的GOTO語句就是非法的:

DECLARE
   pe_ratio   NUMBER (3, 1);
BEGIN
DELETE FROM stats
        WHERE symbol = 'xyz';
SELECT price / NVL (earnings, 0)
    INTO pe_ratio
    FROM stocks
   WHERE symbol = 'xyz';

   <<my_label>>
INSERT INTO stats (symbol, ratio)
       VALUES ('xyz', pe_ratio);
EXCEPTION
WHEN ZERO_DIVIDE THEN
     pe_ratio   := 0;
    GOTO my_label;   -- illegal branch into current block
END;

但是,GOTO語句可以從一個異常控制程序中跳轉到一個封閉塊。
4、獲取錯誤代號與消息:SQLCODE和SQLERRM

在異常處理程序中,我們可以使用內置函數SQLCODE和SQLERRM來查出到底發生了什麼錯誤,並能夠獲取相關的錯誤信息。對於內部異常來說,SQLCODE會返回Oracle錯誤編號。SQLCODE返回的總是一個負數,除非發生的Oracle錯誤是沒有找到數據,這時返回的是+100。SQLERRM會返回對應的錯誤消息。消息是以Oracle錯誤編號開頭的。

如果我們沒有使用編譯指令EXCEPTION_INIT把異常與編號關聯的話,SQLCODE和SQLERRM就會分別返回+1和消息"User-Defined Exception"。Oracle錯誤消息最大長度是512個字符,其中包括錯誤編號、嵌套消息和具體表和字段的名稱。

如果沒有異常拋出,SQLCODE返回0,SQLERRM返回消息"ORA-0000: normal, successful completion"。

我們可以把錯誤編號傳遞給SQLERRM,讓它返回對應的錯誤消息。但是,一定要保證我們傳遞給SQLERRM的錯誤編號是負數。下例中,我們把一個正數傳遞給SQLERRM,結果就不是我們想要的那樣的了:

DECLARE
   err_msg   VARCHAR2(100);
BEGIN
FOR err_num IN 1 .. 9999 LOOP
     err_msg     := SQLERRM(err_num);   -- wrong; should be -err_num

    INSERT INTO ERRORS
         VALUES (err_msg);
END LOOP;
END;

把正數傳給SQLERRM時,如果傳遞的是+100,返回的結果是"no data found",其他情況總是會返回消息"user-defined exception"。把0傳遞給SQLERRM,就會返回消息"normal, successful completion"。

我們不能直接在SQL語句中使用SQLCODE或SQLERRM。我們必須先把它們的值賦給本地變量,然後再在SQL中使用變量,如下例所示:

DECLARE
   err_num   NUMBER;
   err_msg   VARCHAR2(100);
BEGIN
   ...
EXCEPTION
WHEN OTHERS THEN
     err_num     := SQLCODE;
     err_msg     := SUBSTR(SQLERRM, 1, 100);

    INSERT INTO ERRORS
         VALUES (err_num, err_msg);
END;

字符串函數SUBSTR可以保證用SQLERRM爲err_msg賦值時不會引起VALUE_ERROR異常。函數SQLCODE和SQLERRM在OTHERS異常處理程序中特別有用,因爲它們能讓我們知道哪個內部異常被拋出。

注意:在使用編譯指示RESTRICT_REFERENCES判斷存儲函數的純度時,如果函數調用了SQLCODE和SQLERRM,我們就不能指定約束爲WNPS和RNPS了。

5、捕獲未控制異常

記住,如果被拋出的異常找不到合適的異常控制程序,PL/SQL會向主環境拋出一個未捕獲的異常錯誤,然後由主環境決定如何處理。例如,在Oracle預編譯程序環境中,任何一個執行失敗的SQL語句或PL/SQL塊所涉及到的改動都會被回滾。

未捕獲也能影響到子程序。如果我們成功地從子程序中退出,PL/SQL就會把值賦給OUT參數。但是,如果我們因未捕獲異常而退出程序,PL/SQL就不會爲OUT參數進行賦值。同樣,如果一個存儲子程序因異常而執行失敗,PL/SQL也不會回滾子程序所做的數據變化。

我們可以在每個PL/SQL程序的頂級使用OTHERS句柄來捕獲那些沒有被子程序捕捉到的異常。

九、PL/SQL錯誤控制技巧

這裏,我們將學習三個提高程序靈活性的技巧。

1、模擬TRY..CATCH..塊

異常控制程序能讓我們在退出一個塊之前做一些恢復操作。但是在異常程序完成後,語句塊就會終止。我們不能從異常句柄再重新回到當前塊。例如,如果下面的SELECT INTO語句引起了ZERO_DIVIDE異常,我們就不能執行INSERT語句了:

DECLARE
   pe_ratio   NUMBER(3, 1);
BEGIN
DELETE FROM stats
        WHERE symbol = 'XYZ';

SELECT price / NVL(earnings, 0)
    INTO pe_ratio
    FROM stocks
   WHERE symbol = 'XYZ';

INSERT INTO stats(symbol, ratio)
       VALUES ('XYZ', pe_ratio);
EXCEPTION
WHEN ZERO_DIVIDE THEN
     ...
END;

其實我們可以控制某一條語句引起的異常,然後繼續下一條語句。只要把可能引起異常的語句放到它自己的子塊中,並編寫對應的異常控制程序。一旦在子塊中有錯誤發生,它的本地異常處理程序就能捕獲並處理異常。當子塊結束時,封閉塊程序會繼續執行緊接着的下一條語句。如下例:

DECLARE
   pe_ratio   NUMBER(3, 1);
BEGIN
DELETE FROM stats
        WHERE symbol = 'XYZ';

BEGIN   -- sub-block begins
    SELECT price / NVL(earnings, 0)
      INTO pe_ratio
      FROM stocks
     WHERE symbol = 'XYZ';
EXCEPTION
    WHEN ZERO_DIVIDE THEN
       pe_ratio     := 0;
END;   -- sub-block ends

INSERT INTO stats(symbol, ratio)
       VALUES ('XYZ', pe_ratio);
EXCEPTION
WHEN OTHERS THEN
     ...
END;

在上面這個例子中,如果SELECT INTO語句拋出了ZERO_DIVIDE異常,本地異常處理程序就會捕捉到它並把pe_ratio賦值爲0。當處理程序完成時,子塊也就終止,INSERT語句就會被執行。

2、反覆執行的事務

異常發生後,我們也許還不想放棄我們事務,仍想重新嘗試一次。這項技術的實現方法就是:

把事務裝入一個子塊中。
把子塊放入一個循環,然後反覆執行事務
在開始事務之前標記一個保存點。如果事務執行成功的話,就提交事務並退出循環。如果事務執行失敗,控制權就會交給異常處理程序,事務回滾到保存點,然後重新嘗試執行事務。
如下例所示。當異常處理程序完成時,子塊終止,控制權被交給外圍塊的LOOP語句,子塊再次重新開始執行。而且,我們還可以用FOR或WHILE語句來限制重做的次數。

DECLARE
   NAME     VARCHAR2(20);
   ans1     VARCHAR2(3);
   ans2     VARCHAR2(3);
   ans3     VARCHAR2(3);
   suffix   NUMBER        := 1;
BEGIN
   ...
LOOP   -- could be FOR i IN 1..10 LOOP to allow ten tries
    BEGIN   -- sub-block begins
      SAVEPOINT start_transaction;   -- mark a savepoint

     
      DELETE FROM results
            WHERE answer1 = ’no’;

     
      INSERT INTO results
           VALUES (NAME, ans1, ans2, ans3);

      -- raises DUP_VAL_ON_INDEX if two respondents have the same name
      COMMIT;
      EXIT;
    EXCEPTION
      WHEN DUP_VAL_ON_INDEX THEN
        ROLLBACK TO start_transaction;   -- undo changes
         suffix     := suffix + 1;   -- try to fix problem
         NAME       := NAME || TO_CHAR(suffix);
    END;   -- sub-block ends
END LOOP;
END;

3、使用定位變量標記異常發生點

只用一個異常句柄來捕獲一系列語句的話,可能無法知道到底是哪一條語句產生了錯誤:

BEGIN
SELECT ...
SELECT ...
EXCEPTION
WHEN NO_DATA_FOUND THEN ...
-- Which SELECT statement caused the error?
END;

要想解決這個問題,我們可以使用一個定位變量來跟蹤執行語句,例如:

DECLARE
   stmt INTEGER := 1;   -- designates 1st SELECT statement
BEGIN
SELECT ...
   stmt := 2;   -- designates 2nd SELECT statement
SELECT ...
EXCEPTION
WHEN NO_DATA_FOUND THEN
    INSERT INTO errors VALUES ('Error in statement ' || stmt);
END;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章