第七章 控制PL/SQL错误
一、錯誤控制一覽
在PL/SQL中,警告或錯誤被稱為異常。異常可以是內(nèi)部(運行時系統(tǒng))定義的或是用戶定義的。內(nèi)部定義的案例包括除零操作和內(nèi)存溢出等。一些常見的內(nèi)部異常都有一個預(yù)定義的名字,如ZERO_DIVIDE和STORAGE_ERROR等。對于其它的內(nèi)部異常,我們可以手動為它們命名。
我們可以在PL/SQL塊、子程序或包的聲明部分自定義異常。例如,我們可以定義一個名為insufficient_funds的異常來標(biāo)示帳戶透支的情況。與內(nèi)部異常不同的是,用戶自定義異常必須有一個名字。
錯誤發(fā)生時,異常就會被拋出。也就是說,正常的執(zhí)行語句會被終止,控制權(quán)被轉(zhuǎn)到PL/SQL塊的異常控制部分或子程序的異常控制部分。內(nèi)部異常會由運行時系統(tǒng)隱式地拋出,而用戶定義異常必須顯式地用RAISE語句拋出,RAISE語句也可以拋出預(yù)定義異常。
為了控制被拋出的異常,我們需要單獨編寫被稱為"exception handler"的異常控制程序。異常控制程序運行后,當(dāng)前塊就會停止執(zhí)行,封閉塊繼續(xù)執(zhí)行下一條語句。如果沒有封閉塊,控制權(quán)會直接交給主環(huán)境。
下例中,我們?yōu)橐患夜善贝a(Ticker Symbol)為XYZ的公司計算并保存市盈率(price-to-earning)。如果公司的收入為零,預(yù)定義異常ZERO_DIVIDE就會被拋出。這將導(dǎo)致正常的執(zhí)行被終止,控制權(quán)被交給異常控制程序。可選的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'; |
在下面這個例子中,子查詢?yōu)镮NSERT語句提供了數(shù)據(jù)。如果earnings是零的話,函數(shù)DECODE就會返回空,否則DECODE就會返回price與earnings的比值。
二、異常的優(yōu)點
使用異常來控制錯誤有幾個優(yōu)點。如果沒有異常控制的話,每次執(zhí)行一條語句,我們都必須進行錯誤檢查:
| BEGIN ??SELECT?... ????--?check?for?’no?data?found’?error ??SELECT?... ????--?check?for?’no?data?found’?error ??SELECT?... ????--?check?for?’no?data?found’?error |
錯誤處理和正常的處理內(nèi)容界限不明顯,導(dǎo)致代碼混亂。如果我們不編寫錯誤檢查代碼,一個錯誤就可能引起其它錯誤,有時還可能是一些無關(guān)錯誤。
但有了異常后,我們就能很方便的控制錯誤,而且不需要編寫多個檢查代碼:
| BEGIN ??SELECT?... ??SELECT?... ??SELECT?... ??... EXCEPTION ??WHEN?NO_DATA_FOUND?THEN?--?catches?all?'no?data?found'?errors |
異常能把錯誤控制程序單獨分離出來,改善可讀性,主要的算法不會受到錯誤恢復(fù)算法影響。異常還可以提高可靠性。我們不需要在每一個可能出現(xiàn)錯誤的地方編寫錯誤檢查代碼了,只要在PL/SQL塊中添加一個異常控制代碼即可。這樣,如果有異常被拋出,我們就可以確保它能夠被捕獲并處理。
三、預(yù)定義PL/SQL異常
當(dāng)我們的PL/SQL程序與Oracle規(guī)則相沖突或超過系統(tǒng)相關(guān)(system-dependent)的限制時,內(nèi)部異常就會被拋出。每個Oracle錯誤都有一個錯誤編號,但異常只能按名稱捕獲,然后被處理。所以,PL/SQL把一些常見Oracle錯誤定義為異常。例如,如果SELECT INTO語句查詢不到數(shù)據(jù)時,PL/SQL就會拋出預(yù)定義異常NO_DATA_FOUND。
要控制其它Oracle異常,我們可以使用OTHERS處理器。函數(shù)SQLCODE和SQLERRM在OTHERS處理器中特別有用,因為它們能返回Oracle錯誤編號和消息。另外,我們還可以使用編譯指示(pragma)EXCEPTION_INIT把一個異常名稱和一個Oracle錯誤編號關(guān)聯(lián)起來。PL/SQL在STANDARD包中聲明了全局預(yù)定義異常。所以,我們不需要自己聲明它們。我們可以為下面列表中命名的預(yù)定義異常編寫處理程序:
| 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 |
預(yù)定義異常的簡要描述:
| ACCESS_INTO_NULL | 程序嘗試為一個未初始化(自動賦為null)對象的屬性賦值。 |
| CASE_NOT_FOUND | CASE語句中沒有任何WHEN子句滿足條件,并且沒有編寫ELSE子句。 |
| COLLECTION_IS_NULL | 程序嘗試調(diào)用一個未初始化(自動賦為null)嵌套表或變長數(shù)組的集合方法(不包括EXISTS),或者是程序嘗試為一個未初始化嵌套表或變長數(shù)組的元素賦值。 |
| CURSOR_ALREADY_OPEN | 程序嘗試打開一個已經(jīng)打開的游標(biāo)。一個游標(biāo)在重新打開之前必須關(guān)閉。一個游標(biāo)FOR循環(huán)會自動打開它所引用的游標(biāo)。所以,我們的程序不能在循環(huán)內(nèi)部打開游標(biāo)。 |
| DUP_VAL_ON_INDEX | 程序嘗試向一個有著唯一約束條件的數(shù)據(jù)庫字段中保存重復(fù)值。 |
| INVALID_CURSOR | 程序嘗試操作一個不合法的游標(biāo),例如關(guān)閉一個未打開的游標(biāo)。 |
| INVALID_NUMBER | 在一個SQL語句中,由于字符串并不代表一個有效的數(shù)字,導(dǎo)致字符串向數(shù)字轉(zhuǎn)換時會發(fā)生錯誤。(在過程化語句中,會拋出異常VALUE_ERROR。)當(dāng)FETCH語句的LIMIT子句表達式后面不是一個正數(shù)時,這個異常也會被拋出。 |
| LOGIN_DENIED | 程序嘗試使用無效的用戶名和/或密碼來登錄Oracle。 |
| NO_DATA_FOUND | SELECT INTO語句沒有返回數(shù)據(jù),或者是我們的程序引用了一個嵌套表中被刪除了的元素或是索引表中未初始化的元素。SQL聚合函數(shù),如AVG和SUM,總是能返回一個值或空。所以,一個調(diào)用聚合函數(shù)的SELECT INTO語句從來不會拋出NO_DATA_FOUND異常。FETCH語句最終會取不到數(shù)據(jù),當(dāng)這種情況發(fā)生時,不會有異常拋出的。 |
| NOT_LOGGED_ON | 程序沒有連接到Oracle就要調(diào)用數(shù)據(jù)庫。 |
| PROGRAM_ERROR | PL/SQL程序發(fā)生內(nèi)部錯誤。 |
| ROWTYPE_MISMATCH | 賦值語句中使用的主游標(biāo)變量和PL/SQL游標(biāo)變量的類型不兼容。例如,當(dāng)一個打開的主游標(biāo)變量傳遞到一個存儲子程序時,實參的返回類型和形參的必須一致。 |
| SELF_IS_NULL | 程序嘗試調(diào)用一個空實例的MEMBER方法。也就是內(nèi)置參數(shù)SELF(它總是第一個傳遞到MEMBER方法的參數(shù))是空。 |
| STORAGE_ERROR | PL/SQL運行時內(nèi)存溢出或內(nèi)存不足。 |
| SUBSCRIPT_BEYOND_COUNT | 程序引用一個嵌套表或變長數(shù)組元素,但使用的下標(biāo)索引超過嵌套表或變長數(shù)組元素總個數(shù)。 |
| SUBSCRIPT_OUTSIDE_LIMIT | 程序引用一個嵌套表或變長數(shù)組,但使用的下標(biāo)索引不在合法的范圍內(nèi)(如-1)。 |
| SYS_INVALID_ROWID | 從字符串向ROWID轉(zhuǎn)換發(fā)生錯誤,因為字符串并不代表一個有效的ROWID。 |
| TIMEOUT_ON_RESOURCE | 當(dāng)Oracle等待資源時,發(fā)生超時現(xiàn)象。 |
| TOO_MANY_ROWS | SELECT INTO語句返回多行數(shù)據(jù)。 |
| VALUE_ERROR | 發(fā)生算術(shù)、轉(zhuǎn)換、截位或長度約束錯誤。例如,當(dāng)我們的程序把一個字段的值放到一個字符變量中時,如果值的長度大于變量的長度,PL/SQL就會終止賦值操作并拋出異常VALUE_ERROR。在過程化語句中,如果字符串向數(shù)字轉(zhuǎn)換失敗,異常VALUE_ERROR就會被拋出。(在SQL語句中,異常INVALID_NUMBER會被拋出。) |
| ZERO_DIVIDE | 程序嘗試除以0。 |
四、自定義PL/SQL異常
PL/SQL允許我們定義自己的異常。與預(yù)定義異常不同的是,用戶自定義異常必須聲明,并且需要用RAISE語句顯式地拋出。
1、聲明PL/SQL異常
異常只能在PL/SQL塊、子程序或包的聲明部分聲明。下例中,我們聲明一個名為past_due的異常:
| DECLARE ??past_due?EXCEPTION; |
異常和變量的聲明是相似的。但是要記住,異常是一種錯誤情況(error condition),而不是數(shù)據(jù)項。與變量不同的是,異常不能出現(xiàn)在賦值語句或是SQL語句中。但是,變量的作用域規(guī)則也適用于異常。
2、PL/SQL異常的作用域規(guī)則
在同一個塊內(nèi),異常不能聲明兩次。但可以在不同的塊聲明相同的異常。
塊中聲明的異常對于當(dāng)前塊來說是本地的,但對于當(dāng)前塊的所有子塊來說是全局的。因為塊只能引用本地或全局的異常,所以封閉塊不能引用聲明在子塊中的異常。
如果我們在子塊中重新聲明了一個全局的異常,本地聲明的異常的優(yōu)先級是要高于全局的。所以,子塊就不能引用全局的異常,除非全局異常在它的所在塊中用標(biāo)簽作了標(biāo)記,這種情況下可以使用下面的語法來引用全局異常:
| block_label.exception_name |
下例中演示了作用范圍規(guī)則:
| 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優(yōu)先級要高于封閉塊聲明的異常。雖然它們的名字相同,但實際上是兩個不同的past_due異常,就像兩個acct_num變量只是共享著相同的名字一樣,實際上它們是完全不同的兩個變量。因此,RAISE語句和WHEN子句所引用的是不同的異常。如果想讓封閉塊能捕獲到子塊中的past_due異常,我們就必須從子塊中刪除聲明,或是在封閉塊中添加OTHERS處理器。
3、把PL/SQL異常與編號關(guān)聯(lián):編譯指示EXCEPTION_INIT
要想控制沒有預(yù)定義名稱的錯誤(通常為 ORA- 消息),我們就必須使用OTHERS處理器或編譯指示EXCEPTION_INIT。編譯指示就是能在編譯期而非運行時進行處理的編譯指令。
在PL/SQL中,編譯指示EXCPTION_INIT能告訴編譯器把異常名稱和錯誤編號關(guān)聯(lián)起來。這就能讓我們按名稱來引用所有的內(nèi)部異常,并為它編寫特定的處理程序。在我們看到的錯誤棧或是錯誤消息序列中,最頂層的就是我們能捕獲和處理的信息。
我們可以把編譯指示EXCEPTION_INIT寫在PL/SQL塊、子程序或包的聲明部分,語法如下:
| PRAGMA?EXCEPTION_INIT(exception_name,?-Oracle_error_number); |
其中exception_name是已經(jīng)聲明過的異常名稱,Oracle_error_number是Oracle錯誤編號。編譯指示必須和異常聲明處于同一個聲明中,并且只能在異常聲明之后出現(xiàn)。如下例所示:
| 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能幫助我們從存儲子程序中拋出用戶自定義的錯誤消息。這樣,我們就能把錯誤消息報告給應(yīng)用程序而避免返回未捕獲異常。
調(diào)用RAISE_APPLICATION_ERROR的語法如下:
| raise_application_error(error_number,?message[,?{TRUE?|?FALSE}]); |
error_number是一個范圍在-20000至-20999之間的負整數(shù),message是最大長度為2048字節(jié)的字符串。如果第三個可選參數(shù)為TRUE的話,錯誤就會被放到前面錯誤的棧頂。如果為FALSE(默認值),錯誤就會替代前面所有的錯誤。
RAISE_APPLICATION_ERROR是包DBMS_STANDARD的一部分,所以,我們對它的引用不需要添加限定修飾詞。
應(yīng)用程序只能從一個正在執(zhí)行的存儲子程序或方法中調(diào)用raise_application_error。在調(diào)用時,raise_application_error會結(jié)束子程序并把用戶定義的錯誤編號和消息返回給應(yīng)用程序。錯誤編號和消息可以像其它的Oracle錯誤一樣被捕獲。
在下面的例子中,我們在雇員工資欄的內(nèi)容為空的情況下調(diào)用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 ????/*?Issue?user-defined?error?message.?*/ ????raise_application_error?(-20101,?'Salary?is?missing'); ??ELSE ????UPDATE?emp ???????SET?sal?=?curr_sal?+?amount ?????WHERE?empno?=?emp_id; ??END?IF; END?raise_salary; |
調(diào)用程序會得到一個PL/SQL異常,它能在OTHERS處理器中使用錯誤報告函數(shù)SQLCODE和SQLERRM來進行處理。同樣,我們也可以使用編譯指示EXCEPTION_INIT把raise_application_error返回的錯誤編號映射到異常本身。如下面的Pro*C例子所示:
| EXEC?SQL?EXECUTE ??/*?Execute?embedded?PL/SQL?block?using?host ??variables?my_emp_id?and?my_amount,?which?were ??assigned?values?in?the?host?environment.?*/ DECLARE ??null_salary???EXCEPTION; ??/*?Map?error?number?returned?by?raise_application_error ??to?user-defined?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; |
這項技術(shù)能讓調(diào)用程序在特定的異常處理程序中控制錯誤。
5、重新聲明預(yù)定義異常
請記住,PL/SQL把預(yù)定義的異常作為全局內(nèi)容聲明在包STANDARD中,所以,我們沒有必要重新聲明它們。重新聲明預(yù)定義異常是錯誤的做法,因為我們的本地聲明會覆蓋掉全局聲明。例如,如果我們聲明了一個invalid_number,當(dāng)PL/SQL拋出預(yù)定義異常INVALID_NUMBER時,我們?yōu)楫惓NVALID_NUMBER編寫的異常控制程序就無法正確地捕獲到它了。這種情況下,我們必須像下面這樣使用點標(biāo)志來指定預(yù)定義異常:
| EXCEPTION ??WHEN?INVALID_NUMBER?OR?STANDARD.INVALID_NUMBER?THEN ????--?handle?the?error END; |
五、如何拋出PL/SQL異常
內(nèi)部異常會由運行時系統(tǒng)隱式地拋出,其中也包括使用編譯指示EXCEPTION_INIT與Oracle錯誤編號關(guān)聯(lián)起來的用戶自定義異常。但是,用戶自定義的異常就必須顯式地用RAISE語句拋出。
1、使用RAISE語句拋出異常
PL/SQL塊和子程序應(yīng)該只在錯誤發(fā)生或無法完成正常程序處理的時候才拋出異常。下例中,我們用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; |
我們也可以顯式地拋出預(yù)定義異常。這樣,為預(yù)定義異常編寫的處理程序也就能夠處理其它錯誤了,示例如下:
| 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在當(dāng)前塊或子程序中沒有找到對應(yīng)的異常控制程序,異常就會被繼續(xù)向上一級傳遞。也就是說異常會把它自身傳遞到后繼的封閉塊直到找到異常處理程序或是再也沒有可以搜索到的塊為止。在后一種情況下,PL/SQL會向主環(huán)境拋出一個未捕獲異常。
但是,異常是不能通過遠程過程調(diào)用(RPC)來傳遞的。因此,PL/SQL塊不能捕獲由遠程子程序拋出的異常。下面三幅圖演示了異常基本的傳遞規(guī)則。
異常可以跨作用域傳遞,也就是說,它能夠超越聲明它的塊的范圍而存在。如下例所示:
| 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所在的塊并沒有專門針對它的處理程序,所以異常就被傳遞到封閉塊。但是,按照作用域規(guī)則,封閉塊是不能引用子塊聲明的異常。所以,只有OTHERS處理器才能捕獲到這個異常。如果沒有用戶定義異常的處理程序,調(diào)用這個程序就會得到下面的錯誤:
| ORA-06510:?PL/SQL:?unhandled?user-defined?exception |
七、重新拋出PL/SQL異常
有時我們需要重新拋出捕獲到異常,也就是說,我們想在本地處理之后再把它傳遞到封閉塊。比如,在異常發(fā)生的時候,我們可能需要回滾事務(wù),然后在封閉塊中寫下錯誤日志。
要重新拋出異常,只要在本地處理程序中放置一個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語句中省略了異常名稱——只允許在異常處理程序中這樣做——程序就會把當(dāng)前的異常重新拋出。
八、處理PL/SQL異常
異常拋出時,PL/SQL塊或子程序的正常執(zhí)行就會停止,控制權(quán)轉(zhuǎn)到塊或子程序的異常處理部分,語法如下:
| 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子句和語句序列組成。這些語句執(zhí)行完畢后,塊或子程序就會結(jié)束,控制權(quán)不再返回異常被拋起的地方。換句話說,也就是我們不能再次返回異常發(fā)生的地方繼續(xù)執(zhí)行我們的程序。
可選的OTHERS處理器總是塊或子程序的最后一個處理程序,它可以用于捕獲所有的未命名異常。因此,塊或子程序只能有一個OTHERS處理器。如下例所示,OTHERS處理器能夠保證所有的異常都會被控制:
| EXCEPTION ??WHEN?...?THEN ????--?handle?the?error ??WHEN?...?THEN ????--?handle?the?error ??WHEN?OTHERS?THEN ????--?handle?all?other?errors END; |
如果我們想讓兩個或更多的異常執(zhí)行同樣的語句序列,只需把異常名稱用關(guān)鍵字OR隔開,放在同一個WHEN子句中即可,如下例所示:
| EXCEPTION ??WHEN?over_limit?OR?under_limit?OR?VALUE_ERROR?THEN ??--?handle?the?error |
只要在WHEN子句的異常列表中有一項與被拋出異常相匹配,相關(guān)的語句序列就會被執(zhí)行。關(guān)鍵字OTHERS不能出現(xiàn)在異常名稱列表中;它只能單獨使用。我們可以有任意數(shù)量的異常處理程序,而且每個處理程序都與一個異常列表及其對應(yīng)的語句序列相關(guān)聯(lián)。但是,異常名稱只能在塊或子程序的異常處理部分出現(xiàn)一次。
變量作用范圍的規(guī)則在這里也同樣適用,所以我們可以在異常處理程序中引用本地或全局變量。但是,當(dāng)游標(biāo)FOR循環(huán)中有異常拋出時,游標(biāo)就會在異常處理程序調(diào)用之前被隱式地關(guān)閉。因此,顯式游標(biāo)的屬性值在異常處理程序中就不再可用了。
1、聲明中控制異常
如果在聲明時使用了錯誤的初始化表達式也有可能引發(fā)異常。例如,下面的聲明就是因常量credit_limit不能存儲超過999的數(shù)字而拋出了異常:
| DECLARE ??credit_limit?CONSTANT?NUMBER(3)?:=?5000;???--?raises?an?exception ??BEGIN ??... EXCEPTION ??WHEN?OTHERS?THEN???--?cannot?catch?the?exception ??... END; |
當(dāng)前塊中的處理程序并不能捕獲到拋出的異常,這是因為聲明時拋出的異常會被立即傳遞到最近的封閉塊中去。
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語句不能跳轉(zhuǎn)到異常控制程序。同樣,GOTO語句也不能從異常控制程序跳轉(zhuǎn)到當(dāng)前塊。例如,下面的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語句可以從一個異常控制程序中跳轉(zhuǎn)到一個封閉塊。
4、獲取錯誤代號與消息:SQLCODE和SQLERRM
在異常處理程序中,我們可以使用內(nèi)置函數(shù)SQLCODE和SQLERRM來查出到底發(fā)生了什么錯誤,并能夠獲取相關(guān)的錯誤信息。對于內(nèi)部異常來說,SQLCODE會返回Oracle錯誤編號。SQLCODE返回的總是一個負數(shù),除非發(fā)生的Oracle錯誤是沒有找到數(shù)據(jù),這時返回的是+100。SQLERRM會返回對應(yīng)的錯誤消息。消息是以O(shè)racle錯誤編號開頭的。
如果我們沒有使用編譯指令EXCEPTION_INIT把異常與編號關(guān)聯(lián)的話,SQLCODE和SQLERRM就會分別返回+1和消息"User-Defined Exception"。Oracle錯誤消息最大長度是512個字符,其中包括錯誤編號、嵌套消息和具體表和字段的名稱。
如果沒有異常拋出,SQLCODE返回0,SQLERRM返回消息"ORA-0000: normal, successful completion"。
我們可以把錯誤編號傳遞給SQLERRM,讓它返回對應(yīng)的錯誤消息。但是,一定要保證我們傳遞給SQLERRM的錯誤編號是負數(shù)。下例中,我們把一個正數(shù)傳遞給SQLERRM,結(jié)果就不是我們想要的那樣的了:
| DECLARE ??err_msg???VARCHAR2(100); BEGIN ??/*?Get?all?Oracle?error?messages.?*/ ??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; |
把正數(shù)傳給SQLERRM時,如果傳遞的是+100,返回的結(jié)果是"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; |
字符串函數(shù)SUBSTR可以保證用SQLERRM為err_msg賦值時不會引起VALUE_ERROR異常。函數(shù)SQLCODE和SQLERRM在OTHERS異常處理程序中特別有用,因為它們能讓我們知道哪個內(nèi)部異常被拋出。
注意:在使用編譯指示RESTRICT_REFERENCES判斷存儲函數(shù)的純度時,如果函數(shù)調(diào)用了SQLCODE和SQLERRM,我們就不能指定約束為WNPS和RNPS了。
5、捕獲未控制異常
記住,如果被拋出的異常找不到合適的異常控制程序,PL/SQL會向主環(huán)境拋出一個未捕獲的異常錯誤,然后由主環(huán)境決定如何處理。例如,在Oracle預(yù)編譯程序環(huán)境中,任何一個執(zhí)行失敗的SQL語句或PL/SQL塊所涉及到的改動都會被回滾。
未捕獲也能影響到子程序。如果我們成功地從子程序中退出,PL/SQL就會把值賦給OUT參數(shù)。但是,如果我們因未捕獲異常而退出程序,PL/SQL就不會為OUT參數(shù)進行賦值。同樣,如果一個存儲子程序因異常而執(zhí)行失敗,PL/SQL也不會回滾子程序所做的數(shù)據(jù)變化。
我們可以在每個PL/SQL程序的頂級使用OTHERS句柄來捕獲那些沒有被子程序捕捉到的異常。
九、PL/SQL錯誤控制技巧
這里,我們將學(xué)習(xí)三個提高程序靈活性的技巧。
1、模擬TRY..CATCH..塊
異常控制程序能讓我們在退出一個塊之前做一些恢復(fù)操作。但是在異常程序完成后,語句塊就會終止。我們不能從異常句柄再重新回到當(dāng)前塊。例如,如果下面的SELECT INTO語句引起了ZERO_DIVIDE異常,我們就不能執(zhí)行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; |
其實我們可以控制某一條語句引起的異常,然后繼續(xù)下一條語句。只要把可能引起異常的語句放到它自己的子塊中,并編寫對應(yīng)的異常控制程序。一旦在子塊中有錯誤發(fā)生,它的本地異常處理程序就能捕獲并處理異常。當(dāng)子塊結(jié)束時,封閉塊程序會繼續(xù)執(zhí)行緊接著的下一條語句。如下例:
| 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。當(dāng)處理程序完成時,子塊也就終止,INSERT語句就會被執(zhí)行。
2、反復(fù)執(zhí)行的事務(wù)
異常發(fā)生后,我們也許還不想放棄我們事務(wù),仍想重新嘗試一次。這項技術(shù)的實現(xiàn)方法就是:
如下例所示。當(dāng)異常處理程序完成時,子塊終止,控制權(quán)被交給外圍塊的LOOP語句,子塊再次重新開始執(zhí)行。而且,我們還可以用FOR或WHILE語句來限制重做的次數(shù)。
| 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 ??????/*?Remove?rows?from?a?table?of?survey?results.?*/ ??????DELETE?FROM?results ????????????WHERE?answer1?=?’no’; ??????/*?Add?a?survey?respondent’s?name?and?answers.?*/ ??????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、使用定位變量標(biāo)記異常發(fā)生點
只用一個異常句柄來捕獲一系列語句的話,可能無法知道到底是哪一條語句產(chǎn)生了錯誤:
| BEGIN ??SELECT?... ??SELECT?... EXCEPTION ??WHEN?NO_DATA_FOUND?THEN?... ??--?Which?SELECT?statement?caused?the?error? END; |
要想解決這個問題,我們可以使用一個定位變量來跟蹤執(zhí)行語句,例如:
| 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; |
轉(zhuǎn)載于:https://www.cnblogs.com/cxd4321/archive/2008/03/19/1113203.html
總結(jié)
以上是生活随笔為你收集整理的第七章 控制PL/SQL错误的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从今天开始研究Flex的相关项目开发
- 下一篇: MS SQL中的returnoutput