Egy PL/SQL programban a kivételek események. A kivételek rendszere a PL/SQL-ben a futás közben bekövetkező események (kiemelt módon a hibák) kezelését teszi lehetővé. A kivételek lehetnek beépített kivételek (amelyeket általában a futtató rendszer vált ki), vagy felhasználói kivételek, amelyeket valamelyik programegység deklarációs részében határoztunk meg. A beépített kivételeknek lehet nevük (például ZERO_DIVIDE), a felhasználói kivételeknek mindig van nevük. A felhasználói kivételeket mindig explicit módon, külön utasítással kell kiváltani. Ezzel az utasítással beépített kivétel is kiváltható.
A kivételek kezelésére a programegységekbe kivételkezelőt építhetünk be.
Egy kivételhez a PL/SQL-ben egy kód és egy üzenet tartozik. A beépített üzenetek kódja (egyetlen esettől eltekintve) negatív, a felhasználói kivételeké pozitív.
A beépített kivételeket a STANDARD csomag definiálja. A megnevezett beépített kivételek és a hozzájuk tartozó, az Oracle-rendszer által adott hibaüzenetek (ezek nyelve az NLS beállításoktól függ) a 7.1. táblázatban láthatók. A 7.2. táblázat ismerteti a beépített kivételek tipikus kiváltó okait.
Kivétel |
Hibakód SQLCODE |
Hibaüzenet SQLERRM |
ACCESS_INTO_NULL |
-6530 |
ORA-06530: Incializálatlan összetett objektumra való hivatkozás |
CASE_NOT_FOUND |
-6592 |
ORA-06592: CASE nem található a CASE állítás végrehajtásakor |
COLLECTION_IS_NULL |
-6531 |
ORA-06531: Inicializálatlan gyűjtőre való hivatkozás |
CURSOR_ALREADY_OPEN |
-6511 |
ORA-06511: PL/SQL: A kurzor már meg van nyitva |
DUP_VAL_ON_INDEX |
-1 |
ORA-00001: A(z) (.) egyediségre vonatkozó megszorítás nem teljesül |
INVALID_CURSOR |
-1001 |
ORA-01001: Nem megengedett kurzor |
INVALID_NUMBER |
-1722 |
ORA-01722: Nem megengedett szám |
LOGIN_DENIED |
-1017 |
ORA-01017: Nem megengedett felhasználónév/jelszó; a bejelentkezés visszautasítva |
NO_DATA_FOUND |
100 |
ORA-01403: Nem talált adatot |
NOT_LOGGED_ON |
-1012 |
ORA-01012: Nincs bejelentkezve |
PROGRAM_ERROR |
–6501 |
ORA-06501: PL/SQL: programhiba |
ROWTYPE_MISMATCH |
-6504 |
ORA-06504: PL/SQL: Az Eredmény halmaz változói vagy a kérdés visszaadott típusai nem illeszkednek |
SELF_IS_NULL |
-30625 |
ORA-30625: A metódus használata nem engedélyezett NULL SELF argumentummal |
STORAGE_ERROR |
-6500 |
ORA-06500: PL/SQL: tárolási hiba |
SUBSCRIPT_BEYOND_COUNT |
-6533 |
ORA-06533: Számlálón kívüli indexérték |
SUBSCRIPT_OUTSIDE_LIMIT |
-6532 |
ORA-06532: Határon kívüli index |
SYS_INVALID_ROWID |
-1410 |
ORA-01410: Nem megengedett ROWID |
TIMEOUT_ON_RESOURCE |
-51 |
ORA-00051: Időtúllépés történt erőforrásra várakozás közben |
TOO_MANY_ROWS |
-1422 |
ORA-01422: A pontos lehívás (FETCH) a kívántnál több sorral tér vissza |
VALUE_ERROR |
-6502 |
ORA-06502: PL/SQL: numerikus- vagy értékhiba |
ZERO_DIVIDE |
-1476 |
ORA-01476: Az osztó értéke nulla |
7.2. táblázat - Beépített kivételek tipikus kiváltó okai
Kivétel |
Kiváltódik, ha … |
ACCESS_INTO_NULL |
Megpróbál értéket adni egy inicializálatlan (automatikusan NULL) objektum attribútumának. |
CASE_NOT_FOUND |
A CASE utasítás egyetlen WHEN ága sem egyezik meg a feltétellel és nincs ELSE ág. |
COLLECTION_IS_NULL |
Megpróbál hivatkozni egy EXISTS-től különböző kollekciómetódusra egy inicializálatlan (automatikusan NULL) beágyazott tábla vagy dinamikus tömb esetén, vagy megpróbál értéket adni egy inicializálatlan beágyazott tábla vagy dinamikus tömb elemének. |
CURSOR_ALREADY_OPEN |
Megpróbál újra megnyitni egy már megnyitott kurzort. A kurzorokat le kell zárni, mielőtt újra megnyitjuk. A kurzor FOR ciklusa automatikusan megnyitja a hozzárendelt kurzort, így azt a ciklusban nem lehet újra megnyitni. |
DUP_VAL_ON_INDEX |
Már létező értéket próbál meg tárolni egy adatbázistábla olyan oszlopában, amelyen egyedi (UNIQUE) indexmegszorítás van. |
INVALID_CURSOR |
Megpróbál műveletet végezni egy meg nem nyitott kurzoron. |
INVALID_NUMBER |
SQL utasításban sikertelen egy karakterlánc konverziója, mert annak tartalma ténylegesen nem egy számot reprezentál. (A procedurális utasításokban ilyen konverziós hiba esetén VALUE_ERROR kivétel váltódik ki.) Ez a kivétel váltódik ki akkor is, ha a LIMIT előírás nem pozitív számot eredményez egy együttes hozzárendelést tartalmazó FETCH utasításban. |
LOGIN_DENIED |
Bejelentkezéskor hibás a felhasználónév vagy a jelszó. |
NO_DATA_FOUND |
Egy SELECT INTO utasítás nem ad vissza sorokat, vagy a programban hivatkozik egy beágyazott tábla törölt, vagy egy asszociatív tömb inicializálatlan elemére. Az SQL csoportfüggvényei, például AVG, SUM, mindig adnak vissza értéket vagy NULL-t, ezért csoportfüggvényt tartalmazó SELECT INTO utasítás sohasem váltja ki ezt a kivételt. |
NOT_LOGGED_ON |
Megpróbál adatbázis-műveletet végrehajtani úgy, hogy nem kapcsolódik Oracle-példányhoz. |
PROGRAM_ERROR |
PL/SQL belső hiba. |
ROWTYPE_MISMATCH |
Egy értékadásban a kurzor gazdaváltozó és az értékül adandó PL/SQL kurzor visszatérési típusa nem kompatibilis. Például abban az esetben, ha egy már megnyitott kurzorváltozót adunk át egy alprogramnak, akkor a formális és aktuális paraméterek visszatérési típusának kompatibilisnek kell lennie. Ugyanígy előfordulhat, hogy egy megnyitott gyenge kurzorváltozót adunk értékül egy erős kurzorváltozónak, és azok visszatérési típusa nem kompatibilis. |
SELF_IS_NULL |
Megpróbálja egy NULL referenciájú objektum metódusát meghívni, azaz a rögzített SELF paraméter (ami minden metódus első paramétere) NULL. |
STORAGE_ERROR |
Elfogyott a PL/SQL számára rendelkezésre álló memória, vagy a memóriaterület megsérült. |
SUBSCRIPT_BEYOND_COUNT |
Egy beágyazott tábla vagy dinamikus tömb méreténél nagyobb indexű elemére hivatkozik. |
SUBSCRIPT_OUTSIDE_LIMIT |
Egy beágyazott tábla vagy dinamikus tömb elemére hivatkozik egy olyan indexszel, ami nincs a megengedett tartományban (például –1). |
SYS_INVALID_ROWID |
Sikertelen egy karakterlánc konverziója ROWID típussá, mert a karakterlánc ténylegesen nem egy ROWID értéket reprezentál. |
TIMEOUT_ON_RESOURCE |
Időtúllépés történik egy erőforrásra várakozás közben. Az erőforrást valaki zárolta. |
TOO_MANY_ROWS |
Egy SELECT INTO utasítás egynél több sort ad vissza. |
VALUE_ERROR |
Aritmetikai, konverziós, csonkítási vagy hosszmegszorítási hiba történik. Például ha egy sztring változónak a deklarált maximális hosszánál hosszabb sztringet próbál meg értékül adni, akár egy SELECT INTO utasítással. Ilyenkor az értékadás érvénytelen, semmis lesz és VALUE_ERROR kivétel váltódik ki. Procedurális utasítások esetén akkor is ez a kivétel váltódik ki, ha egy sztring konverziója számmá sikertelen (SQL utasításokban ilyenkor INVALID_NUMBER kivétel váltódik ki). |
ZERO_DIVIDE |
Megpróbál nullával osztani. |
Felhasználói kivételeket a EXCEPTION alapszóval deklarálhatunk:
DECLARE
sajat_kivetel EXCEPTION;
Olyan beépített kivételhez, amely eredetileg nincs nevesítve, egy pragma segítségével a programunkban nevet rendelhetünk egy programegység deklarációs részében. Ekkor deklarálnunk kell egy felhasználói kivételnevet, majd ugyanezen deklarációs részben később alkalmazni rá a pragmát. Ezután az adott beépített kivételt név szerint tudjuk kezelni. A pragma alakja:
PRAGMA EXCEPTION_INIT(kivételnév,kód);
1. példa
DECLARE
i PLS_INTEGER;
j NUMBER NOT NULL := 1;
/* Egy névtelen hiba nevesítése. */
numeric_overflow EXCEPTION;
PRAGMA EXCEPTION_INIT(numeric_overflow, -1426); -- numeric overflow
/* Egy már amúgy is nevesített kivételhez még egy név rendelése. */
VE_szinonima EXCEPTION;
PRAGMA EXCEPTION_INIT(VE_szinonima, -6502); -- VALUE_ERROR
BEGIN
/* Kezeljük a numeric overflow hibát, PL/SQL-ben ehhez
a hibához nincs előre definiálva kivételnév. */
<<blokk1>>
BEGIN
i := 2**32;
EXCEPTION
WHEN numeric_overflow THEN
DBMS_OUTPUT.PUT_LINE('Blokk1 - numeric_overflow! ' || SQLERRM);
END blokk1;
/* A VE_szinonima használható VALUE_ERROR helyett. */
<<blokk2>>
BEGIN
i := NULL;
j := i; -- VALUE_ERROR-t vált ki, mert i NULL.
DBMS_OUTPUT.PUT_LINE(j);
EXCEPTION
WHEN VE_szinonima THEN
DBMS_OUTPUT.PUT_LINE('Blokk2 - VALUE_ERROR: ' || SQLERRM);
END blokk2;
/* A VALUE_ERROR is használható VE_szinonima helyett. A két kivétel megegyezik. */
<<blokk2>>
BEGIN
RAISE VE_szinonima;
EXCEPTION
WHEN VALUE_ERROR THEN -- A saját kivételünk szinonima a VALUE_ERROR-ra
DBMS_OUTPUT.PUT_LINE('Blokk3 - VALUE_ERROR: ' || SQLERRM);
END blokk1;
END;
/
/*
Eredmény:
Blokk1 - numeric_overflow! ORA-01426: numerikus túlcsordulás
Blokk2 - VALUE_ERROR: ORA-06502: PL/SQL: numerikus- vagy értékhiba ()
Blokk3 - VALUE_ERROR: ORA-06502: PL/SQL: numerikus- vagy értékhiba ()
A PL/SQL eljárás sikeresen befejeződött.
*/
Bármely megnevezett kivétel kiváltható a következő utasítással:
RAISE kivételnév;
Az utasítás bárhol elhelyezhető, ahol végrehajtható utasítás szerepelhet.
Kivételkezelő bármely programegység végén az EXCEPTION alapszó után helyezhető el. Felépítése a következő:
WHEN kivételnév [OR kivételnév]…
THEN utasítás [utasítás]…
[WHEN kivételnév [OR kivételnév]…
THEN utasítás [utasítás]…]…
[WHEN OTHERS THEN utasítás [utasítás]…]
A kivételkezelő tehát olyan WHEN ágakból áll, amelyek név szerint kezelik a kivételeket és legutolsó ágként szerepelhet egy OTHERS ág, amely minden kivételt kezel.
Ha egy blokkban vagy alprogramban a végrehajtható részben bekövetkezik egy kivétel, akkor a végrehajtható rész futása félbeszakad és a futtató rendszer megvizsgálja, hogy a blokk vagy alprogram tartalmaz-e kivételkezelőt. Ha igen, megnézi, hogy valamelyik WHEN ágban nevesítve van-e a bekövetkezett kivétel. Ha igen, akkor végrehajtódnak a THEN után megadott utasítások. Ha köztük van GOTO, akkor a megadott címkén, ha nincs GOTO, blokk esetén a blokkot követő utasításon (ha van tartalmazó programegység) folytatódik a futás, egyébként pedig a vezérlés visszaadódik a hívási környezetbe. Az a programegység, amelyben egy kivétel bekövetkezik, inaktívvá válik, tehát futása nem folytatható. Így GOTO-val sem lehet visszatérni azon végrehajtható részre, ahol a kivétel bekövetkezett.
2. példa
DECLARE
a NUMBER NOT NULL := 1;
b NUMBER;
BEGIN
<<tartalmazo>>
BEGIN
<<vissza>>
a := b; -- VALUE_ERROR-t vált ki, mert b NULL
EXCEPTION
WHEN VALUE_ERROR THEN
b := 1; -- Hogy legközelebb b ne legyen NULL;
-- GOTO vissza; -- fordítási hibát eredményezne
GOTO tartalmazo; -- külső blokk cimkéjére ugorhatunk
END;
END;
/
Ha egyik WHEN ágban sincs nevesítve a kivétel, de van WHEN OTHERS ág, akkor az abban megadott utasítások hajtódnak végre és a folytatás a fent elmondottaknak megfelelő.
Ha nincs OTHERS ág vagy nincs is kivételkezelő, akkor a kivétel továbbadódik. A kivétel továbbadása azt jelenti, hogy a programegység befejeződik, és a futtató rendszer blokk esetén a tartalmazó blokkban, alprogram esetén a hívó programegységben keres megfelelő kivételkezelőt.
Ha nincs több tartalmazó vagy hívó programegység, akkor a hívási környezetben egy UNHANDLED EXCEPTION kivétel váltódik ki. A kivétel távoli eljáráshívás (RPC) esetén nem adódik tovább.
Ha deklarációs részben vagy kivételkezelőben következik be egy kivétel, az azonnal továbbadódik.
3. példa
BEGIN
DECLARE
/* A következő változó inicializációja során
VALUE_ERROR kivétel váltódik ki. */
i NUMBER(5) := 123456;
BEGIN
NULL;
EXCEPTION
WHEN VALUE_ERROR THEN
/* Ez a kezelő nem tudja elkapni a deklarációs
részben bekövetkezett kivételt. */
DBMS_OUTPUT.PUT_LINE('belső');
END;
EXCEPTION
WHEN VALUE_ERROR THEN
/* Ez a kezelő kapja el a kivételt. */
DBMS_OUTPUT.PUT_LINE('külső');
END;
/
/*
Eredmény:
külső
A PL/SQL eljárás sikeresen befejeződött.
*/
Néha szükség lehet egy bekövetkezett kivétel újbóli kiváltására. Például ha egy kivételkezelőben az adott kivétel lokális kezelése után ugyanazt a kivételt tovább kell adni. Erre szolgál a RAISE utasítás kivételnév nélküli alakja, amely csak kivételkezelőben alkalmazható. Hatására újra kiváltódik az a kivétel, amely az adott kivételkezelőt aktiválta (és miután kivételkezelőben váltódott ki, azonnal tovább is adódik).
A kivételkezelőben használható két beépített függvény az SQLCODE és az SQLERRM. Paraméter nélkül az SQLCODE a bekövetkezett kivétel kódját, az SQLERRM a hozzárendelt üzenetet adja meg az NLS beállításoktól függő nyelven. Felhasználói kivételek esetén SQLCODE értéke +1, SQLERRM értéke: ‘User-Defined Exception’.
Az SQLERRM meghívható paraméterrel is, a paraméternek egy kivételkódnak kell lennie. Ekkor az adott kódú kivétel üzenetét adja. Pozitív érték esetén (kivéve a 100-at) mindig a ‘User-Defined Exception’ az eredmény, 0 esetén ‘ORA-0000: normal, successful completion’, negatív kód esetén pedig a beépített kivétel üzenete.
Az SQLCODE és SQLERRM függvények természetesen nem csak kivételkezelőben használhatók, ilyenkor, ha nem volt kivétel, az SQLCODE 0 értékkel tér vissza, az SQLERRM pedig az ennek megfelelő üzenettel.
4. példa
DECLARE
hibas_argumentum EXCEPTION;
v_Datum DATE;
/*
Megnöveli p_Datum-ot a második p_Ido-vel, aminek a mértékegységét
p_Egyseg tartalmazza. Ezek értéke perc', 'óra', 'nap', 'hónap' egyike lehet.
Ha hibás a mértékegység, akkor hibas_argumentum kivétel váltódik ki.
*/
FUNCTION hozzaad(p_Datum DATE, p_Ido NUMBER, p_Egyseg VARCHAR2) RETURN DATE IS
rv DATE;
BEGIN
CASE p_Egyseg
WHEN 'perc' THEN
rv := p_Datum + p_Ido/(24*60);
WHEN 'óra' THEN
rv := p_Datum + p_Ido/24;
WHEN 'nap' THEN
rv := p_Datum + p_Ido;
WHEN 'hónap' THEN
rv := ADD_MONTHS(p_Datum, p_Ido);
ELSE
RAISE hibas_argumentum;
END CASE;
RETURN rv;
END hozzaad;
/* Ez a függvény hibás mértékegység esetén nem vált ki
kivételt, hanem NULL-t ad vissza. */
FUNCTION hozzaad2(p_Datum DATE, p_Ido NUMBER, p_Egyseg VARCHAR2) RETURN DATE IS
rv DATE;
BEGIN
RETURN hozzaad(p_Datum, p_Ido, p_Egyseg);
EXCEPTION
WHEN hibas_argumentum THEN
RETURN NULL;
END hozzaad2;
BEGIN
<<blokk1>>
BEGIN
v_Datum := hozzaad(SYSDATE, 1, 'kiskutyafüle');
EXCEPTION
WHEN hibas_argumentum THEN
DBMS_OUTPUT.PUT_LINE('Blokk1 - hibás argumentum: '
|| SQLCODE || ', ' || SQLERRM);
END blokk1;
<<blokk2>>
BEGIN
v_Datum := hozzaad2(SYSDATE, 1, 'kiskutyafüle');
DBMS_OUTPUT.PUT_LINE('Blokk2 - nincs kivétel.');
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Blokk2 - hiba: '
|| SQLCODE || ', ' || SQLERRM);
RAISE; -- A hiba továbbadása külső irányba.
END blokk2;
END;
/
/*
Eredmény:
Blokk1 - hibás argumentum: 1, User-Defined Exception
Blokk2 - nincs kivétel.
A PL/SQL eljárás sikeresen befejeződött.
*/
A STANDARD csomag tartalmaz egy RAISE_APPLICATION_ERROR eljárást, amelynek három paramétere van:
Az első paraméter egy kivételkód –20000 és –20999 között.
A második paraméter egy maximum 2048 bájt hosszúságú sztring.
A harmadik opcionális paraméter TRUE vagy FALSE (ez az alapértelmezés). TRUE esetén a hibaveremben az első paraméter által megadott kód felülírja a legutolsónak bekövetkezett kivétel kódját, FALSE esetén kiürül a verem és csak a most megadott kód kerül bele.
Az eljárás hívása esetén kiváltódik a megadott felhasználói kivétel a megadott üzenettel.
5. példa
DECLARE
SUBTYPE t_ugyfelrec IS ugyfel%ROWTYPE;
v_Ugyfel t_ugyfelrec;
v_Nev VARCHAR2(10);
/* A függvény megadja az adott keresztnevű ügyfél adatait.
Ha nem egyértelmű a kérdés, akkor ezt kivétellel jelzi. */
FUNCTION ugyfel_nevhez(p_Keresztnev VARCHAR2) RETURN t_ugyfelrec IS
v_Ugyfel t_ugyfelrec;
BEGIN
SELECT * INTO v_Ugyfel FROM ugyfel
WHERE UPPER(nev) LIKE '% %' || UPPER(p_Keresztnev) || '%';
RETURN v_Ugyfel;
EXCEPTION
/* Egy WHEN ág több nevesített kivételt is kezelhet. */
WHEN NO_DATA_FOUND OR TOO_MANY_ROWS THEN
RAISE_APPLICATION_ERROR(-20010,
'A keresett ügyfél nem vagy nem egyértelműen létezik');
END ugyfel_nevhez;
BEGIN
FOR i IN 1..3 LOOP
CASE i
WHEN 1 THEN v_Nev := 'Máté'; -- Egy Máté van a könyvtárban.
WHEN 2 THEN v_Nev := 'István'; -- Több István is van.
WHEN 3 THEN v_Nev := 'Gergő'; -- Nincs Gergő nálunk.
END CASE;
<<blokk1>>
BEGIN
DBMS_OUTPUT.PUT_LINE(i || '. Keresett név: "' || v_Nev || '".');
v_Ugyfel := ugyfel_nevhez(v_Nev);
DBMS_OUTPUT.PUT_LINE('Nincs hiba, ügyfél: ' || v_Ugyfel.nev);
EXCEPTION
/* Csak a WHEN OTHERS ág tudja elkapni a névtelen kivételeket. */
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Hiba: ' || SQLCODE || ', ' || SQLERRM);
END blokk1;
END LOOP;
/* A felhasználói hiba számához is rendelhetünk kivételt. */
<<blokk2>>
DECLARE
hibas_ugyfelnev EXCEPTION;
PRAGMA EXCEPTION_INIT(hibas_ugyfelnev, -20010);
BEGIN
DBMS_OUTPUT.NEW_LINE;
DBMS_OUTPUT.PUT_LINE('Blokk2 - Szilveszter van-e?');
v_Ugyfel := ugyfel_nevhez('Szilveszter'); -- Persze nincs.
DBMS_OUTPUT.PUT_LINE('Igen van: ' || v_Ugyfel.nev);
EXCEPTION
WHEN hibas_ugyfelnev THEN
DBMS_OUTPUT.PUT_LINE('Hiba: '
|| SQLCODE || ', ' || SQLERRM);
RAISE; -- A hiba továbbadása
END blokk2;
/* Mivel itt nincs kivételkezelő, a blokk2 kivételkezelőjéből
továbbadott kivételt a futtató rendszer kezeli. */
END;
/
/*
Eredmény:
1. Keresett név: "Máté".
Nincs hiba, ügyfél: Szabó Máté István
2. Keresett név: "István".
Hiba: -20010, ORA-20010: A keresett ügyfél nem vagy nem egyértelműen létezik
3. Keresett név: "Gergő".
Hiba: -20010, ORA-20010: A keresett ügyfél nem vagy nem egyértelműen létezik
Blokk2 - Szilveszter van-e?
Hiba: -20010, ORA-20010: A keresett ügyfél nem vagy nem egyértelműen létezik
Hiba a(z) 1. sorban:
DECLARE
*
ORA-20010: A keresett ügyfél nem vagy nem egyértelműen létezik
ORA-06512: a(z) helyen a(z) 53. sornál
*/