Együttes hozzárendelés

A PL/SQL motor minden procedurális utasítást végrehajt, azonban a PL/SQL programba beépített SQL utasításokat átadja az SQL motornak. Az SQL motor fogadja az utasítást, végrehajtja azt és esetleg adatokat szolgáltat vissza a PL/SQL motornak. Minden egyes ilyen motorváltás növeli a végrehajtási időt. Ha sok a váltás, csökken a teljesítmény. Különösen igaz ez, ha az SQL utasítás ciklus magjába van ágyazva, például egy kollekció elemeinek egyenkénti feldolgozásánál.

1. példa

DECLARE
  /* Összegyűjtjük a lejárt kölcsönzésekhez tartozó ügyfeleket */
  TYPE t_ugyfelek IS TABLE OF ugyfel%ROWTYPE;
  TYPE t_konyvek IS TABLE OF konyv%ROWTYPE;
  v_Ugyfelek t_ugyfelek := t_ugyfelek();
  v_Konyvek t_konyvek := t_konyvek();
  -- Rögzítjük a mát a példa kedvéért
  v_Ma DATE := TO_DATE('2002-05-11', 'YYYY-MM-DD');
  PROCEDURE feldolgoz(
  p_Ugyfelek t_ugyfelek,
  p_Konyvek t_konyvek
  ) IS
  BEGIN
  FOR i IN 1..p_Ugyfelek.COUNT LOOP
  DBMS_OUTPUT.PUT_LINE(p_Ugyfelek(i).nev || ' - ' || v_Konyvek(i).cim);
  END LOOP;
  END feldolgoz;
BEGIN
  -- Ebben a ciklusban sok a SELECT, sok a motorváltás
  FOR bejegyzes IN (SELECT * FROM kolcsonzes) LOOP
    IF TRUNC(bejegyzes.datum) + 30*(bejegyzes.hosszabbitva + 1) < v_Ma THEN
      v_Ugyfelek.EXTEND;
      v_Konyvek.EXTEND;
      SELECT *
      INTO v_Ugyfelek(v_Ugyfelek.LAST)
      FROM ugyfel
      WHERE id = bejegyzes.kolcsonzo;
      SELECT *
      INTO v_Konyvek(v_Konyvek.LAST)
      FROM konyv
      WHERE id = bejegyzes.konyv;
    END IF;
  END LOOP;
  END;
/

/* Eredmény:
József István - Piszkos Fred és a többiek
József István - ECOOP 2001 - Object-Oriented Programming
József István - Java - start!
József István - Matematikai zseblexikon
József István - Matematikai Kézikönyv
Jaripekka Hämälainen - A critical introduction to twentieth-century American drama - Volume 2
Jaripekka Hämälainen - The Norton Anthology of American Literature - Second Edition - Volume 2

A PL/SQL eljárás sikeresen befejeződött.
*/

Hozzárendelésnek hívjuk azt a tevékenységet, amikor egy PL/SQL változónak SQL utasításban adunk értéket. Egy kollekció minden elemének egyszerre történő hozzárendelését együttes hozzárendelésnek nevezzük. Az együttes hozzárendelés csökkenti a PL/SQL és SQL motorok közötti átváltások számát és így növeli a teljesítményt.

A PL/SQL oldali együttes hozzárendelés eszköze a FORALL utasítás, az SQL oldalié a BULK COLLECT utasításrész. A FORALL utasítás alakja:

FORALL index IN {alsó_határ..felső_határ
  | INDICES OF kollekció
  [BETWEEN alsó_határ AND felső_határ]
  | VALUES OF indexkollekció_név}
[SAVE EXCEPTIONS] sql_utasítás;

Az index explicit módon nem deklarált változó, amelynek hatásköre a FORALL utasítás, és azon belül is csak kollekció indexeként használható fel. Kifejezésben nem szerepelhet és érték nem adható neki. Az alsó_határ és felső_határ numerikus értékű kifejezések, amelyek egyszer értékelődnek ki a FORALL végrehajtásának kezdetén, és értékük egész vagy egészre kerekítődik. Az egészeknek egy érvényes kollekcióindex-tartományt kell megadniuk.

Az INDICES OF utasításrész esetén az index a megadott kollekció elemeinek indexeit veszi fel. A BETWEEN segítségével ezt az indextartományt korlátozhatjuk le. Ha az indextartomány valamely indexe a kollekcióban nem létezik, akkor figyelmen kívül marad. Ez az utasításrész törölt elemeket tartalmazó beágyazott tábla vagy numerikus kulcsú asszociatív tömb esetén használható.

A VALUES OF utasításrész azt írja elő, hogy az index által felveendő értékeket egy, az indexkollekció_név által megnevezett kollekció tartalmazza. Ekkor a megadott indexkollekció tetszőleges (akár ismétlődő) indexeket tartalmazhat. Az indexkollekció beágyazott tábla, vagy numerikus kulcsú asszociatív tömb lehet, az elemek típusa pedig vagy PLS_INTEGER vagy BINARY_INTEGER. Ha az indexkollekció üres, akkor a FORALL nem fut le, és kivétel váltódik ki.

Az sql_utasítás egy olyan INSERT, DELETE vagy UPDATE utasítás, amely kollekcióelemeket hivatkozik WHERE, VALUES vagy SET utasításrészében. Összetett adattípust tartalmazó kollekciók esetén a ciklusváltozóval történő indexelés után a további alstruktúrákba történő hivatkozás nem megengedett, azaz nem hivatkozhatunk rekord elemek esetén a rekordelem mezőire, objektum elemeknél attribútumokra, kollekció elemeknél a beágyazott kollekciók egyes elemeire.

Az SQL motor az SQL utasítást a megadott indextartomány minden értéke mellett egyszer végrehajtja. Az adott indexű kollekcióelemeknek létezniük kell.

A FORALL utasítás csak szerveroldali programokban alkalmazható.

2. példa

CREATE OR REPLACE TYPE T_Id_lista IS TABLE OF NUMBER;
/

/* FORALL nélkül */
CREATE OR REPLACE PROCEDURE kolcsonzes_torol(
  p_Ugyfelek T_Id_lista,
  p_Konyvek T_Id_lista
) IS
/* Több kölcsönzésbejegyzést töröl, nem módosítja a
szabad példányok számát stb. */
BEGIN
  FOR i IN 1..p_Ugyfelek.COUNT LOOP
    DELETE FROM kolcsonzes
    WHERE kolcsonzo = p_Ugyfelek(i)
      AND konyv = p_Konyvek(i);
  END LOOP;
END kolcsonzes_torol;
/
show errors

/* FORALL használatával */
CREATE OR REPLACE PROCEDURE kolcsonzes_torol(
  p_Ugyfelek T_Id_lista,
   p_Konyvek T_Id_lista
) IS
/* Több kölcsönzésbejegyzést töröl, nem módosítja a
szabad példányok számát stb. */
BEGIN
  FORALL i IN 1..p_Ugyfelek.COUNT
    DELETE FROM kolcsonzes
    WHERE kolcsonzo = p_Ugyfelek(i)
      AND konyv = p_Konyvek(i);
END kolcsonzes_torol;
/
show errors

/*
Megjegyzés:
Szintaktikájukban alig van különbség, viszont
a FORALL esetében csak egy hozzárendelés van, FORALL
nélkül annyi, ahányszor lefut a ciklusmag.
*/

3. példa

INDICES OF és VALUES OF használatára

CREATE TABLE t1 AS
SELECT ROWNUM id, LPAD('x', 10) adat FROM dual CONNECT BY LEVEL <= 4;
/

SELECT * FROM t1;

/*
ID ADAT
--- ----
  1   x
  2   x
  3   x
  4   x
*/

/* FORALL - INDICES OF */
DECLARE
  TYPE T_Id_lista IS TABLE OF NUMBER;
  v_Id_lista T_Id_lista := T_Id_lista(3,1);
BEGIN
  -- Törüljük az 1. elemet, 1 elem marad
  v_Id_lista.DELETE(1);
  FORALL i IN INDICES OF v_Id_lista -- i: {2}
  UPDATE t1
  SET adat = 'INDICES OF'
  WHERE id = v_Id_lista(i); -- T_Id_lista(2) = 1 -> 1-es Id-re fut le az UPDATE
END;
/

SELECT * FROM t1;

/*
ID ADAT
--- ----------
  1 INDICES OF
  2          x
  3          x
  4          x
*/

/* FORALL - VALUES OF */
DECLARE
  TYPE T_Id_lista IS TABLE OF NUMBER;
  TYPE T_Index_lista IS TABLE OF BINARY_INTEGER;
  v_Id_lista T_Id_lista := T_Id_lista(5,1,4,3,2);
  v_Index_lista T_Index_lista := T_Index_lista(4,5);
BEGIN
  FORALL i IN VALUES OF v_Index_lista -- i: {4, 5}
  UPDATE t1
  SET adat = 'VALUES OF'
  WHERE id = v_Id_lista(i);
  -- p_Id_lista(4) = 3
  -- p_Id_lista(5) = 2
  -- -> 3-as és 2-es Id-kre fut le az UPDATE
END;
/

SELECT * FROM t1;

/*
ID ADAT
--- ----------
  1 INDICES OF
  2 VALUES OF
  3 VALUES OF
  4          x
*/

DROP TABLE t1;

Ha egy FORALL utasításban az SQL utasítás nem kezelt kivételt vált ki, akkor az egész FORALL visszagörgetődik. Ha viszont a kiváltott kivételt kezeljük, akkor csak a kivételt okozó végrehajtás görgetődik vissza az SQL utasítás előtt elhelyezett implicit mentési pontig, a korábbi végrehajtások eredménye megmarad.

4. példa

CREATE OR REPLACE PROCEDURE kolcsonoz(
  p_Ugyfelek T_Id_lista,
  p_Konyvek T_Id_lista
) IS
/* Több könyv kölcsönzését végzi el.
  p_Ugyfelek és p_Konyvek mérete meg kell egyezzen. */
  v_Most DATE := SYSDATE;
  v_Tulkolcsonzes NUMBER;
BEGIN
  SAVEPOINT kezdet;

  /* Ha nem létezik az ügyfél vagy a könyv, akkor a ciklus
  magjában lesz egy kivétel. */
  FORALL i IN 1..p_Ugyfelek.COUNT
    INSERT INTO kolcsonzes VALUES
      (p_Ugyfelek(i), p_Konyvek(i), v_Most, 0,
  
  /* Ha elfogy valamelyik könyv, akkor lesz egy kivétel */
  FORALL i IN 1..p_Ugyfelek.COUNT
  UPDATE konyv SET szabad = szabad -1
  WHERE id = p_Konyvek(i);
  /* Az ügyfelek konyvek táblájának bővítése */
  FORALL i IN 1..p_Ugyfelek.COUNT
  INSERT INTO TABLE(SELECT konyvek FROM ugyfel WHERE id = p_Ugyfelek(i))
  VALUES (p_Konyvek(i), v_Most);
  /* Nézzük, lett-e túlkölcsönzés ? */
  SELECT COUNT(1) INTO v_Tulkolcsonzes
  FROM ugyfel
  WHERE max_konyv < (SELECT COUNT(1) FROM TABLE(konyvek));
  IF v_Tulkolcsonzes > 0 THEN
  RAISE_APPLICATION_ERROR(-20010,
  'Valaki túl sok könyvet akar kölcsönözni');
  END IF;
EXCEPTION
  WHEN OTHERS THEN
    ROLLBACK TO kezdet;
    RAISE;
END kolcsonoz;
/
show errors

SELECT COUNT(1) "Kölcsönzések" FROM kolcsonzes;

BEGIN
  kolcsonoz(T_Id_lista(10,10,15,15,15), T_Id_lista(35,35,25,20,10));
END;
/

BEGIN
  kolcsonoz(T_Id_lista(15), T_Id_lista(5));
END;
/

SELECT COUNT(1) "Kölcsönzések" FROM kolcsonzes;

/*
Kölcsönzések
------------
  14
Hiba a(z) 1. sorban:
BEGIN
*
ORA-02290: ellenőrző megszorítás (PLSQL.KONYV_SZABAD) megsértése
ORA-06512: a(z) "PLSQL.KOLCSONOZ", helyen a(z) 40. sornál
ORA-06512: a(z) helyen a(z) 2. sornál
BEGIN
Hiba a(z) 1. sorban:
*
ORA-20010: Valaki túl sok könyvet akar kölcsönözni
ORA-06512: a(z) "PLSQL.KOLCSONOZ", helyen a(z) 40. sornál
ORA-06512: a(z) helyen a(z) 2. sornál
Kölcsönzések
------------
14
*/

A DML utasítások végrehajtásához az SQL motor felépíti az implicit kurzort (lásd 8. fejezet). A FORALL utasításhoz kapcsolódóan a szokásos kurzorattribútumok (%FOUND, %ISOPEN, %NOTFOUND, %ROWCOUNT) mellett az implicit kurzornál használható a %BULK_ROWCOUNT attribútum is. Ezen attribútum szemantikája megegyezik egy asszociatív tömbével. Az attribútum i. eleme a DML utasítás i. futásánál feldolgozott sorok számát tartalmazza. Értéke 0, ha nem volt feldolgozott sor. Indexeléssel lehet rá hivatkozni. A %BULK_ROWCOUNT indextartománya megegyezik a FORALL indextartományával.

5. példa

CREATE OR REPLACE PROCEDURE ugyfel_visszahoz(p_Ugyfelek T_Id_lista) IS
/* Több ügyfél minden könyvének visszahozatalát adminisztrálja
a függvény. Kiírja, hogy ki hány könyvet hozott vissza. */
BEGIN
  /* Könyvek szabad példányainak adminisztrálása */
  FOR k IN (
    SELECT konyv, COUNT(1) peldany FROM kolcsonzes
    WHERE kolcsonzo IN (SELECT COLUMN_VALUE
                      FROM TABLE(CAST(p_Ugyfelek AS T_Id_lista)))
    GROUP BY konyv
  ) LOOP
    UPDATE konyv SET szabad = szabad + k.peldany
    WHERE id = k.konyv;
  END LOOP;

  /* Az ügyfelek konyvek tábláinak üresre állítása. */
  FORALL i IN 1..p_Ugyfelek.COUNT
    UPDATE ugyfel SET konyvek = T_Konyvek()
    WHERE id = p_Ugyfelek(i);

  /* A kölcsönzések törlése */
  FORALL i IN 1..p_Ugyfelek.COUNT
  DELETE FROM kolcsonzes WHERE kolcsonzo = p_Ugyfelek(i);


  /* A %BULK_ROWCOUNT segítségével jelentés készítése */
  FOR i IN 1..p_Ugyfelek.COUNT LOOP
  DBMS_OUTPUT.PUT_LINE('Ügyfél: ' || p_Ugyfelek(i)
    || ', visszahozott könyvek: ' || SQL%BULK_ROWCOUNT(i));
  END LOOP;
END ugyfel_visszahoz;
/
show errors

A FORALL utasítás SAVE EXCEPTIONS utasításrésze lehetőséget ad arra, hogy a FORALL működése közben kiváltódott kivételeket tároljuk, csak az utasítás végrehajtása után kezeljük őket. Az Oracle ehhez egy új kurzorattribútumot értelmez, amelynek neve %BULK_EXCEPTIONS. Ez rekordok asszociatív tömbje. A rekordoknak két mezőjük van. A %BULK_EXCEPTIONS(i). ERROR_INDEX a FORALL indexének azon értékét tartalmazza, amelynél a kivétel bekövetkezett, a %BULK_EXCEPTIONS(i). ERROR_CODE értéke pedig a megfelelő Oracle hibakód.

A %BULK_EXCEPTIONS mindig a legutoljára végrehajtott FORALL információit tartalmazza. Az eltárolt kivételek számát a %BULK_EXCEPTIONS.COUNT szolgáltatja, az indexek 1-től eddig mehetnek.

Ha a SAVE EXCEPTIONS utasításrészt nem adjuk meg, akkor egy kivétel bekövetkezte után a FORALL működése befejeződik. Ekkor a %BULK_EXCEPTIONS a bekövetkezett kivétel információit tartalmazza csak.

6. példa

CREATE OR REPLACE TYPE T_Id_lista IS
TABLE OF NUMBER;

CREATE OR REPLACE TYPE T_Szamok IS
TABLE OF NUMBER;
/

CREATE OR REPLACE FUNCTION selejtez(
  p_Konyvek T_Id_lista,
  p_Mennyit T_Szamok
) RETURN T_Id_lista IS
/* A könyvtárból selejtezi a megadott könyvekből
a megadott számú példányt. Visszaadja azoknak a könyveknek
az azonosítóit, amelyekből nem lehet ennyit selejtezni
(mert nincs annyi vagy kölcsönzik). Ha nem volt ilyen,
akkor üres kollekciót (nem NULL-t) ad vissza.
A paraméterek méretének meg kell egyeznie.
*/
  v_Konyvek T_Id_lista := T_Id_lista();
  v_Index PLS_INTEGER;
  bulk_kivetel EXCEPTION;
  PRAGMA EXCEPTION_INIT(bulk_kivetel, -24381);
BEGIN
  /* Amit tudunk módosítunk */
  FORALL i IN 1..p_Konyvek.COUNT SAVE EXCEPTIONS
  UPDATE konyv SET szabad = szabad - p_Mennyit(i),
  keszlet = keszlet - p_Mennyit(i)
  WHERE id = p_Konyvek(i);
  RETURN v_Konyvek;
EXCEPTION
  WHEN bulk_kivetel THEN
    /* A sikertelen módosítások összegyűjtése */
    FOR i IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP
      v_Index := SQL%BULK_EXCEPTIONS(i).ERROR_INDEX;
      v_Konyvek.EXTEND;
      v_Konyvek(v_Konyvek.LAST) := p_Konyvek(v_Index);
    END LOOP;
  RETURN v_Konyvek;
END selejtez;
/
show errors

DECLARE
  /* Kipróbáljuk */
  v_Konyvek T_Id_lista;
  v_Konyv konyv%ROWTYPE;
BEGIN
  DBMS_OUTPUT.NEW_LINE;
  DBMS_OUTPUT.PUT_LINE('Könyvek selejtezése: 5, 35');
  DBMS_OUTPUT.NEW_LINE;
  v_Konyvek := selejtez(T_Id_lista(5, 35), T_Szamok(1, 1));
  IF v_Konyvek.COUNT > 0 THEN
    DBMS_OUTPUT.PUT_LINE('Voltak hibák - nem mindent lehetett törölni');
    FOR i IN 1..v_Konyvek.COUNT LOOP
      SELECT * INTO v_Konyv FROM konyv WHERE id = v_Konyvek(i);
      DBMS_OUTPUT.PUT_LINE(v_Konyv.id || ', ' || v_Konyv.szabad
        || ', ' || v_Konyv.cim );
    END LOOP;
  END IF;
END;
/

/* Eredmény:
Könyvek selejtezése: 5, 35

Voltak hibák - nem mindent lehetett törölni
35, 0, A critical introduction to twentieth-century American drama - Volume 2

A PL/SQL eljárás sikeresen befejeződött.
*/

A SELECT, INSERT, DELETE, UPDATE, FETCH utasítások INTO utasításrészében használható a BULK_COLLECT előírás, amely az SQL motortól az együttes hozzárendelést kéri. Alakja:

BULK COLLECT INTO kollekciónév [,kollekciónév]…

A kollekciók elemtípusainak rendre meg kell egyezniük az eredmény oszlopainak típusaival. Több oszlop tartalmát egy megfelelő rekord elemtípusú kollekcióba is össze lehet gyűjteni.

7. példa

CREATE OR REPLACE FUNCTION aktiv_kolcsonzok
RETURN T_Id_lista IS
  v_Id_lista T_Id_lista; -- BULK COLLECT-nél nem kell inicializálni
BEGIN
  SELECT DISTINCT kolcsonzo
  BULK COLLECT INTO v_Id_lista
  FROM kolcsonzes
  ORDER BY kolcsonzo;
  RETURN v_Id_lista;
END aktiv_kolcsonzok;
/

SELECT * FROM TABLE(aktiv_kolcsonzok);

/*
COLUMN_VALUE
------------
          10
          15
          20
          25
          30
          35

6 sor kijelölve.
*/

DECLARE
/* Növeljük a kölcsönzött könyvek példányszámait 1-gyel.
A módosított könyvek azonosítóit összegyűjtjük. */
v_Konyvek T_Id_lista;
BEGIN
  UPDATE konyv k
  SET keszlet = keszlet + 1,
  szabad = szabad + 1
  WHERE EXISTS (SELECT 1 FROM kolcsonzes
  WHERE konyv = k.id)
  RETURNING id
  BULK COLLECT INTO v_Konyvek;
  DBMS_OUTPUT.PUT_LINE('count: ' || v_Konyvek.COUNT);
END;
/

/* Eredmény:
count: 10

A PL/SQL eljárás sikeresen befejeződött.
*/

A BULK COLLECT mind az implicit, mind az explicit kurzorok esetén használható. Az adatokat a kollekcióban az 1-es indextől kezdve helyezi el folyamatosan, felülírva az esetleges korábbi elemeket.

Az együttes hozzárendelést tartalmazó FETCH utasításnak lehet egy olyan utasításrésze, amely a betöltendő sorok számát korlátozza. Ennek alakja:

FETCH … BULK COLLECT INTO … LIMIT sorok;

ahol a sorok pozitív számértéket szolgáltató kifejezés. A kifejezés értéke egészre kerekítődik, ha szükséges. Ha értéke negatív, akkor az INVALID_NUMBER kivétel váltódik ki. Ezzel korlátozhatjuk a betöltendő sorok számát.

8. példa

DECLARE
  CURSOR cur_ugyfel_konyvek IS SELECT nev, konyvek FROM ugyfel;

  -- Kollekció rekord típusú elemekkel
  TYPE t_ugyfel_adatok IS
  VARRAY(4) OF cur_ugyfel_konyvek%ROWTYPE;
  v_Buffer t_ugyfel_adatok;
BEGIN
  OPEN cur_ugyfel_konyvek;
  LOOP
    FETCH cur_ugyfel_konyvek
    BULK COLLECT INTO v_Buffer
    LIMIT v_Buffer.LIMIT; -- Ennyi fér bele
    EXIT WHEN v_Buffer.COUNT = 0;
    FOR i IN 1..v_Buffer.COUNT LOOP
      DBMS_OUTPUT.NEW_LINE;
      DBMS_OUTPUT.PUT_LINE(v_Buffer(i).nev);
      FOR j IN 1..v_Buffer(i).konyvek.COUNT LOOP
        DBMS_OUTPUT.PUT_LINE(' '
          || LPAD(v_Buffer(i).konyvek(j).konyv_id, 3) || ' '
          || TO_CHAR(v_Buffer(i).konyvek(j).datum, 'YYYY-MON-DD'));
      END LOOP;
    END LOOP;
  END LOOP;
  CLOSE cur_ugyfel_konyvek;
END;
/

/* Eredmény:
Kovács János

Szabó Máté István
  30 2002-ÁPR. -21
  45 2002-ÁPR. -21
  50 2002-ÁPR. -21

József István
  15 2002-JAN. -22
  20 2002-JAN. -22
  25 2002-ÁPR. -10
  45 2002-ÁPR. -10
  50 2002-ÁPR. -10

Tóth László
  30 2002-FEBR. -24

Erdei Anita
  35 2002-ÁPR. -15

Komor Ágnes
   5 2002-ÁPR. -12
  10 2002-MÁRC. -12

Jaripekka Hämälainen
  35 2002-MÁRC. -18
  40 2002-MÁRC. -18

A PL/SQL eljárás sikeresen befejeződött.
*/

A BULK COLLECT csak szerveroldali programokban alkalmazható.

A FORALL utasítás tartalmazhat olyan INSERT, DELETE és UPDATE utasítást, amelynek van BULK COLLECT utasításrésze, de nem tartalmazhat ilyen SELECT utasítást. A FORALL működése közben a BULK COLLECT által visszaadott eredményeket az egyes iterációk a megelőző iteráció eredményei után fűzik (ezzel szemben ha egy FOR ciklusban szerepelne a BULK COLLECT-et tartalmazó utasítás, akkor az minden iterációban felülírná az előző eredményeket).

9. példa

/* Hasonlítsa össze az ugyfel_visszahoz eljárás
előzőleg megadott implementációját a mostanival. */
CREATE OR REPLACE PROCEDURE ugyfel_visszahoz(p_Ugyfelek T_Id_lista) IS
  /* Több ügyfél minden könyvének visszahozatalát adminisztrálja
  a függvény. Kiírja, hogy ki hány könyvet hozott vissza. */
  v_Konyvek T_Id_lista;
BEGIN
  /* A kölcsönzések törlése a törölt könyvek azonosítóinak
  összegyűjtése mellett, egy könyv azonosítója többször
  is szerepelhet a visszaadott kollekcióban. */
  FORALL i IN 1..p_Ugyfelek.COUNT
    DELETE FROM kolcsonzes WHERE kolcsonzo = p_Ugyfelek(i)
    RETURNING konyv BULK COLLECT INTO v_Konyvek;

  /* A %BULK_ROWCOUNT segítségével jelentés készítése */
  FOR i IN 1..p_Ugyfelek.COUNT LOOP
    DBMS_OUTPUT.PUT_LINE('Ügyfél: ' || p_Ugyfelek(i)
      || ', visszahozott könyvek: ' || SQL%BULK_ROWCOUNT(i));
  END LOOP;

  /* Könyvek szabad példányainak adminisztrálása */
  FORALL i IN 1..v_Konyvek.COUNT
    UPDATE konyv SET szabad = szabad + 1
    WHERE id = v_Konyvek(i);

  /* Az ügyfelek konyvek tábláinak üresre állítása. */
  FORALL i IN 1..p_Ugyfelek.COUNT
    UPDATE ugyfel SET konyvek = T_Konyvek()
    WHERE id = p_Ugyfelek(i);
END ugyfel_visszahoz;
/
show errors