Oracle:pl/sql 異常處理[轉]

 

pl/sql 提供了強大而靈活的手段來捕捉和處理程序產生的異常,從而使 oracle 的用戶遠離一些令人煩惱的 bug

 

pl/sql 異常處理的概念和術語

 

oracle 中所有的錯誤都被認爲是不應該發生的異常。一個異常可能是以下 3 種情況的一種:

u       由系統產生的錯誤(“ out of memory ”或“ duplicate value in index ”)

u       用戶行爲導致的錯誤

u       應用程序給用戶的一個警告

pl/sql 用一種異常句柄的結構來捕捉和響應錯誤。正是有了異常句柄的存在,我們能很方便的分離異常處理代碼與可執行代碼。與線性的代碼相比,爲了處理異常,異常句柄提供了一種類似事件驅動的模式;換句話說,就是不管一種特定的錯誤在何時何地發生,它都將被同一個代碼處理。

當一個錯誤出現後,無論它是系統還是程序產生的,都將導致一個異常。之後,可執行程序被中斷,控制權轉移給異常處理代碼。處理完異常後,程序將不會回到先前被中斷的位置,相反的,控制權被交給了當前程序的外圍模塊(可能是程序,也可能是系統)。

       procedure jimmy

       is

              new_value varchar(35)

       begin

|--------new_value:=old_value || ‘-new’;

|          if new_value like ‘like%’

|          then

|                 …..

|          end if;

|   exception

|----- à        when value_error

              then

                     …..

       end;

因爲 old_value 是一個未被定義的變量,所以將產生一個錯誤,並將給異常處理模塊處理。

 

從異常的可應用範圍出發,可將異常分爲兩類:

系統異常:

oracle 定義並由 pl/sql runtime 引擎在檢測到錯誤時產生的異常。一些系統異常有名字,比如 NO_DATA_FOUND ,然而大多數的異常僅僅只有數字編號和描述。這些異常無論在哪個 pl/sql 中程序都能被應用。

共有 21 個命名的系統異常:

命名的系統異常

產生原因

ACCESS_INTO_NULL

未定義對象

CASE_NOT_FOUND

CASE 中若未包含相應的 WHEN ,並且沒有設置 ELSE

COLLECTION_IS_NULL

集合元素未初始化

CURSER_ALREADY_OPEN

遊標已經打開

DUP_VAL_ON_INDEX

唯一索引對應的列上有重複的值

INVALID_CURSOR

在不合法的遊標上進行操作

INVALID_NUMBER

內嵌的 sql 語句不能將字符轉換爲數字

NO_DATA_FOUND

使用 select into 未返回行,或應用索引表未初始化的元素時

TOO_MANY_ROWS

執行 select iotn 時,返回超過一行

ZERO_DIVIDE

除數爲 0

SUBSCRIPT_BEYOND_COUNT

元素下標超過嵌套表或 VARRAY 的最大值

SUBSCRIPT_OUTSIDE_LIMIT

使用嵌套表或 VARRAY 時,將下標指定爲負數

VALUE_ERROR

賦值時,變量長度不足以容納實際數據

LOGIN_DENIED

Pl/sql 應用程序連接到 oracle 數據庫時,提供了不正確的用戶名或密碼

NOT_LOGGED_ON

Pl/sql 應用程序在沒有連接 oralce 數據庫的情況下訪問數據

PROGRAM_ERROR

Pl/sql 內部問題,可能需要重裝數據字典& pl./sql 系統包

ROWTYPE_MISMATCH

宿主遊標變量與 pl/sql 遊標變量的返回類型不兼容

SELF_IS_NULL

使用對象類型時,在 null 對象上調用對象方法

STORAGE_ERROR

運行 pl/sql 時,超出內存空間

SYS_INVALID_ID

無效的 ROWID 字符串

TIMEOUT_ON_RESOURCE

Oracle 在等待資源時超時

 

由程序員定義的異常:

程序員在程序中定義的異常,它只是在特定的程序種有效。可以使用 EXCEPTION_INT 這個 pragma 將一個無名字的系統異常與一個程序員定義的名字相關聯。或者用 RAISE_APPLICATION_ERROE 來自己定義一個異常的數字編號和描述。

 

按異常生成方式可分爲:

預定義異常:

       就是上面表中的 21 種有名字的系統異常。

非預定義異常:

       沒名字的系統異常,可以用 pragma exception_int 給它關聯一個名字。

自定義異常:

       需要用 RAISE RAISE_APPLICATION_ERROR 生成的異常。

 

下面是一些要用到的屬於 ;

Exception section (異常處理模塊)

它是 pl/sql 語句塊種包含一個或多個異常句柄的部分。 Exception section 的結構基本上與 case 相似。

Raise (產生)

通過通知 pl/sql runtime 引擎有錯誤來中止當前程序的運行。也可通過顯式的請求,如: RAISE RAISE_APPLICATION_ERROR RAISE 一個異常。

Handle (句柄,某一個異常處理的代碼)

exception section 中捕捉錯誤。可以在 handle 中編寫程序來處理異常,比如將錯誤記入 log 中,顯示一個錯誤信息,將異常傳出當前程序快。

Scope (作用範圍

       一個異常從產生、被捕捉到處理整個過程所處的程序部分。

Propagation (傳遞)

如果一個異常沒有被處理,那麼它將被傳遞到但前塊的上一級,它有可能是另一個代碼快,也可能是系統。

Unhandled exception (未被處理的異常)

如果一個異常沒有被處理,並一直被傳遞道理系統中,那麼它被稱爲 unhandled exception

Un-named or anonymous excepttion (匿名異常)

       (在異常類型中有介紹)

Named exception (命名異常)

       包括系統異常中有名字的那部分和用戶定義的名字。

 

異常定義

 

在一個異常產生、被捕獲並處理之前,它必須被定義。 Oracle 定義了幾千個異常,絕大多數只有錯誤編號和相關描述,僅僅命名了若干個最常被用到的異常。這些名字被儲存在 STANDARD UTL_FILE DBMS_SQL 這幾個系統包中,詳情請見 oracle:pl/sql 異常處理( 1 )。

出自之外的絕大多數異常需要程序員命名。有 2 種命名異常的方法:

 

1 :聲明一個自定義異常

STANDARD 中的命名了的異常基本山是與系統的錯誤相關的(當然那些只有 errorcode 的異常也是這樣),但在實際的應用中我們經常需要與特定的應用程序相關的異常,由程序員聲明的異常就是用於處理這種情況的。

Oracle 異常處理模塊的方便的地方在於,它並沒有區別對待自定義的與預定義的異常。這使得我們可以像對待預定義異常一樣,捕捉和處理自定義異常,只是在此之前需要聲明它;同時對於一個自定義的異常,我們需要用 RAISE 來手動產生。

下面是一個聲明的例子:

       procedure calc_ammul_sales

              (company_id_in in company.company_id%tye)

       is

              invalid_company_id exception;

              negative_balance excrption;

              duplicate_company Boolean;

       begin

              /*body of executable statement*/

       exception

              when invalid_company_id

              then /*handle exception*/

              when no_data_found

              then /*handle exception*/

              /*…..*/

       end;

需要注意的是處理定義的時候,只有兩個地方會出現自定義的異常:

ü         raise exception

ü         when exception then

 

2 :爲非預定義異常關聯一個名字

僅僅 21 個預定義異常對我們來說實在是太少了,還有幾千個異常只有 errorcode 和描述。另外,程序員也可以用 RAISE_APPLICATION_ERROR 定義一個含 errorcode 和描述的異常。

當然,只用 errorcode 也可以很好地完成工作,只要你不擔心會忘了那串數字代表的意思就行。比方說 ;

       exception

              when others

              then

                     if sqlcode=-1843 then  /*sqlcode 是內建的用於返回最近一次錯誤編號的函數 */

                     …..

這的確是一段讓人感到晦澀的代碼,還是給它關聯個名字吧。

我們要用到的是 pragma exception_init(exception,integer) ,然後就可以像對待預定義異常一樣對待它了,我是說沒必要像上面的那種一樣用 raise Exception_init 是一個編譯時運行的函數,它只能出現在代碼的聲明部分,而異常名字必須在此之前被定義。下面用一個匿名過程舉個例子:

       declare

              invalid_company_id exception;

              pragma exception_init(invalid_company_id, -1834);

要注意的時:

ü         不可以用 -1403 no_data_found ),用 100 ,事實上 exception_init 中的 integer 對應的是 sqlcode 返回的值。

ü         不能爲 0 ,不能大於 100 ,不能小於 -1000000

一個例子:

       procedure delete_company(company_id_in in number)

       is

              still_have_emplyee exception;

              pragma exception(still_have_employee, -2293);

       begin

              delete from compamy

              where company_id=company_id_in;

       exception

              when still_have_employee

              then dbms_output.put_line(‘delete employees for company first’);

       end;

在一下兩種情況下,我們有必要使用 exception_init

ü         一個非預定義異常是經常要被用到的。

ü         我們將用 raise_applocation_error 產生了一個自定義的 errorcode 時。

一種簡便的方法是將以上兩種情況中的異常定義在一個包中,這樣我們就沒有必要每次都重複定義了。

       Create or replace package dynsql

       Is

              Invalid_table_name exception;

              Pragma exception_init(invalid_table_name, -903);

              Invalid_column_name exception;

              Pragma exception_init(invalid_column_name, -904);

              En_too_young const number:=-200001;

              Exc_too_young exception;

              Pragma exception_init(exc_too_young, -20001);

       End;

有了上面這個包,就可以方便的處理異常了 ;

       procedure validate_emp(birthdate in date)

       is

              min_tear const pls_integer:=18;

       begin

              if add_month(sysdate,min_year*12*-1)<birthdate_in

              then

raise_application_error(dynsql.en_too_young, ‘employee must be’ || min_year ||‘old’);

              end if;

       end;

除了 standard 包中的 21 個預定義異常外,還有一些包也定義了一些異常。但與 standard 包中異常不同的是,在使用這些異常時,需要帶上包的名字。如:

       when dbms_lob.invalid_argval then ……

 

非常有用的一點是,可以在最外層的 pl/sql 塊的異常處理模塊中加入 others ,這樣就可以把從內部傳遞出來的未被處理的剩餘異常全部處理掉了。

       Exception

       When others

              Then ….

 

處理 oracle 系統自動生成系統異常外,可以使用 raise 來手動生成錯誤。

l         Raise exception;

l         Raise package.exception;

l         Raise;

以上是 raise 的三種使用方法。第一種用於生成當前程序中定義的異常或在 standard 中的系統異常。

       Declare

              Invalid_id exception;

              Id_values varchar(2);

       Begin

              Id_value:=id_for(‘smith’);

              If substr(id_value,1,1)!=’x’

              Then

                     Raise invalid_id;

              End if;

       Exception

              When invalid_id

              Then

                     Dbms_output.put_line(‘this is an invalid id!’);

       End;

這是一個生成自定義異常的例子,當然也可以生成系統異常:

       declare

              employee_id_in number;

       Begin

Select employee_id into employee_id_in from employ_list where employee_name=&n;

If employee_id_in=0

Then

       Raise zero_devided;

End if;

       Exception

              When zero_devided

              Then

                     Dbms_output.put_line(‘wrong!’);

       End;

有一些異常是定義在非標準包中的,如 UTL_FILE DBMS_SQL 以及程序員創建的包中異常。可以使用 raise 的第二種用法來生成異常。

       If day_overdue(isbn_in, browser_in) > 365

       Then

              Raise overdue_pkg.book_is_lost

       End if;

在最後一種 raise 的形式中,不帶任何參數。這種情況只出現在希望將當前的異常傳到外部程序時。

       Exception

              When no_data_found

              Then

                     Raise;

       End;

 

Pl.sql 使用 raise_application_error 過程來生成一個有具體描述的異常。當使用這個過程時,當前程序被中止,輸入輸出參數被置爲原先的值,但任何 DML 對數據庫所做的改動將被保留,可以在之後用 rollback 命令回滾。下面是該過程的原型:

       Procedure raise_application_error(

       Num binary_integer;

       Msg varchar2;

       Keeperrorstack Boolean default false

)

其中 num 是在 -20999 -20000 之間的任何數字(但事實上, DBMS_OUPUT DBMS_DESCRIBLE 包使用了 -20005 -20000 的數字); msg 是小於 2K 個字符的描述語,任何大於 2K 的字符都將被自動丟棄; keeperrorstack 默認爲 false ,是指清空異常棧,再將當前異常入棧,如果指定 true 的話就直接將當前異常壓入棧中。

    CREATE OR REPLACE PROCEDURE raise_by_language (code_in IN PLS_INTEGER)

    IS

       l_message error_table.error_string%TYPE;

    BEGIN

       SELECT error_string

         INTO l_message

         FROM error_table, v$nls_parameters v

        WHERE error_number = code_in

          AND string_language  = v.VALUE

          AND v.parameter = 'NLS_LANGUAGE';

 

       RAISE_APPLICATION_ERROR (code_in, l_message);

    END;

 

異常處理

 

當異常生成之後,程序被中止,控制權交給異常處理模塊,異常處理模塊捕獲當前異常句柄,並交由相應的程序處理;如果,異常促裏模塊沒有捕捉到異常句柄,那麼它將被傳輸到當前程序的外圍。

 

除非由一些特殊的要求,一般情況下異常將再當前程序的異常處理模塊中被處理。異常處理模塊以 EXCEPTION 開始 END; 結尾。

       Declare

              /*…………*/

       begin

              /*…………*/

       exception

              when /* 異常名稱 */

              then /* 異常處理 */

              when other

              then /* 異常處理 */

       end;

異常處理模塊的語法基本上以 CASE 一致,凡是在 when 中有定義的異常都將被處理,而沒有的則被傳輸。一個特殊的異常處理語句是 WHEN OTHERS 。就想在( 3 )中所說的,它會處理所有爲被處理的異常,因此必須小心使用它,最好是在最外層的程序中。當然如果喜歡偷懶的,大可以在異常處理模塊中只放一個 OTHERS 。注意,無論哪種情況, OTHERS 只能這隻在異常處理的最後一位。

 

有趣的是,可以在一個 when 中處理多個異常句柄。

       Exception

              When no_data_found or invalid_employee_id or dbms_ldap.invalid session

              Then /*………..*/

       End;

       /

在這個例子裏,有標準包的異常、自定義異常和非標準包中的異常。這些異常只能用 or 連接,不可以用 and ,因爲只有一個異常能夠生成。

 

raise_application_error 生成的異常,如果沒有被處理而一直傳遞到系統環境中,那麼環境將視情況作出相應的反映。在 sqlplus 中, oracle 將回滾所有 DML 對數據所做的修改。在 sqlplus 環境中,因爲有自動回滾的存在,我們可以保留出現未被處理的異常的可能性;而在另外的一些環境中,則需要仔細設計最外層程序。

ü         捕捉任何有可能傳出的異常。

ü         記錄錯誤以便於分析。

ü         給外部環境一個信息,以便於其作出相應的處理。

 

對於自定義異常,因爲 sqlcode 值永遠是 1 ,所以當它被傳出時,如果外圍程序中沒有定義相同名稱的異常,我們將不知道是什麼異常產生了。因此,不要將自定義異常傳遞出去。

 

在程序中處理幾個互相獨立的操作時,爲了避免出現因爲一個操作產生異常而使整個程序被中斷的情況,有必要將這些獨立的操作放在各自的虛擬塊中。

       Procedure change_data is

       Begin

              Begin

                     Delete from employee where …..

              Exception

                     When others then null;

              End;

 

              Begin

                     Update company set …….

              Exception

When others then null;

              End;

 

              Begin

                     Insert into company_history select * from company where ….

              Exception

                     When others then null;

              End;

       End;

       /

 

 

Pl/sql 提供了一些內建的函數來幫助我們確定、分析異常。

 

SQLCODE

這個函數在前面有提到過,它是一個用於返回當前模塊中最近一次異常值的函數,或者說是非入棧程序的異常值。打個比方:如果在當前程序的異常模塊中調用了另一個程序, oracle 將當前程序及相應的環境變量(包括異常值)壓入系統棧;在被調用程序中生成了一個值爲 1 的異常,那麼 sqlcode 將返回 1 ;之後剛纔的程序出棧, sqlcode 返回當前異常值。需要注意的是,不要在異常模塊之外使用它,這樣不會有任何意義。當沒有異常或在異常模塊之外使用時, SQLCODE 返回 0 ;返回值 1 是指自定義異常。

 

SQLERRM

接收異常值,返回相應的長度不超過 512 字節的描述語。如果沒有傳入異常值,則返回當前異常描述。

       Begin

              Dbms_output.put_line( sqlerrm(-1403);

       End;

Sql>/

Ora-1403: no data found

在需要體構長度超過 512 字節的描述時, oracle 建議使用 dbms_utility.format_error_stack 。顯然,用這個函數來判斷一個異常是否爲系統異常是很有用的,如果不是的話,將返回以下兩種情況的一種。

如果是一個負數:

       ora-nnnnn: message not found,; product=rdbms; facility=ora

如果是一個正數:

       -nnnnn: non-oracle exception

 

DBMS_UTILITY.FORMAT_ERROR_STACK

返回當前異常相應的描述,沒有字符長度限制。與 SQLCODE 相同的是,必須在異常處理模塊中使用。雖然名稱中有一個 stack 在,但通過它並不能知道異常的最初生成處,需要的話就必須使用 DBMS_UTILITY.FORMAT_ERROR_BACKTRACE

 

DBMS_UTILITY.FORMAT_ERROR_BACKTRACE

系統爲最近一次生成的異常設置了一個棧,並跟蹤它的傳遞過程,而這個函數使用這個棧,然後返回該異常的整個傳遞過程。這個函數對錯誤的定位和實施下一步處理起着至關重要的作用。

       Create or replace procedure procl is

       Begin

              Dbms_output.put_line(‘running proc1’);

              Raise no_data_found;

       End;

       /

       create or replace procedure proc2 is

       begin

              dbms_output.put_line(‘calling proc1’);

              proc1;

       end;

       /

       create or replace procedure proc3 is

       begin

              dbms_output.put_line(‘calling proc2’);

              proc2;

       exception

              when no_data_found

              then

                     dbms_output.put_line(‘error stack at top level’);

                     dbms_output.put_line(dbms_utility.format_error_backtrace);

       end;

       /

現在可以運行 proc3 來看看結果。

Sql>set serveroutput on;

Sql>begin

2                         dbms_output.put_line(‘proc3->proc2->proc1 backtrace’);

3                         proc3;

4     end;

5     /

    Proc3 -> Proc2 -> Proc1 backtrace

    calling proc2

    calling proc1

    running proc1

    Error stack at top level:

    ORA-06512: at "SCOTT.PROC1", line 4

    ORA-06512: at "SCOTT.PROC2", line 5

ORA-06512: at "SCOTT.PROC3", line 4

事實上,每次異常的產生都將重置這個異常棧,只是最後一次從系統棧出棧的是最外層的程序塊,所以可以清楚地看到異常生成的整個過程。上面這個程序的執行過程是這樣的:首先用 put_line 打印 Proc3 -> Proc2 -> Proc1 backtrace 調用 proc3 ,當前程序入棧 => 打印 calling proc2 ,調用 proc2 proc3 入棧 => 打印 calling proc1 ,調用 proc1 proc2 入棧 => 打印 running proc1 ,生成 no_data_found 異常,該異常被壓入異常棧中 =>  proc2 出棧,並檢測到來自第 5 行調用傳遞過來的異常,將它在此壓入異常棧 => proc3 出棧,並檢測到來自第 4 行調用傳遞過來的異常,將它在此壓入異常棧, dbms_utility.format_error_backtrace 將異常棧中信息反相打印出來 =>  最外層程序出棧, end

以下是正確使用這個函數的一些注意事項:

ü         在當前程序的異常處理模塊中調用這個函數。

ü         避免在中間程序中使用異常處理模塊。

這樣異常就能被正確地傳輸到最外層程序中,並打印出這個過程了。

 

 

本文出處:http://www.cppblog.com/ivenher/articles/14133.html

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