7. fejezet - Kivételkezelés

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.

7.1. táblázat - Hibaüzenetek

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 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
*/