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
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;
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.
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
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;
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
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.
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
. . .
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;
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).
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;