控制PL/SQL錯誤 EXCEPTION,SQLCODE,SQLERRM --瀏覽時(shí)可通過(guò)查找功能跳躍式的尋找自己要查看的內容!
一、錯誤控制一覽
在PL/SQL中,警告或錯誤被稱(chēng)為異常。異??梢允莾炔?運行時(shí)系統)定義的或是用戶(hù)定義的。內部定義的案例包括除零操作和內存溢出等。一些常見(jiàn)的內部異常都有一個(gè)預定義的名字,如ZERO_DIVIDE和STORAGE_ERROR等。對于其它的內部異常,我們可以手動(dòng)為它們命名。
我們可以在PL/SQL塊、子程序或包的聲明部分自定義異常。例如,我們可以定義一個(gè)名為insufficient_funds的異常來(lái)標示帳戶(hù)透支的情況。與內部異常不同的是,用戶(hù)自定義異常必須有一個(gè)名字。
錯誤發(fā)生時(shí),異常就會(huì )被拋出。也就是說(shuō),正常的執行語(yǔ)句會(huì )被終止,控制權被轉到PL/SQL塊的異??刂撇糠只蜃映绦虻漠惓?刂撇糠?。內部異常會(huì )由運行時(shí)系統隱式地拋出,而用戶(hù)定義異常必須顯式地用RAISE語(yǔ)句拋出,RAISE語(yǔ)句也可以?huà)伋鲱A定義異常。
為了控制被拋出的異常,我們需要單獨編寫(xiě)被稱(chēng)為"exception handler"的異??刂瞥绦?。異??刂瞥绦蜻\行后,當前塊就會(huì )停止執行,封閉塊繼續執行下一條語(yǔ)句。如果沒(méi)有封閉塊,控制權會(huì )直接交給主環(huán)境。
下例中,我們?yōu)橐患夜善?a onclick="javascript:tagshow(event, '%B4%FA%C2%EB');" href="javascript:;" target="_self">代碼(Ticker Symbol)為XYZ的公司計算并保存市盈率(price-to-earning)。如果公司的收入為零,預定義異常ZERO_DIVIDE就會(huì )被拋出。這將導致正常的執行被終止,控制權被交給異??刂瞥绦?。可選的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語(yǔ)句的使用就有些低效了。使用下面的語(yǔ)句就要好一些:
INSERT INTO stats (symbol, ratio)
SELECT symbol,DECODE(earnings, 0, NULL, price / earnings)
FROM stocks
WHERE symbol = 'XYZ';
在下面這個(gè)例子中,子查詢(xún)?yōu)镮NSERT語(yǔ)句提供了數據。如果earnings是零的話(huà),函數DECODE就會(huì )返回空,否則DECODE就會(huì )返回price與earnings的比值。
二、異常的優(yōu)點(diǎn)
使用異常來(lái)控制錯誤有幾個(gè)優(yōu)點(diǎn)。如果沒(méi)有異??刂频脑?huà),每次執行一條語(yǔ)句,我們都必須進(jìn)行錯誤檢查:
BEGIN
SELECT ...
-- check for ’no data found’ error
SELECT ...
-- check for ’no data found’ error
SELECT ...
-- check for ’no data found’ error
錯誤處理和正常的處理內容界限不明顯,導致代碼混亂。如果我們不編寫(xiě)錯誤檢查代碼,一個(gè)錯誤就可能引起其它錯誤,有時(shí)還可能是一些無(wú)關(guān)錯誤。
但有了異常后,我們就能很方便的控制錯誤,而且不需要編寫(xiě)多個(gè)檢查代碼:
BEGIN
SELECT ...
SELECT ...
SELECT ...
...
EXCEPTION
WHEN NO_DATA_FOUND THEN -- catches all 'no data found' errors
異常能把錯誤控制程序單獨分離出來(lái),改善可讀性,主要的算法不會(huì )受到錯誤恢復算法影響。異常還可以提高可靠性。我們不需要在每一個(gè)可能出現錯誤的地方編寫(xiě)錯誤檢查代碼了,只要在PL/SQL塊中添加一個(gè)異??刂拼a即可。這樣,如果有異常被拋出,我們就可以確保它能夠被捕獲并處理。
三、預定義PL/SQL異常
當我們的PL/SQL程序與Oracle規則相沖突或超過(guò)系統相關(guān)(system-dependent)的限制時(shí),內部異常就會(huì )被拋出。每個(gè)Oracle錯誤都有一個(gè)錯誤編號,但異常只能按名稱(chēng)捕獲,然后被處理。所以,PL/SQL把一些常見(jiàn)Oracle錯誤定義為異常。例如,如果SELECT INTO語(yǔ)句查詢(xún)不到數據時(shí),PL/SQL就會(huì )拋出預定義異常NO_DATA_FOUND。
要控制其它Oracle異常,我們可以使用OTHERS處理器。函數SQLCODE和SQLERRM在OTHERS處理器中特別有用,因為它們能返回Oracle錯誤編號和消息。另外,我們還可以使用編譯指示(pragma)EXCEPTION_INIT把一個(gè)異常名稱(chēng)和一個(gè)Oracle錯誤編號關(guān)聯(lián)起來(lái)。PL/SQL在STANDARD包中聲明了全局預定義異常。所以,我們不需要自己聲明它們。我們可以為下面列表中命名的預定義異常編寫(xiě)處理程序:
異常 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_NUMBERORA-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
預定義異常的簡(jiǎn)要描述:
異常 拋出時(shí)機
ACCESS_INTO_NULL程序嘗試為一個(gè)未初始化(自動(dòng)賦為null)對象的屬性賦值。
CASE_NOT_FOUNDCASE語(yǔ)句中沒(méi)有任何WHEN子句滿(mǎn)足條件,并且沒(méi)有編寫(xiě)ELSE子句。
COLLECTION_IS_NULL程序嘗試調用一個(gè)未初始化(自動(dòng)賦為null)嵌套表或變長(cháng)數組的集合方法(不包括EXISTS),或者是程序嘗試為一個(gè)未初始化嵌套表或變長(cháng)數組的元素賦值。
CURSOR_ALREADY_OPEN程序嘗試打開(kāi)一個(gè)已經(jīng)打開(kāi)的游標。一個(gè)游標在重新打開(kāi)之前必須關(guān)閉。一個(gè)游標FOR循環(huán)會(huì )自動(dòng)打開(kāi)它所引用的游標。所以,我們的程序不能在循環(huán)內部打開(kāi)游標。
DUP_VAL_ON_INDEX程序嘗試向一個(gè)有著(zhù)唯一約束條件的數據庫字段中保存重復值。
INVALID_CURSOR程序嘗試操作一個(gè)不合法的游標,例如關(guān)閉一個(gè)未打開(kāi)的游標。
INVALID_NUMBER在一個(gè)SQL語(yǔ)句中,由于字符串并不代表一個(gè)有效的數字,導致字符串向數字轉換時(shí)會(huì )發(fā)生錯誤。(在過(guò)程化語(yǔ)句中,會(huì )拋出異常VALUE_ERROR。)當FETCH語(yǔ)句的LIMIT子句表達式后面不是一個(gè)正數時(shí),這個(gè)異常也會(huì )被拋出。
LOGIN_DENIED程序嘗試使用無(wú)效的用戶(hù)名和/或密碼來(lái)登錄Oracle。
NO_DATA_FOUNDSELECT INTO語(yǔ)句沒(méi)有返回數據,或者是我們的程序引用了一個(gè)嵌套表中被刪除了的元素或是索引表中未初始化的元素。SQL聚合函數,如AVG和SUM,總是能返回一個(gè)值或空。所以,一個(gè)調用聚合函數的SELECT INTO語(yǔ)句從來(lái)不會(huì )拋出NO_DATA_FOUND異常。FETCH語(yǔ)句最終會(huì )取不到數據,當這種情況發(fā)生時(shí),不會(huì )有異常拋出的。
NOT_LOGGED_ON 程序沒(méi)有連接到Oracle就要調用數據庫。
PROGRAM_ERROR PL/SQL程序發(fā)生內部錯誤。
ROWTYPE_MISMATCH賦值語(yǔ)句中使用的主游標變量和PL/SQL游標變量的類(lèi)型不兼容。例如,當一個(gè)打開(kāi)的主游標變量傳遞到一個(gè)存儲子程序時(shí),實(shí)參的返回類(lèi)型和形參的必須一致。
SELF_IS_NULL程序嘗試調用一個(gè)空實(shí)例的MEMBER方法。也就是內置參數SELF(它總是第一個(gè)傳遞到MEMBER方法的參數)是空。
STORAGE_ERRORPL/SQL運行時(shí)內存溢出或內存不足。
SUBSCRIPT_BEYOND_COUNT程序引用一個(gè)嵌套表或變長(cháng)數組元素,但使用的下標索引超過(guò)嵌套表或變長(cháng)數組元素總個(gè)數。
SUBSCRIPT_OUTSIDE_LIMIT程序引用一個(gè)嵌套表或變長(cháng)數組,但使用的下標索引不在合法的范圍內(如-1)。
SYS_INVALID_ROWID從字符串向ROWID轉換發(fā)生錯誤,因為字符串并不代表一個(gè)有效的ROWID。
TIMEOUT_ON_RESOURCE當Oracle等待資源時(shí),發(fā)生超時(shí)現象。
TOO_MANY_ROWSSELECT INTO語(yǔ)句返回多行數據。
VALUE_ERROR發(fā)生算術(shù)、轉換、截位或長(cháng)度約束錯誤。例如,當我們的程序把一個(gè)字段的值放到一個(gè)字符變量中時(shí),如果值的長(cháng)度大于變量的長(cháng)度,PL/SQL就會(huì )終止賦值操作并拋出異常VALUE_ERROR。在過(guò)程化語(yǔ)句中,如果字符串向數字轉換失敗,異常VALUE_ERROR就會(huì )被拋出。(在SQL語(yǔ)句中,異常INVALID_NUMBER會(huì )被拋出。)
ZERO_DIVIDE 程序嘗試除以0。
四、自定義PL/SQL異常
PL/SQL允許我們定義自己的異常。與預定義異常不同的是,用戶(hù)自定義異常必須聲明,并且需要用RAISE語(yǔ)句顯式地拋出。
1、聲明PL/SQL異常
異常只能在PL/SQL塊、子程序或包的聲明部分聲明。下例中,我們聲明一個(gè)名為past_due的異常:
DECLARE
past_due EXCEPTION;
異常和變量的聲明是相似的。但是要記住,異常是一種錯誤情況(error condition),而不是數據項。與變量不同的是,異常不能出現在賦值語(yǔ)句或是SQL語(yǔ)句中。但是,變量的作用域規則也適用于異常。
2、PL/SQL異常的作用域規則
在同一個(gè)塊內,異常不能聲明兩次。但可以在不同的塊聲明相同的異常。
塊中聲明的異常對于當前塊來(lái)說(shuō)是本地的,但對于當前塊的所有子塊來(lái)說(shuō)是全局的。因為塊只能引用本地或全局的異常,所以封閉塊不能引用聲明在子塊中的異常。
如果我們在子塊中重新聲明了一個(gè)全局的異常,本地聲明的異常的優(yōu)先級是要高于全局的。所以,子塊就不能引用全局的異常,除非全局異常在它的所在塊中用標簽作了標記,這種情況下可以使用下面的語(yǔ)法來(lái)引用全局異常:
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;
上例中的封閉塊并不能捕獲拋出來(lái)的異常,因為在子塊中聲明的past_due優(yōu)先級要高于封閉塊聲明的異常。雖然它們的名字相同,但實(shí)際上是兩個(gè)不同的past_due異常,就像兩個(gè)acct_num變量只是共享著(zhù)相同的名字一樣,實(shí)際上它們是完全不同的兩個(gè)變量。因此,RAISE語(yǔ)句和WHEN子句所引用的是不同的異常。如果想讓封閉塊能捕獲到子塊中的past_due異常,我們就必須從子塊中刪除聲明,或是在封閉塊中添加OTHERS處理器。
3、把PL/SQL異常與編號關(guān)聯(lián):編譯指示EXCEPTION_INIT
要想控制沒(méi)有預定義名稱(chēng)的錯誤(通常為 ORA- 消息),我們就必須使用OTHERS處理器或編譯指示EXCEPTION_INIT。編譯指示就是能在編譯期而非運行時(shí)進(jìn)行處理的編譯指令。
在PL/SQL中,編譯指示EXCPTION_INIT能告訴編譯器把異常名稱(chēng)和錯誤編號關(guān)聯(lián)起來(lái)。這就能讓我們按名稱(chēng)來(lái)引用所有的內部異常,并為它編寫(xiě)特定的處理程序。在我們看到的錯誤?;蚴清e誤消息序列中,最頂層的就是我們能捕獲和處理的信息。
我們可以把編譯指示EXCEPTION_INIT寫(xiě)在PL/SQL塊、子程序或包的聲明部分,語(yǔ)法如下:
PRAGMA EXCEPTION_INIT(exception_name, -Oracle_error_number);
其中exception_name是已經(jīng)聲明過(guò)的異常名稱(chēng),Oracle_error_number是Oracle錯誤編號。編譯指示必須和異常聲明處于同一個(gè)聲明中,并且只能在異常聲明之后出現。如下例所示:
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、自定我們自己的錯誤消息:過(guò)程RAISE_APPLICATION_ERROR
過(guò)程RAISE_APPLICATION_ERROR能幫助我們從存儲子程序中拋出用戶(hù)自定義的錯誤消息。這樣,我們就能把錯誤消息報告給應用程序而避免返回未捕獲異常。
調用RAISE_APPLICATION_ERROR的語(yǔ)法如下:
raise_application_error(error_number, message[, {TRUE | FALSE}]);
error_number是一個(gè)范圍在-20000至-20999之間的負整數,message是最大長(cháng)度為2048字節的字符串。如果第三個(gè)可選參數為T(mén)RUE的話(huà),錯誤就會(huì )被放到前面錯誤的棧頂。如果為FALSE(默認值),錯誤就會(huì )替代前面所有的錯誤。
RAISE_APPLICATION_ERROR是包DBMS_STANDARD的一部分,所以,我們對它的引用不需要添加限定修飾詞。
應用程序只能從一個(gè)正在執行的存儲子程序或方法中調用raise_application_error。在調用時(shí),raise_application_error會(huì )結束子程序并把用戶(hù)定義的錯誤編號和消息返回給應用程序。錯誤編號和消息可以像其它的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
/* 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;
調用程序會(huì )得到一個(gè)PL/SQL異常,它能在OTHERS處理器中使用錯誤報告函數SQLCODE和SQLERRM來(lái)進(jìn)行處理。同樣,我們也可以使用編譯指示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ù)能讓調用程序在特定的異常處理程序中控制錯誤。
5、重新聲明預定義異常
請記住,PL/SQL把預定義的異常作為全局內容聲明在包STANDARD中,所以,我們沒(méi)有必要重新聲明它們。重新聲明預定義異常是錯誤的做法,因為我們的本地聲明會(huì )覆蓋掉全局聲明。例如,如果我們聲明了一個(gè)invalid_number,當PL/SQL拋出預定義異常INVALID_NUMBER時(shí),我們?yōu)楫惓NVALID_NUMBER編寫(xiě)的異??刂瞥绦蚓蜔o(wú)法正確地捕獲到它了。這種情況下,我們必須像下面這樣使用點(diǎn)標志來(lái)指定預定義異常:
EXCEPTION
WHENINVALID_NUMBER OR STANDARD.INVALID_NUMBER THEN
-- handle the error
END;
五、如何拋出PL/SQL異常
內部異常會(huì )由運行時(shí)系統隱式地拋出,其中也包括使用編譯指示EXCEPTION_INIT與Oracle錯誤編號關(guān)聯(lián)起來(lái)的用戶(hù)自定義異常。但是,用戶(hù)自定義的異常就必須顯式地用RAISE語(yǔ)句拋出。
1、使用RAISE語(yǔ)句拋出異常
PL/SQL塊和子程序應該只在錯誤發(fā)生或無(wú)法完成正常程序處理的時(shí)候才拋出異常。下例中,我們用RAISE語(yǔ)句拋出一個(gè)用戶(hù)自定義的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;
我們也可以顯式地拋出預定義異常。這樣,為預定義異常編寫(xiě)的處理程序也就能夠處理其它錯誤了,示例如下:
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異常的傳遞
異常被拋出時(shí),如果PL/SQL在當前塊或子程序中沒(méi)有找到對應的異??刂瞥绦?,異常就會(huì )被繼續向上一級傳遞。也就是說(shuō)異常會(huì )把它自身傳遞到后繼的封閉塊直到找到異常處理程序或是再也沒(méi)有可以搜索到的塊為止。在后一種情況下,PL/SQL會(huì )向主環(huán)境拋出一個(gè)未捕獲異常。
但是,異常是不能通過(guò)遠程過(guò)程調用(RPC)來(lái)傳遞的。因此,PL/SQL塊不能捕獲由遠程子程序拋出的異常。
異??梢钥缱饔糜騻鬟f,也就是說(shuō),它能夠超越聲明它的塊的范圍而存在。如下例所示:
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所在的塊并沒(méi)有專(zhuān)門(mén)針對它的處理程序,所以異常就被傳遞到封閉塊。但是,按照作用域規則,封閉塊是不能引用子塊聲明的異常。所以,只有OTHERS處理器才能捕獲到這個(gè)異常。如果沒(méi)有用戶(hù)定義異常的處理程序,調用這個(gè)程序就會(huì )得到下面的錯誤:
ORA-06510: PL/SQL: unhandled user-defined exception
七、重新拋出PL/SQL異常
有時(shí)我們需要重新拋出捕獲到異常,也就是說(shuō),我們想在本地處理之后再把它傳遞到封閉塊。比如,在異常發(fā)生的時(shí)候,我們可能需要回滾事務(wù),然后在封閉塊中寫(xiě)下錯誤日志。
要重新拋出異常,只要在本地處理程序中放置一個(gè)RAISE語(yǔ)句即可,示例如下:
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語(yǔ)句中省略了異常名稱(chēng)——只允許在異常處理程序中這樣做——程序就會(huì )把當前的異常重新拋出。
八、處理PL/SQL異常
異常拋出時(shí),PL/SQL塊或子程序的正常執行就會(huì )停止,控制權轉到塊或子程序的異常處理部分,語(yǔ)法如下:
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;
為捕獲拋出的異常,我們需要編寫(xiě)異常處理程序。每個(gè)處理程序都由一個(gè)WHEN子句和語(yǔ)句序列組成。這些語(yǔ)句執行完畢后,塊或子程序就會(huì )結束,控制權不再返回異常被拋起的地方。換句話(huà)說(shuō),也就是我們不能再次返回異常發(fā)生的地方繼續執行我們的程序。
可選的OTHERS處理器總是塊或子程序的最后一個(gè)處理程序,它可以用于捕獲所有的未命名異常。因此,塊或子程序只能有一個(gè)OTHERS處理器。如下例所示,OTHERS處理器能夠保證所有的異常都會(huì )被控制:
EXCEPTION
WHEN ... THEN
-- handle the error
WHEN ... THEN
-- handle the error
WHEN OTHERS THEN
-- handle all other errors
END;
如果我們想讓兩個(gè)或更多的異常執行同樣的語(yǔ)句序列,只需把異常名稱(chēng)用關(guān)鍵字OR隔開(kāi),放在同一個(gè)WHEN子句中即可,如下例所示:
EXCEPTION
WHEN over_limit OR under_limit OR VALUE_ERROR THEN
-- handle the error
只要在WHEN子句的異常列表中有一項與被拋出異常相匹配,相關(guān)的語(yǔ)句序列就會(huì )被執行。關(guān)鍵字OTHERS不能出現在異常名稱(chēng)列表中;它只能單獨使用。我們可以有任意數量的異常處理程序,而且每個(gè)處理程序都與一個(gè)異常列表及其對應的語(yǔ)句序列相關(guān)聯(lián)。但是,異常名稱(chēng)只能在塊或子程序的異常處理部分出現一次。
變量作用范圍的規則在這里也同樣適用,所以我們可以在異常處理程序中引用本地或全局變量。但是,當游標FOR循環(huán)中有異常拋出時(shí),游標就會(huì )在異常處理程序調用之前被隱式地關(guān)閉。因此,顯式游標的屬性值在異常處理程序中就不再可用了。
1、聲明中控制異常
如果在聲明時(shí)使用了錯誤的初始化表達式也有可能引發(fā)異常。例如,下面的聲明就是因常量credit_limit不能存儲超過(guò)999的數字而拋出了異常:
DECLARE
credit_limit CONSTANT NUMBER(3) := 5000; -- raises an exception
BEGIN
...
EXCEPTION
WHEN OTHERS THEN -- cannot catch the exception
...
END;
當前塊中的處理程序并不能捕獲到拋出的異常,這是因為聲明時(shí)拋出的異常會(huì )被立即傳遞到最近的封閉塊中去。
2、異常句柄中控制異常
在一個(gè)塊或子程序中,一次只能有一個(gè)異常被激活。所以,一個(gè)被異常處理程序拋出的異常會(huì )被立即傳遞到封閉塊,在那兒,封閉塊會(huì )為它查找新的處理程序。從那一刻起,異常傳遞才開(kāi)始正?;?。參考下面的例子:
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語(yǔ)句不能跳轉到異??刂瞥绦?。同樣,GOTO語(yǔ)句也不能從異??刂瞥绦蛱D到當前塊。例如,下面的GOTO語(yǔ)句就是非法的:
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語(yǔ)句可以從一個(gè)異??刂瞥绦蛑刑D到一個(gè)封閉塊。
4、獲取錯誤代號與消息:SQLCODE和SQLERRM
在異常處理程序中,我們可以使用內置函數SQLCODE和SQLERRM來(lái)查出到底發(fā)生了什么錯誤,并能夠獲取相關(guān)的錯誤信息。對于內部異常來(lái)說(shuō),SQLCODE會(huì )返回Oracle錯誤編號。SQLCODE返回的總是一個(gè)負數,除非發(fā)生的Oracle錯誤是沒(méi)有找到數據,這時(shí)返回的是+100。SQLERRM會(huì )返回對應的錯誤消息。消息是以Oracle錯誤編號開(kāi)頭的。
如果我們沒(méi)有使用編譯指令EXCEPTION_INIT把異常與編號關(guān)聯(lián)的話(huà),SQLCODE和SQLERRM就會(huì )分別返回+1和消息"User-Defined Exception"。Oracle錯誤消息最大長(cháng)度是512個(gè)字符,其中包括錯誤編號、嵌套消息和具體表和字段的名稱(chēng)。
如果沒(méi)有異常拋出,SQLCODE返回0,SQLERRM返回消息"ORA-0000: normal, successful completion"。
我們可以把錯誤編號傳遞給SQLERRM,讓它返回對應的錯誤消息。但是,一定要保證我們傳遞給SQLERRM的錯誤編號是負數。下例中,我們把一個(gè)正數傳遞給SQLERRM,結果就不是我們想要的那樣的了:
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;
把正數傳給SQLERRM時(shí),如果傳遞的是+100,返回的結果是"no data found",其他情況總是會(huì )返回消息"user-defined exception"。把0傳遞給SQLERRM,就會(huì )返回消息"normal, successful completion"。
我們不能直接在SQL語(yǔ)句中使用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賦值時(shí)不會(huì )引起VALUE_ERROR異常。函數SQLCODE和SQLERRM在OTHERS異常處理程序中特別有用,因為它們能讓我們知道哪個(gè)內部異常被拋出。
注意:在使用編譯指示RESTRICT_REFERENCES判斷存儲函數的純度時(shí),如果函數調用了SQLCODE和SQLERRM,我們就不能指定約束為WNPS和RNPS了。
5、捕獲未控制異常
記住,如果被拋出的異常找不到合適的異??刂瞥绦?,PL/SQL會(huì )向主環(huán)境拋出一個(gè)未捕獲的異常錯誤,然后由主環(huán)境決定如何處理。例如,在Oracle預編譯程序環(huán)境中,任何一個(gè)執行失敗的SQL語(yǔ)句或PL/SQL塊所涉及到的改動(dòng)都會(huì )被回滾。
未捕獲也能影響到子程序。如果我們成功地從子程序中退出,PL/SQL就會(huì )把值賦給OUT參數。但是,如果我們因未捕獲異常而退出程序,PL/SQL就不會(huì )為OUT參數進(jìn)行賦值。同樣,如果一個(gè)存儲子程序因異常而執行失敗,PL/SQL也不會(huì )回滾子程序所做的數據變化。
我們可以在每個(gè)PL/SQL程序的頂級使用OTHERS句柄來(lái)捕獲那些沒(méi)有被子程序捕捉到的異常。
九、PL/SQL錯誤控制技巧
這里,我們將學(xué)習三個(gè)提高程序靈活性的技巧。
1、模擬TRY..CATCH..塊
異??刂瞥绦蚰茏屛覀冊谕顺鲆粋€(gè)塊之前做一些恢復操作。但是在異常程序完成后,語(yǔ)句塊就會(huì )終止。我們不能從異常句柄再重新回到當前塊。例如,如果下面的SELECT INTO語(yǔ)句引起了ZERO_DIVIDE異常,我們就不能執行INSERT語(yǔ)句了:
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;
其實(shí)我們可以控制某一條語(yǔ)句引起的異常,然后繼續下一條語(yǔ)句。只要把可能引起異常的語(yǔ)句放到它自己的子塊中,并編寫(xiě)對應的異??刂瞥绦?。一旦在子塊中有錯誤發(fā)生,它的本地異常處理程序就能捕獲并處理異常。當子塊結束時(shí),封閉塊程序會(huì )繼續執行緊接著(zhù)的下一條語(yǔ)句。如下例:
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;
在上面這個(gè)例子中,如果SELECT INTO語(yǔ)句拋出了ZERO_DIVIDE異常,本地異常處理程序就會(huì )捕捉到它并把pe_ratio賦值為0。當處理程序完成時(shí),子塊也就終止,INSERT語(yǔ)句就會(huì )被執行。
2、反復執行的事務(wù)
異常發(fā)生后,我們也許還不想放棄我們事務(wù),仍想重新嘗試一次。這項技術(shù)的實(shí)現方法就是:
把事務(wù)裝入一個(gè)子塊中。
把子塊放入一個(gè)循環(huán),然后反復執行事務(wù)
在開(kāi)始事務(wù)之前標記一個(gè)保存點(diǎn)。如果事務(wù)執行成功的話(huà),就提交事務(wù)并退出循環(huán)。如果事務(wù)執行失敗,控制權就會(huì )交給異常處理程序,事務(wù)回滾到保存點(diǎn),然后重新嘗試執行事務(wù)。
如下例所示。當異常處理程序完成時(shí),子塊終止,控制權被交給外圍塊的LOOP語(yǔ)句,子塊再次重新開(kāi)始執行。而且,我們還可以用FOR或WHILE語(yǔ)句來(lái)限制重做的次數。
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、使用定位變量標記異常發(fā)生點(diǎn)
只用一個(gè)異常句柄來(lái)捕獲一系列語(yǔ)句的話(huà),可能無(wú)法知道到底是哪一條語(yǔ)句產(chǎn)生了錯誤:
BEGIN
SELECT ...
SELECT ...
EXCEPTION
WHEN NO_DATA_FOUND THEN ...
-- Which SELECT statement caused the error?
END;
要想解決這個(gè)問(wèn)題,我們可以使用一個(gè)定位變量來(lái)跟蹤執行語(yǔ)句,例如:
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;
聯(lián)系客服