8.    Tipuri de date compuse (II)

 

Acest capitol prezinta colectiile de tip TABLE si VARRAY.

 

Acestea se aseamana intre ele in urmatoarele privinte:

 

§        O colectie de acest fel poate fi stocata intr-o celula a unei tabele din baza de date (deci o tabela poate avea o coloana de tip colectie TABLE sau VARRAY). In cazul tablourilor asociative (TABLE .. INDEX BY) acest lucru nu era posibil.

§        Accesul la elemente se face prin indicele acestora care porneste de la 1.

§        In ambele cazuri variabilele de acest tip pot fi initializate la declarare si li se pot atribui valori in portiunea executabila folosind constructori de aceeasi forma.

§        Numarul de elemente dintr-o astfel de colectie poate varia pe parcursul executiei programului (dar in cazul VARRAY nu poate depasi limita maxima declarata)

§        Elementele unei astfel de colectii sunt toate de acelasi tip de date.

§        Limitarile privind tipul elementelor sunt aceleasi in cele doua cazuri.

 

 

Diferentele sunt urmatoarele:

 

§        Numarul maxim de elemente ale tipului TABLE nu este fixat la descrierea lui, spre deosebire de VARRAY unde se specifica aceasta limita.

§        Indicii elementelor unui VARRAY se pastreaza prin stocarea in baza de date si regasirea lui pe cand in cazul TABLE acest lucru nu este adevarat. Din acest motiv tipul TABLE seamana cu multimile din limbajele de programare Pascal si C spre deosebire de tipul VARRAY care este asemanator cu tablourile.

§        In cazul unui VARRAY indicii elementelor sunt succesivi, pornind de la indicele 1, crescator cu o unitate. La TABLE desi intitial indicii elementelor sunt succesivi, prin stergerea unor elemente pot sa apara 'goluri' (operatia de stergere 'din mijloc' nu este posibila la VARRAY).

§        Ca stocare fizica in baza de date, datele de tip VARRAY se memoreaza fie in interiorul tabelei (daca are mai putin de 4 KB) fie, daca e voluminoasa, in spatiul asociat tabelei (tablespace). In cazul TABLE stocarea valorilor se face intr-o asa numita 'tabela de stocare' (store table) care este un fel de tabela asociata tabelei de baza.

 

Limitarilor privind tipul elementelor unor date de acest fel sunt urmatoarele:

 

§        In cazul declararii lor in PL/SQL tipul poate fi oricare tip valid PL/SQL cu exceptia REF CURSOR

§        In cazul folosirii lor in SQL, pe langa REF CURSOR sunt interzise si tipurile:

o       BINARY_INTEGER, PLS_INTEGER

o       BOOLEAN

o       LONG, LONG RAW

o       NATURAL, NATURALN

o       POSITIVE, POSITIVEN

o       REF CURSOR

o       SIGNTYPE

o       STRING

 

8.1.          Sintaxa definitiei tipurilor TABLE si VARRAY

 

TABLE:

 

TYPE nume_tip IS TABLE OF tip_element [NOT NULL];

 

VARRAY:

 

TYPE nume_tip IS {VARRAY | VARYING ARRAY} (indice_maxim)

                 OF tip_element [NOT NULL];

 

unde:

nume_tip - Numele tipului definit de acea declaratie

indice_maxim - Dimensiunea maxima pentru acel VARRAY

tip_element - tipul elementelor colectiei

NOT NULL  - specifica optional ca elementele nu pot fi nule

 

Exemple:

 

A.   Exemplu comun

 

TABLE: Declararea a doua colectii care pot contine un numar nedefinit de denumiri de cursuri la care sunt inscrisi doi studenti:

 

TYPE ListaCursuri IS TABLE OF VARCHAR2(30);

ListaMea ListaCursuri;

ListaLui ListaMea%Type;

 

VARRAY: Declararea a doua colectii care pot contine maxim 25 de denumiri de cursuri la care sunt inscrisi doi studenti:

 

TYPE ListaCursuri IS VARRAY(25) OF VARCHAR2(30);

ListaMea ListaCursuri;

ListaLui ListaMea%Type;

 

Se observa ca se poate folosi constructia %TYPE pentru a defini o variabila colectie ca avand aclasi tip cu alta variabila colectie.

 

B.   %TYPE si %ROWTYPE

 

Pentru tipul elementelor se pot folosi de asemenea constructiile %TYPE si %ROWTYPE:

TYPE NumeAng IS TABLE OF emp.ename%TYPE; -- col%TYPE

TYPE ListaAng IS TABLE OF emp%ROWTYPE; -- tabela%ROWTYPE

CURSOR c_dept IS SELECT * FROM dept; -- cursor%ROWTYPE

TYPE ListaDept IS VARRAY(20) OF c_dept%ROWTYPE;

 

In primul caz, NumeAng este tipul unor colectii de valori scalare de acelasi tip cu coloana ENAME din tabela EMP. In celelalte doua cazuri ListaAng si ListaDept sunt colectii avand ca elemente integistrari definite pe baza structurii tabelei emp respectiv a cursorului c_dept.

 

C.   Elemente de tip inregistrare

 

DECLARE

TYPE Student IS RECORD (

nume VARCHAR2(40),

varsta DATE);

TYPE grupa IS VARRAY(25) OF Student;

TYPE seria IS TABLE OF Student;

 

 

8.2.         Initializarea TABLE si VARRAY. Atribuiri.

 

A.   Initializarea la declarare

 

Initializarea unor colectii de acest tip se poate face la declarare prin constructia

:= nume_tip_colectie(lista_de_valori)

 

Exemple:

 

TYPE ListaCursuri IS TABLE OF VARCHAR2(30);

ListaMea ListaCursuri := ListaCursuri('Analiza', 

         'Algebra', 'Fizica', 'Limba engleza');

 

sau similar in cazul VARRAY:

 

TYPE ListaCursuri IS VARRAY(25) OF VARCHAR2(30);

ListaMea ListaCursuri := ListaCursuri('Analiza', 

         'Algebra', 'Fizica', 'Limba engleza');

 

In cazul in care constructorul a fost apelat fara valori intre paranteze se genereaza o colectie vida dar nenula:

 

DECLARE

TYPE ListaCursuri IS VARRAY(25) OF VARCHAR2(30);

ListaMea ListaCursuri := ListaCursuri();

BEGIN

  IF ListaMea IS NOT NULL THEN

   .  .  .  -- conditia de mai sus returneaza TRUE!

 

In schimb daca o colectie nu este initializata testul de NOT NULL returneaza FALSE (deci IS NULL returneaza TRUE):

 

DECLARE

TYPE ListaCursuri IS VARRAY(25) OF VARCHAR2(30);

ListaMea ListaCursuri;

BEGIN

  IF ListaMea IS NOT NULL THEN

   .  .  .  -- conditia de mai sus returneaza FALSE!

 

 

B.   Atribuirea

 

Se poate lasa neinitializata colectia la declarare si initializa in zona executabila. Exemplul urmator este doar pentru TABLE dar similar se procedeaza pentru VARRAY:

 

TYPE ListaCursuri IS TABLE OF VARCHAR2(30);

ListaMea ListaCursuri;

BEGIN

  ListaMea := ListaCursuri('Analiza', 

         'Algebra', 'Fizica', 'Limba engleza');

 

Lista de valori poate contine valori nule:

 

TYPE ListaCursuri IS VARRAY(25) OF VARCHAR2(30);

ListaMea ListaCursuri;

BEGIN

  ListaMea := ListaCursuri('Analiza', NULL, NULL, 

         'Algebra', NULL, 'Fizica', 'Limba engleza');

 

ultimele doua exemple au fost asignate colectii continand 4 respectiv 7 elemente (au fost numarate si valorile NULL)

 

 

C.   Atribuirea intre doua colectii

 

Doua colectii de acelasi tip se pot atribui una alteia. Daca insa tipul difera rezulta o exceptie.

 

DECLARE

TYPE ListaCursuri IS TABLE OF VARCHAR2(30);

TYPE AltTip IS TABLE OF VARCHAR2(30);

ListaMea ListaCursuri;

ListaLui ListaMea%Type;

ListaEi  AltTip;

BEGIN

  ListaLui := ListaCursuri('Analiza', NULL, NULL,

         'Algebra', NULL, 'Fizica', 'Limba engleza');

  ListaMea := ListaLui;

  ListaEi := ListaMea;

  dbms_output.put_line('Numar de elemente: '|| ListaMea.count); -- 7

end;

 

In exemplul de mai sus atribuirea pentru variabila ListaEi genereaza exceptie desi ambele variabile sunt TABLE OF VARCHAR2(30) dar tipul lor are alt nume (desi descrierile sunt identice).

In schimb atribuirea pentru ListaMea este valida, ambele variabile fiind de acelasi tip.

 

D.   Compararea colectiilor

 

O colectie poate fi testata daca este nula sau nu cu IS NULL si IS NOT NULL. In schimb nu putem folosi = sau <> pentru testa egalitatea sau inegalitatea a doua colectii.

Din acest motiv o coloana care este de tip colectie nu poate apare in cereri SQL in conjunctie cu clauzele DISTINCT, GROUP BY sau ORDER BY.

 

8.3.         Metode pentru TABLE si VARRAY

 

In maniputatea datelor de tip TABLE si VARRAY putem folosi metodele mentionate anterior.

 

Iata cateva exemple:

 

TABLE:

 

DECLARE

TYPE ListaCursuri IS TABLE OF VARCHAR2(30);

ListaMea ListaCursuri;

BEGIN

  ListaMea := ListaCursuri('Analiza', NULL, NULL,

         'Algebra', NULL, 'Fizica', 'Limba engleza');

  dbms_output.put_line('Numar de elemente: '||

 ListaMea.count); -- 7

  ListaMea.Extend(3, 2);

  dbms_output.put_line('Numar de elemente: '||

 ListaMea.count); -- 10

  ListaMea.Delete(7);

  dbms_output.put_line('Numar de elemente: '||

 ListaMea.count); -- 9

  ListaMea.Delete(3, 6);

  dbms_output.put_line('Numar de elemente: '||

 ListaMea.count); -- 5

  ListaMea.Trim(2);

  dbms_output.put_line('Numar de elemente: '||

 ListaMea.count); -- 3

end;

 

VARRAY:

 

DECLARE

TYPE ListaCursuri IS VARRAY(25) OF VARCHAR2(30);

ListaMea ListaCursuri;

BEGIN

  ListaMea := ListaCursuri('Analiza', NULL, NULL,

         'Algebra', NULL, 'Fizica', 'Limba engleza');

  dbms_output.put_line('Numar de elemente: '||

 ListaMea.count); -- 7

  ListaMea.Extend(3, 2);

  dbms_output.put_line('Numar de elemente: '||

 ListaMea.count); -- 10

  ListaMea.Trim(2);

  dbms_output.put_line('Numar de elemente: '||

 ListaMea.count); -- 3

end;

/

 

Reamintire metode:

 

EXISTS(n)

TRUE daca elementul cu indicele n exista (este setat)

COUNT

Numarul de elemente din tablou.

LIMIT

Doar pentru VARRAY: numarul maxim de elemente permis. Pentru celelalte tipuri intoarce NULL.

FIRST si LAST

Primul, respectiv ultimul element (se refera la valorile cheii - tablouri asociative - sau indicelui).

PRIOR(n) si NEXT(n)

Indexul care precede respectiv succede in tablou elementul cu indice n. Daca nu exista un astfel de element intoarce NULL.

EXTEND[(n[,i)]]

Extinde tabloul cu un element nul, n elemente nule sau n elemente egale cu elementul de indice i - nu se aplica la tablouri asociative

TRIM[(n)]

Sterge un element, respectiv n elemente de la sfarsitul tabloului - nu se aplica la tablouri asociative

DELETE[(n[,k)]]

DELETE: sterge toate elementele tabloului.

DELETE(n) sterge elementul n

DELETE(n, k) sterge toate elementele din plaja n . . k

Nu se aplica la VARRAY. Se foloseste TRIM.

 

De asemenea metodele FIRST, LAST, NEXT si PRIOR ne permit parcurgerea unei variabile de tip colectie. In unele exemple de mai jos am folosit colectii TABLE cu elemente sterse.

 

Exemplul 1: Parcurgerea unei colectii cu FOR. Acest bloc ridica exceptie daca exista elemente sterse.

 

DECLARE

TYPE ListaCursuri IS TABLE OF VARCHAR2(30);

ListaMea ListaCursuri;

i NUMBER;

BEGIN

  ListaMea := ListaCursuri('Analiza', 'Algebra', 'Sport',

 'Fizica', 'Limba engleza', 'Programare');

  FOR i IN ListaMea.FIRST .. ListaMea.LAST LOOP

    dbms_output.put_line('Curs: '||i||' '||ListaMea(i));

  END LOOP;

END;

 

Rezultat:

Curs: 1 Analiza

Curs: 2 Algebra

Curs: 3 Sport

Curs: 4 Fizica

Curs: 5 Limba engleza

Curs: 6 Programare

 

 

Exemplul 2: Parcurgere folosind NEXT pentru a evita exceptia anterioara.

 

DECLARE

TYPE ListaCursuri IS TABLE OF VARCHAR2(30);

ListaMea ListaCursuri;

i NUMBER;

BEGIN

  ListaMea := ListaCursuri('Analiza', 'Algebra', 'Sport',

 'Fizica', 'Limba engleza', 'Programare');

  ListaMea.Delete(3,4);

  i := ListaMea.FIRST;

  LOOP

    dbms_output.put_line('Curs: '||i||' '||ListaMea(i));

    i := ListaMea.NEXT(i);

    EXIT WHEN i IS NULL;

  END LOOP;

END;

 

Rezultat:

 

Curs: 1 Analiza

Curs: 2 Algebra

Curs: 5 Limba engleza

Curs: 6 Programare

 

 

8.4.         Folosirea datelor tip TABLE si VARRAY in SQL

 

In acest caz trebuie creat un tip corespunzator folosind CREATE TYPE. De exemplu daca se doreste ca o coloana a unei tabele de studenti sa fie lista cursurilor la care sunt inscrisi (o lista de denumiri de cursuri) putem scrie cererea de creare a tabelei astfel, folosind TABLE:

 

CREATE TYPE ListaCursuri AS TABLE OF VARCHAR2(30)

/

CREATE TABLE Student (

id_student INTEGER(4),

nume VARCHAR2(25),

adresa VARCHAR2(35),

cursuri ListaCursuri)   -- Coloana de tip TABLE

NESTED TABLE cursuri STORE AS cursuri_tab;

/

 

Asa cum am specificat, coloanele de tip TABLE se stocheaza intr-o tabela separata asociata tabelei de baza. In cererea de creare anterioara clauza NESTED TABLE defineste numele acestei tabele.

 

Daca numarul de cursuri este maxim 25 putem folosi VARRAY:

 

CREATE TYPE ListaCursuri AS VARRAY(25) OF VARCHAR2(30)

/

CREATE TABLE Student (

id_student INTEGER(4),

nume VARCHAR2(25),

adresa VARCHAR2(35),

cursuri ListaCursuri);   -- Coloana de tip TABLE

/

 

Si in acest caz putem folosi constructori in cereri SQL:

 

INSERT INTO Student VALUES(1234, 'Ionescu', 'Bucuresti',

           ListaCursuri('Analiza', NULL, NULL, 

                'Algebra', NULL, 'Fizica', 'Limba engleza'));

 

In cazul unei cereri SELECT se folosesc variabile de tip colectie pentru a regasi datele. De exemplu, pentru regasirea colectiei adaugate in baza de date prin cererea INSERT instructiunile sunt:

 

DECLARE

  o_lista ListaCursuri;

BEGIN

SELECT cursuri INTO o_lista FROM Student

WHERE id_student = 1234;

.  .  .

END;

 

 

Bineinteles ca in locul constructorilor putem folosi o variabila de tip colectie compatibila cu definitia coloanei respective si initializata corespunzator.

 

Exemplu:

 

DECLARE

o_lista ListaCursuri := ListaCursuri('Filosofie',

           'Compozitie', 'Limba italiana', 'Canto');

BEGIN

     UPDATE Student SET cursuri = o_lista

     WHERE id_student=1234;

.  .  .

END;

 

8.5.         Manipularea elementelor colectiei cu SQL

 

 

TABLE:

 

In cazul in care se doreste actualizarea directa a unei colectii stocate in baza de date se foloseste operatorul TABLE (subcerere) care returneaza o colectie de tip TABLE. Cererea principala se va aplica colectiei si nu liniei din tabela.

 

Inserare. Exemplu:

 

BEGIN

INSERT INTO

TABLE (SELECT cursuri

  FROM Student

  WHERE id_student = 1234)

VALUES('Limba spaniola');

END;

 

Actualizare: Exemplu (in exemplele urmatoare se lucreaza cu o colectie de obiecte):

BEGIN

     UPDATE

TABLE (SELECT cursuri

  FROM Student

  WHERE id_student = 1234)

SET credite = credite + 2

WHERE cod_curs IN (123, 324);

END;

 

Stergere. Exemplu:

BEGIN

     DELETE

TABLE (SELECT cursuri

  FROM Student

  WHERE id_student = 1234)

WHERE credite = 5;

END;

 

Regasire. Exemplu:

BEGIN

     SELECT Denumire INTO v_denumire FROM

TABLE (SELECT cursuri

  FROM Student

  WHERE id_student = 1234)

WHERE cod_curs = 123;

END;

 

VARRAY:

 

In Oracle9i nu este permisa actualizarea directa a unei colectii de tip VARRAY stocata in baza de date. Regasirea se face insa folosind ca in exemplul anterior operatorul TABLE (subcerere).

 

Pentru operatii de tip INSERT, UPDATE si DELETE la nivel de element al unei colectii de tip VARRAY din baza de date:

§        se regaseste colectia intr-o variabila PL/SQL,

§        se actualizeaza variabila si

§        se reintroduce in baza de date cu o cerere UPDATE ca la 8.4

 

8.6.         Clauza BULK_COLLECT

 

In cazul in care o cerere SELECT intoarce mai multe linii fiecare coloana a rezultatului se poate incarca intr-o colectie folosind BULK COLLECT INTO in loc de INTO. Iata un exemplu:

 

DECLARE

TYPE TabCod IS TABLE OF emp.empno%TYPE;

TYPE TabNume IS TABLE OF emp.ename%TYPE;

coduri TabCod;

nume TabNume;

BEGIN

SELECT empno, ename BULK COLLECT INTO coduri, nume

FROM emp;

       dbms_output.put_line('Nr.Coduri=' ||

       coduri.count);

END;

 

BULK COLLECT se poate folosi si la FETCH pentru a incarca dintr-un cursor intreg rezultatul in tot atatea colectii cate coloane are acesta.

 

Exemplu:

 

 

DECLARE

TYPE TabCod IS TABLE OF emp.empno%TYPE;

TYPE TabNume IS TABLE OF emp.ename%TYPE;

coduri TabCod;

nume TabNume;

CURSOR c IS SELECT empno, ename FROM emp;

BEGIN

OPEN c;

FETCH c BULK COLLECT INTO coduri, nume;

       dbms_output.put_line('Nr.Coduri=' ||

       coduri.count);

END;

 

In acest caz se poate limita numarul maxim de linii incarcate din cursor folosind clauza LIMIT expresie_intreaga. De exemplu, daca in programul de mai sus dorim sa incarcam maxim 5 angajati instructiunea FETCH va fi:

 

FETCH c BULK COLLECT INTO coduri, nume LIMIT 5;

 

Daca instructiunea de mai sus e plasata intr-un ciclu la fiecare pas se vor incarca cate 5 linii - in afara de ultima incarcare care poate aduce mai putin de 5 linii. Dupa ce s-au incarcat toate liniile c%NOTFOUND devine TRUE, ca si mai inainte.

 

8.7.         Exceptii ridicate pentru colectii

 

In cazul in care manipularea unei colectii este defectuoasa se pot ridica o serie de exceptii.

 

Exemple:

 

DECLARE

TYPE Lista IS TABLE OF NUMBER;

numere Lista; -- neinitializata deci nula

BEGIN

numere(1) := 1; -- Exceptie COLLECTION_IS_NULL

numere := Lista(1,2); -- initializam variabila

numere(NULL) := 3 -- Exceptie VALUE_ERROR

numere(0) := 3; -- Exceptie SUBSCRIPT_OUTSIDE_LIMIT

numere(3) := 3; -- Exceptie SUBSCRIPT_BEYOND_COUNT

numere.DELETE(1); -- stergem elementul 1

IF numere(1) = 1 THEN ... -- Exceptie NO_DATA_FOUND

.  .  .

 

8.8.         Instructiunea FORALL

 

Sintaxa acestei instructiuni este:

 

FORALL index IN val_minima .. val_maxima

  cerere_sql;

 

unde:

§        index - o variabila care nu trebuie declarata si care are ca domeniu de valabilitate doar cererea SQL. Acest index este indicele care identifica elementele unei colectii.

§        val_minima, val_maxima - arata pentru care indici ai colectiei se aplica cererea SQL

§        cerere_sql - o cerere SQL in care apare o colectie indexata dupa indicele index.

 

Instructiunea FORALL nu este o instructiune de ciclare, in sensul ca nu se face un dialog intre PL/SQL si serverul Oracle pentru fiecare valoare a indexului.

Ea se foloseste nu ca o parcurgere ci ca o indicare a plajei de valori implicata in cererea SQL.

 

Exemplu:

 

DECLARE

TYPE Lista IS VARRAY(10) OF NUMBER;

sectii Lista := Lista(20,30,50,55,57,60,70,75,90,92);

BEGIN

FORALL i IN 3..8

  UPDATE emp SET sal = sal * 1.10 WHERE deptno = sectii(i);

END;

 

Efectul in acest caz este acelasi cu al executiei cererii

  UPDATE emp

  SET sal = sal * 1.10

  WHERE deptno in (50,55,57,60,70,75);

 

%BULK_ROWCOUNT

 

In cazul folosirii lui FORALL programul poate afla cate linii au fost afectate la fiecare pas folosindu-se atributul %BULK_ROWCOUNT a cursorului implicit SQL. Aceasta este compus din mai multe valori, cate una pentru fiecare valoare a indicelui. De exemplu, pentru programul de mai sus plaja este 3..8.

 

Exemplu:

 

DECLARE

TYPE Lista IS TABLE OF NUMBER;

sectii Lista := Lista(20,30,50,55,57,60,70,75,90,92);

BEGIN

FORALL i IN 3..8

       UPDATE emp SET sal = sal * 1.10

  WHERE deptno = sectii(i);

dbms_output.put_line('Pasul 2, afectate '||

       SQL%BULK_ROWCOUNT(4)||' linii');

END;

 

Exceptii

 

Toate elementele colectiei referite de index trebuie sa existe. Programul urmator ridica o exceptie:

 

DECLARE

TYPE Lista IS TABLE OF NUMBER;

sectii Lista := Lista(20,30,50,55,57,60,70,75,90,92);

BEGIN

Lista.Delete(5);  -- stergem elementul cu indice 5

FORALL i IN 3..8  -- plaja cuprinde si elementul sters

  UPDATE emp SET sal = sal * 1.10 WHERE deptno = sectii(i);

EXCEPTION

  WHEN OTHERS THEN COMMIT;

END;

§        Daca o exceptie aparuta in FORALL nu este tratata toate modificarile facute sunt anulate (ROLLBACK inclusiv pentru executiile i=3 si i=4).

§        Daca insa, ca in exemplul de mai sus, erorile sunt tratate, actualizarile vor fi comise pentru valorile 3 si 4 ale lui i. La valoarea 5 se ridica exceptia si cererea UPDATE nu se mai executa pentru valorile urmatoare (6, 7 si 8). De asemenea se face implicit un ROLLBACK pentru cererea care a ridicat exceptia (UPDATE pentru valoare 5 a lui i).

 

SAVE EXCEPTIONS

 

Se poate folosi de asemenea clauza SAVE EXCEPTIONS care specifica faptul ca orice exceptie ridicata este salvata in atributul %BULK_EXCEPTIONS atasat cursorului SQL care contine o colectie de inregistrari cu doua campuri fiecare, ERROR_INDEX si ERROR_CODE iar executia merge mai departe.

Saltul in zona de exceptii se face abia dupa terminarea iteratiilor. Aici putem folosi:

 

§        SQL%BULK_EXCEPTIONS(i).ERROR_INDEX - indexul FORALL la care a aparut exceptia

§        SQL%BULK_EXCEPTIONS(i).ERROR_CODE - codul erorii respective

§        SQL%BULK_EXCEPTIONS.COUNT - cate exceptii au aparut

 

Observatie: indicele i de mai sus poate lua valori cuprinse intre 1 si valoarea lui SQL%BULK_EXCEPTIONS.COUNT. Acest fapt poate fi folosit pentru a raporta exceptiile aparute folosind un ciclu FOR, ca in exemplul urmator.

 

In acest caz sintaxa unei instructiuni FORALL este:

 

FORALL index IN val_minima .. val_maxima SAVE EXCEPTIONS

  {cerere_insert | cerere_update | cerere_delete}

 

Exemplu:

 

DECLARE

  TYPE Lista IS TABLE OF NUMBER;

  numere Lista := Lista(10,0,11,12,30,0,20,199,2,0,9,1);

  erori NUMBER;

BEGIN

  FORALL i IN numere.FIRST..numere.LAST SAVE EXCEPTIONS

DELETE FROM emp WHERE sal > 500000/numere(i);

EXCEPTION

  erori := SQL%BULK_EXCEPTIONS.COUNT;

  dbms_output.put_line('Nr.de erori= ' || erori);

  FOR i IN 1..erori LOOP

    dbms_output.put_line('Eroarea ' || i ||

       ' aparuta la iteratia ' ||

       SQL%BULK_EXCEPTIONS(i).ERROR_INDEX);

    dbms_output.put_line('Eroarea Oracle este ' ||

       SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE));

  END LOOP;

END;