Sunteți pe pagina 1din 57

Capitolul 7. Surogate, reguli, incluziuni.

Un
caz practic. Implementri SQL/Oracle
Cu atuurile i limitele ei, normalizarea reprezint un excelent ghid pe care l-am
folosit pentru a obine o schem acceptabil a bazei de date. Pentru ceea ce
urmeaz ncercm s legm normalizarea de alte elemente structurale ale bazei,
cum ar fi identificarea mai simpl a entitilor i restriciile complexe ale datelor
din baz. De asemenea, acest capitol ncearc s demonstreze c, n multe cazuri,
n normalizare, pe lng dependenele funcionale, multivaloare i de jonciune,
eseniale sunt i cele ce fac parte dintr-o specie destul de neglijat - dependenele
de incluziune.
De asemena, vom ncepe s prezentm i modaliti practice de declara-
re/implementare a diferitot categorii de restricii discutate, iar cazul practic de la
finalul capitolui ncearc s lege elementele teoretice presrate n primele ase
capitole, s ilustreze cteva dintre frmntri, dac nu chiar convulsii, care
nsoesc procesul de elaborare a schemei baze de date pentru o aplicaie real,
chiar n condiiile n care datele problemei nu sunt complicate din cale-afar.

7.1. Chei surogat


Folosind limbajul de lemn al tehnologiei informaionale, putem spune c bazele
de date stocheaz persistent i gestioneaz varii informaii privitoare la fenomene,
procese, tranzacii, persoane etc. din mai toate domeniile de activitate, n funcie de
aplicaiile pentru care sunt proiectate i implementate. Una din problemele de
cpti n lucrul cu aceste entiti, procese, tranzacii etc. ine de posibilitatea
identificrii lor fr ambiguitate.

7.1.1. Nevoia de surogate

nchipuii-v ce-ar nsemna ca firma noastr s vireze cteva sute de milioane


de lei n contul unui client, pltind astfel o factur, iar contul respectiv s fie
confundat cu un altul, al altui client. Sau ce s-ar ntmpla dac la plata "pe card" a
salariilor pltite s-ar produce niscaiva confuzii: cei mai oropsii s-ar oripila vznd
ct ctig efii (confirmnd luminoasa tez "impozita-i-ar naiba !" a unor
preedini retardai istoric i economic), iar efii ar fi cel puin la fel de oripilai,
dac nu chiar ngrozii de mnia popular/proletar.
Aa c vedem n jurul nostru tot soiul de "ingrediente" menite a face diferenieri
sau, altfel spus, a identifica fiecare persoan, carte, factur, plat etc. Privind
retrospectiv, n relaia STUDENI_EXAMENE - figura 2.1, paragraful 2.1 - fiecare
student este identificat de matricolul su, iar disciplinele au un cod (CodDisc) unic
2 Capitolul 7

prin care evitm confuzia ce s-ar putea genera prin folosirea exclusiv a denumirii
disciplinei (respectiv, numelui studentului). De asemenea, valoarea atributului
NrFact este unic pentru orice factur emis de firma noastr i trimis unui
client (baza de date VNZRI). ISBN-ul este un cod unic asociat, la nivel mondial,
unei cri tiprite, n timp ce cota identific ntr-o bibliotec un exemplar al unei
cri. Codul numeric personal (CNP-ul) este cel mai bun mod de a identifica o
persoan, n timp ce codul fiscal este un element de ncredere cnd ne propunem s
identificm un client.
Din contr, n paragraful 3.4, exasperai de problemele ridicate de nulitatea
atributelor cheie din relaia CODURI_NOI_V2 (figura 3.18), am apelat la o soluie
"gordian" introducnd un atribut cu oarecare doz de artificialitate, Id, care ar
identifica fiecare grup de adrese atribuite unui cod potal (figura 3.20, relaia CO-
DURI_NOI_V3). Atributul Id este un exemplu de cheie surogat, adic un cmp
oarecum artificial de care vorbeam. Dealtminteri, noiunea de surogat desemneaz
un produs artificial sau sintetic care este utilizat drept substitut pentru un produs
natural. O cheie surogat este o cheie artificial sau sintetic utilizat ca substitut al
unei chei "naturale".
Pe parcursul precedentelor capitole au mai fost strecurate chei surogat precum:
IdFilm (baza de date FILMOGRAFIE), CodFamilie (relaia DOTA-
RE_JUCRII), CodCl, CodPr, Codnc i CodDoc (baza de date VNZRI),
CodProf (baza de date EXAMENE - paragraful 5.5.2) sau IdProf (relaia
PROF_DISCIPLINE din paragraful 5.5.3).
De cele mai multe ori, cheile surogat simplific graful dependenelor
funcionale i, implicit, sarcina demarcrii relaiilor. Astfel, revenind la relaia
LINII_ARTICOLE_CONTABILE din figura 4.1, tiind c o not contabil are mai
multe operaiuni care, la rndul lor, conin mai mai multe corespondene
ContDebitor - ContCreditor, introducem dou atribute care s preia
explicaiile/comentariile despre note, respectiv, operaiuni - ExplicaiiNot i
ExplicaiiOp. Din paragraful 4.1.2 tim c, n funcie de modul de contare,
configuraia dependenelor poate fi sensibil diferit. Pentru uniformizarea celor
dou variante de numerotare a notelor contabile, putem recurge la dou atribute
de tip cheie surogat. IdNotContabil va fi un numr unic asociat fiecrei note
contabile, iar IdOperaiune va identifica orice operaiune, indiferent de nota
contabil din care face parte. Graful ia configuraia din figura 7.1.

Figura 7.1. Dou chei surogat pentru LINII_ARTICOLE_CONTABILE


Proiectarea bazelor de date 3

Schema obinut prin decuparea dependenelor din graf este:

NOTE_CONTABILE {IdNotContabil, NrNot, Data, ExplicaiiNot}

OPERAIUNI {IdOperaiune, NrOp, IdNotContabil, ExplicaiiOp}

DETALII_OPERAIUNI {IdOperaiune, ContDebitor, ContCreditor, Suma}

Un alt caz n care folosirea cheii surogat modific sensibil lucrurile este cel al
relaiei DOTARE_JUCRII_1 reprezentat n figura 3.14 (finalul paragrafului
3.3.3), despre care ultima dat am discutat n paragraful 6.2 - vezi figura 6.13. n
condiiile date la acel moment, aceast relaie necesit trei atribute care s confere
unicitate fiecrui tuplu - (CodFamilie, Copil, Jucrie). Introducem un atribut
suplimentar, IdJucrie, nimic altceva dect o cheie surogat prin care difereniem
jucriile ntre ele. Aceasta ar fi a doua cheie surogat din relaie, dup CodFamilie.
Corobornd dependenele funcionale din paragraful 4.1.2 cu dependenele multi-
valoare din paragraful 6.1 i discuia din 6.2, n condiiile n care, ntr-o familie,
orice jucrie este cumprat n N exemplare, unde N reprezint numrul copiilor
din acea familie, grafurile sunt cele din figura 7.2, diferenierea fiind fcut de
momentul cumprrii celor N "instane" ale jucriei ntr-o familie.

Figura 7.2. Grafurile dependenelor pentru relaia DOTARE_JUCRII "dotat" cu dou


chei surogat
ncepem cu graful din strnga, cel care reflect achiziionarea simultan a cte
un exemplar pentru fiecare copil dintr-o familie. Dependena funcional dintre
IdJucrie i DataCumprrii este una tranzitiv, aa c s-a recurs la
"punctarea" sa, pentru a nu fi luat n considerare. Din dependenele funcionale,
am obine relaiile:

FAMILII {CodFamilie, NumeFamilie},


4 Capitolul 7

JUCRII_COPII {IdJucrie, Jucrie, CodFamilie, Copil}


i
JUCRII_FAMILII {CodFamilie, Jucrie, DataCumprrii}.

Dac inem cont de dependenele multivaloare, s-ar mai aduga acestor relaii
i:

COPII_FAMILII {CodFamilie, Copil}


i
JUCRII_FAMILII2 {CodFamilie, Jucrie }.

Ambele sunt redundante, ns n timp ce COPII_FAMILII se constituie ca un soi


de nomenclator al copiilor dintr-o familie, permind preluarea n baza de date i a
copiilor din familiile fr nici o jucrie nregistrat (aa cum observam la finalul pa-
ragrafului 6.2), JUCRII_FAMILII2 nu se justific cu nici un pre, toate informaiile
furnizate de aceasta gsindu-se n relaia JUCRII_COPII.
Continum cu partea dreapt a grafului din figura 7.2. Faptul c exemplare
diferite din aceeai jucrie pot fi cumprate la momente diferite, chiar dac se
preiau simultan, ne scutete de dependena funcional cu sursa compus vizibil
pe graful din stnga. De asemenea, dependena funcional dintre IdJucrie i
DataCumprrii nu mai este tranzitiv. Decuparea DF genereaz relaiile:

FAMILII {CodFamilie, NumeFamilie}


i
JUCRII {IdJucrie, Jucrie, CodFamilie, Copil, DataCumprrii}.

Dac inem cont de DMV, am mai obine relaiile COPII_FAMILII {CodFami-


lie, Copil} i JUCRII_FAMILII2 {CodFamilie, Jucrie}. Din raiunile expuse
mai sus, ar fi bine s o pstrm doar pe prima i s renunm la cea din urm.
Din moment ce am introdus un atribut pentru identificarea fiecrui exemplar
dintr-o jucrie, pare ct se poate de normal s folosim o asemenea cheie surogat
pentru identificarea fiecrui copil din baza de date. La drept vorbind, acest atribut
exist, i nu e ctui de puin surogat - CNPCopil. Noi, ns, fideli tematicii acestui
paragraf, ne vom preface c nu avem de unde s tim CNP-urile copiilor, aa c
vom lucra cu IdCopil.
Noul rnd de grafuri ale dependenelor capt forma din figura 7.3. Motivul
principal pentru care ne ocupm de acest caz este apariia n ambele grafuri a unei
simetrii ciudate ntre o dependen funcional i una multivaloare. Astfel, n
graful din partea stng, IdCopil CodFamilie, dar i CodFamilie
IdCopil. Simetria ce apare n graful din dreapta se manifest ntre aceleai
atribute.
Proiectarea bazelor de date 5

Figura 7.3. Grafurile dependenelor pentru relaia DOTARE_JUCRII "dotat" cu trei


chei surogat
Nu este prima dat n aceast carte cnd ridicm din umeri i ne ntrebm ce-i
de fcut. Primul impuls este de a elimina una dintre dependene. Cel mai piezi ne
uitm spre cea multivaloare, ns trebuie s ne temperm elanul atta vreme ct o
dependen multivaloare se stabilete ntre dou atribute, dar numai n prezena
unui al treilea: CodFamilie IdCopil|Jucrie. Ne va fi greu s
justificm prezena n graf a DMV CodFamilie Jucrie, atta vreme ct
CodFamilie IdCopil este eliminat din calcul. Aa c, deocamdat nu
renunm la nimic din simetrie...
Ocupndu-ne de graful din stnga imaginii, observm c acum dou dintre
dependene sunt punctate, pe motiv de tranzitivitate. Pe baza DF obinem relaiile:

FAMILII {CodFamilie, NumeFamilie}

COPII {IdCopil, Copil, CodFamilie}

CUMPRRI_JUCRII {CodFamilie, Jucrie, DataCumprrii}.


i
JUCRII_COPII {IdJucrie, Jucrie, IdCopil}.

Din DMV, s-ar mai aduga:


COPII_FAMILII {CodFamilie, IdCopil}
i
JUCRII_FAMILII2 {CodFamilie, Jucrie}.

Informaia furnizat de COPII_FAMILII, copiii unei familii, este furnizat de


COPII, iar la JUCRII_FAMILII2 putem renuna fr remucri, atta vreme ct
exist CUMPRRI_JUCRII. Iat cum am ajuns ca DMV s nu aib nici o
6 Capitolul 7

inciden direct n schema final a bazei de date ! Graful din partea dreapt a
figurii rmne drept tem pentru acas.

7.1.2. Puncte de vedere privind cheile surogat

Prerile despre cheile surogat sunt, firete, mprite. Exist autori care fac
distincia ntre chei surogat definite/controlate de utilizatori i chei surogat
gestionate exclusiv de sistem (SGBD). Pentru Codd, cheile primare definite i
controlate de utilizator folosite ca surogate permanente pentru entiti, ar prezenta
trei dificulti la ntrebuinare1:
Valorile cheilor surogat trebuie uneori schimbate. De exemplu, atunci cnd
dou companii fuzioneaz, angajaii trebuie reunii ntr-o sigur tabel, i
nu este exclus ca, datorit suprapunerilor, unele mrci s fie modificate.
Dou relaii ar putea avea chei surogat utilizator definite pe domenii
diferite, chiar dac entitile pe care le identific sunt aceleai; de exemplu,
o firm poate fi deopotriv client i furnizor al companiei noastre. n tabela
FURNIZORI firma ar putea fi identificat prin CodFurnizor, iar n tabela
CLIENI prin CodClient. Dei firma este aceeai, ea este identificat prin
dou chei surogat diferite.
Uneori este necesar preluarea informaiei despre o entitate naintea
atribuirii unei chei surogat utilizator pentru aceasta sau, pe de alt parte,
pstrarea valorii cheii surogat chiar i dup ce entitatea nu mai constituie
subiect al aplicaiei. Cazurile tipice sunt cele ale candidailor pentru
angajarea pe un post (li se va atribui o marc doar dac vor fi angajai,
atribuirea petrecndu-se dup angajare), sau cele ale angajailor
pensionai.
Cele trei dificulti sunt suficiente lui Codd pentru a justifica folosirea cheilor
surogat sistem, pe care utilizatorul nu le poate nici modifica, iar uneori nici mcar
vizualiza. Astfel, fiecare entitate ar avea propria cheie surogat, unic la nivelul
ntregii baze de date. Dou chei surogat sunt identice dac i numai dac
desemneaz aceeai entitate. Codd se gndea chiar i la o comand special de
fuzionare (COALESCE) a dou chei surogat care desemneaz, n fapt, aceeai
entitate. Mai mult, n articolul su din 1979 publicat n ACM Transactions on
Database Systems, autorul pledeaz pentru un model relaional extins n care,
printre multe altele, unul dintre domenii s serveasc drept surs pentru toate
cheile surogat din baz.

La ntrebarea "Ce e mai preferabil, s folosim cheile primare "naturale" sau cele
surogat ?" nu exist un rspuns universal sau, chiar dac ar exista, am evita s-l
dm. Atunci cnd atributul/atributele ce identific entitatea sunt disponibile i
stabile n timp, soluia "natural" este mai la ndemn: CNP-ul pentru persoane,
ISBN-ul pentru cri, CodFiscal pentru firme. Cnd apar probleme de

1 [Codd79]
Proiectarea bazelor de date 7

identificare, iar unicitatea este asigurat de un mare grup de atribute, sau cnd
atributul/atributele poteniale chei nu prezint stabilitate pe termen lung, atunci
soluia "surogat" este cea mai indicat. La exemplele din acest paragraf, mai putem
aduga:
Seria i numrul de buletin/carte de identitate - dei identific fiecare
persoan, peste un numr de ani, datorit schimbrii buletinului/crii de
identitate, combinaia i schimb valoarea, iar dac s-au fcut deja
arhivri, vor aprea complicaii.
Matricol - n multe coli, licee i universiti s-a practicat un sistem simplu
de atribuire a numrului matricol, astfel nct, dup civa ani, un matricol
era "reciclat". Cutarea n baza de date arhiv a liceului ar ridica probleme
mari, dac intervalul care intereseaz este de ordinul deceniilor. Necazuri
similare ar aprea dac unele firme ar recicla, n timp, mrcile angajailor,
numerele de inventar ale mijloacelor fixe etc.
Asemenea gen de discuii este valabil ns i la reciclarea cheilor surogat, aadar
ideea folosirii unor surogate-sistem se poate dovedi benefic.

Dintre materialele dedicate cheilor surogat, v-am recomanda pe cele scrise de


Mike Lonigro2, Ian Harrington3, Graeme C. Simsion4 i Fabian Pascal. n ceea ce
m privete, nu mprtesc nici lehamitea unor autori fa de cheile surogat,
mergndu-se, n acest sens, pn la a le "demasca" drept identificatoare deghizate
de obiecte (celebrele OID-uri din orientarea pe obiecte), dar nici frenezia "surogrii"
manifestat de muli proiectani de baze de date, dei, recunosc, frenezia cu pricina
nu are nimic toxic n ea, ci, mai degrab, ine de un soi de lene (subscriu cu toat
inima la ideea c lenea nu e toxic, dac nu e nsoit de "aditivi").
O alt temere legat de cheile surogat privete ngreunarea accesului la
informaiile din baza de date, ntruct acesta nu se mai realizeaz prin atribute
explicite, ci prin numere destul de irelevante pentru obiectul/entitatea n cauz.
Ori, utilizatorii obinuii ar fi obligai s rein kilograme ntregi de cifre nesuferite,
doar pentru a obine datele de care au nevoie. Nici acest neajuns nu trebuie s
descurajeze, deoarece trebuie s vedem o aplicaie, indiferent de tipologia sa, pe cel
puin dou straturi, date i interfa. n majoritatea copleitoare a cazurilor,
utizatorii interacioneaz cu baza doar prin meniuri, rapoarte i mai ales formulare,
iar cheile surogat pot fi chiar ascunse.
n orice caz, dac nu am reuit s tranm discuia chei naturale-chei surogat,
mcar s artm cu degetul pe cele mai toxice: cheile inteligente. Astfel, au existat
firme n care angajaii compartimentelor personal-salarizare audiaser sau au citiser
n cursurile de baze de date sau analiz/proiectare despre teoria codurilor i, la
angajarea unei persoane la compartimentul Contabilitate, i atribuiau o marc de tip
CTB85CC101, n care primele trei litere semnalizau c este ncadrat la

2 [Lonigro98]
3 [Harrington02], pp.77-82
4 [Simsion01], pp.282-285
8 Capitolul 7

compartimentul Contabilitate, iar cele dou C-uri semnalizau biroul Calculaia


costurilor, n cadrul compartimentului Contabilitate. Partizanii acestui sistem
argumentau c marca ar conine, n acest fel, informaii preioase (compartimentul
i biroul) despre fiecare angajat, cheia fiind chiar, dup unii autori, inteligent.
Dup cinci ani, ns, persoana respectiv se transfer la compartimentul
Financiar. Asta nseamn c marca, fiind inteligent, trebuie schimbat. Costul
schimbrii depinde de ntrebarea: unde se afl cele 5 (ani) * 12 (luni) nregistrri
despre calcululul lunar al salariului, plus 5(ani) * 12 (luni) * 20 (zile lucrtoare)
nregistrri, dac pontajul se preia zilnic ? Rspunsul poate fi nsoit de apstoare
dureri de cap.

7.1.3. Declararea/obinerea cheilor surogat

Modalitile prin care un atribut poate fi declarat cheie surogat difer de la


SGBD la SGBD. Trei dintre variante sunt folosite ceva mai des. Prima este
declararea unui atribut de tip autoincrement. Spre exemplu, n PostgreSQL exist
tipul de dat SERIAL:

CREATE TABLE note_contabile (


idnotacontabila SERIAL PRIMARY KEY
...
)

n momentul inserrii unei linii noi n tabel, atributul IdNotContabil va


primi, pe rnd, valorile: 1, 2, ... pn la 21474836475. Dac se dorete crearea unei
chei surogat de tip sistem, iar valoarea maxim de mai sus nu este de ajuns, se
poate folosi tipul BIGSERIAL, cu o plaj imens.
i n Visual FoxPro, odat cu versiunea 8 un atribut poate fi declarat cheie
surogat folosing clauza AUTOINC:

CREATE TABLE note_contabile (;


idnotacont INTEGER AUTOINC NEXTVALUE 1001 STEP 1 PRIMARY KEY;
)

Clauza adiional NEXTVALUE este util atunci cnd se dorete ca valoarea


iniial s nu fie 1, iar STEP stabilete mrimea incrementat. n versiunile mai
vechi ale VFP era necesar folosirea clauzei DEFAULT care fcea apel la o funcie
stocat6:

CREATE TABLE note_contabile (;


idnotacont NUMBER(10) DEFAULT vi_nc_idnota() PRIMARY KEY;
)
funcia fiind cea din listing 7.1.

5 Vezi documentaia PostgreSQL de pe www.postgresql.org


6 Vezi i [Fotache s.a.02], pp.184-188
Proiectarea bazelor de date 9

Listing 7.1. Funcia stocat care furnizeaz valoarea implicit a cheii surogat
PROCEDURE vi_nc_idnota
LOCAL v_idnota (1,1)
v_idnota=0
SELECT MAX(idnotacont) FROM note_contabile ;
INTO ARRAY v_idnota
IF v_idnota (1,1) = 0 && e prima nota contabila
RETURN 1001
ELSE
RETURN v_idnota(1,1) + 1
ENDIF
ENDPROC

O alt soluie, disponibil n Oracle i PostgreSQL, presupune folosirea


secvenelor, care sunt obiecte ale bazei de date ce furnizeaz la fiecare invocare a
clauzei NextVal o valoare unic:

CREATE SEQUENCE seq_idnota START WITH 1001


MINVALUE 1001 MAXVALUE 9999999999 NOCACHE NOCYCLE ORDER

Secvena seq_idnota creat va furniza valori strict ordonate dup momentul


apelului, cuprinse ntre 1001 i 9999999999, iar dup atingerea limitei superioare
secven se blocheaz (clauza NOCYCLE). n lipsa clauzei NOCYCLE, dup
atingerea valorii maxime, urmtoarea valoare furnizat este cea minim. Secvena
va fi folosit n declanatorul de inserare al tabelei NOTE_CONTABILE. Scriptul
de crearea a celor trei tabele discutate n paragraful 7.1.1 (vezi figura 7.1) constituie
subiectul listingului 7.2 care poate fi descrcat de la adresa http://www.feaa.uaic.
ro/cercetare/publicatii). Iat corpul declanatorului n listing 7.37.
Listing 7.3. Declanatorul Oracle pentru valoarea implicit a cheii surogat
CREATE OR REPLACE TRIGGER trg_note_contabile_ins
BEFORE INSERT ON note_contabile FOR EACH ROW
BEGIN
SELECT seq_idnota.NextVal INTO :NEW.IdNotaContabila FROM dual ;
END ;

7.2. Restricii simple i complexe. Atribute redundante


ajuttoare
Nu ntotdeauna atributele implicate n schema bazei pot fi identificate cu
uurin. Mai mult, deseori nu stric o doz rezonabil de artificial, de
improvizaie, doz care poate salva pe termen lung o schem sau mcar s o
scuteasc de multe necazuri. Dei mai puin intuitiv dect modelul obiectual,
relaionalul are certe atuuri n ceea ce privete mecanismul de declarare a

7 Detalii privind declaatoarele pentru valori implicite n Oracle sunt prezentate n paragraful 11.2
al lucrrii [Fotache s.a.03], pp.386-391
10 Capitolul 7

restriciilor i mai ales n ceea ce privete opiunile de extragere a informaiilor din


baz (mecanismul de manipulare a datelor). Chiar i aa, rmn n discuie o mare
parte din restricii care in de integritatea bazei de date, dar sunt legate nu att de
reguli intrinseci modelului (cheie primar, restricie de entitate, integritate
referenial), ct de regulile aplicaiei, sau, altfel spus, reguli ale afacerii. Din
pcate, nici un model de date nu ofer un mecanism infailibil de declarare a
tuturor restriciilor semantice dintr-o baz de date.

7.2.1. Reguli la nivel de atribut i nregistrare

Pn n acest paragraf, cele mai pomenite restricii semantice ale bazei au fost
dependenele funcionale, multivaloare i de jonciune. Probabil, ns, c practici-
enii sunt cel mai bine familiarizai cu altfel de restricii: reguli de validare la nivel
de atribut (field validation rule) i reguli de validate la nivel de nregistrare (record
validation rule).
S lum n discuie schema bazei de date EXAMENE din paragraful 5.5.2 (caz
practic nr.2), al crei graf DF constituie subiectul figurii 5.17. Reamintim c baza de
date preia informaii legate de cursurile predate i rezultatele obinute la
examinrile organizate la Facultatea de Economie i Administrarea Afacerilor. Chiar i
la o privire sumar a celor ase relaii, se pot desprinde cteva reguli de validare la
nivel de atribut:
- n relaia STUDENI:
o An (anul de studiu al studentului) este un numr natural cuprins
ntre 1 i 5;
o Modul-ul n care poate fi nscris un student de la FEAA este 1 sau
2;
o Spec-ializarea poate avea doar una cele 10 valori: Finane-Bnci,
Contabilitate, Informatic economic, Marketing etc.
- n relaia DISCIPLINE numrul de credite al unei discipline (NrCredDisc)
nu poate fi mai mare de 8;
- n PROFESORI, atributul GradProf poate avea doar una din ase valori:
colaborator, preparator, asistent, lector, confereniar i profesor.
- examenele sunt programate doar n lunile ianuarie-februarie i mai-iunie-
iulie, corespunztor celor dou sesiuni; prin urmare, componenta lun a
atributului DataEx n tabelele SLI_EX i NOTE trebuie s se ncadreze n
cele cinci valori;
- nota maxim obinut este 10, iar pentru absent valoarea convenit a notei
este zero (discutabil !).
Acest gen de restricii poate fi legat n modelul relaional de noiunea de
domeniu de valori. Dup cum vom discuta spre finalul acestei lucrri, majoritatea
SGBD-urilor nu se bazeaz pe definirea explicit a domeniului, ci prin opiuni de
declarare a acestor reguli de o manier intuitiv, prin comenzi SQL sau interactiv,
de fiecare SGBD.
Proiectarea bazelor de date 11

n privina regulilor la nivel de nregistrare, principala diferen fa de cele la


nivel de atribut ine de numrul atributelor implicate simultan, cel puin dou. De
exemplu, n relaia STUDENI, innd seama c primii doi ani de studiu sunt
comuni, iar nscrierea la o specializare se face de fiecare student ncepnd cu anul
3, este evident c pentru pentru firecarea linie n care An este 1 sau 2, valoarea
atributului Spec nu trebuie s se regseasca n lista celor 10 descris mai sus;
aadar, avem nevoie i de o valoare de genul Trunchi comun care se adaug
domeniului de valori pentru Spec; firete, am putea recurge i la valoarea NULL,
ns, fideli principiului ocolii NULL-ul ori de cte ori avei ocazia, vom mara pe
varianta trunchiului comun. ncercnd s ne apropiem mai mult de o formalizare
aproximativ, putem redacta restricia la modul: dac valoarea atributului An este
1 sau 2, atunci obligatoriu atributul Spec ia valoarea Trunchi comun !

7.2.2. Reguli, atribute redundante i declanatoare

Cele dou tipuri de restricii acoper o zon destul de subire din clasa regulilor
ce pot guverna o aplicaie/baz de date. ncercm s trecem ntr-un registru mai
dificil. Ne ntoarcem la baza de date BIBLIOTEC, a crei schem a fost finalizat
n paragraful 6.2 (vezi figura 6.10) prin aducerea n 4NF. Din raiuni bugetare,
conducerea facultii/bibliotecii stabilete urmtoarea restricie: O carte nu poate fi
achiziionat n mai mult de 10 exemplare ! Cum se poate implementa o asemenea
restricie ? Norocul nostru este c, ntre cele cinci relaii ale bazei de date, e uor de
identificat cea mpricinat - EXEMPLARE {Cot, ISBN}. Respectarea acestei
restricii presupune ca, la inserarea unei linii n aceast tabel, sau la modificarea
unui ISBN, s se verifice dac numrul cotelor pentru "noul" ISBN (valoarea
atributului ISBN de pe linia inserat sau modificat) nu depete 10, situaie n
care inserarea/modificarea s fie blocat.
Acest gen de restricie poate fi implementat n dou moduri. Fr a schimba
schema bazei de date, se recurge la declanatoarele de inserare i modificare ale
tabelei EXEMPLARE, declanatoare care opereaz dup o logic simpl de genul
prezentat n listingul 7.5 (listingul 7.4 poate fi descrcat de pe pagina Web
amintit mai sus, coninnd scriptul de creare a tabelelor):
Listing 7.5. Declanator Oracle de inserare8 pentru implementarea restriciei legate de
nr. de exemplare dintr-o carte
CREATE OR REPLACE TRIGGER trg_exemplare_ins
BEFORE INSERT ON exemplare
FOR EACH ROW
DECLARE
v_nrexemplare NUMBER(5) := 0 ;
BEGIN
SELECT COUNT(*) INTO v_nrexemplare
FROM exemplare WHERE isbn = :NEW.isbn ;

8 Declanatorul de modificare trebuie s fie redactat astfel nct s prentmpine situaiile de


"mutan" a tabelei EXEMPLARE (vezi [Fotache s.a.03], pp.396-402).
12 Capitolul 7

IF v_nrexemplare > 9 THEN


RAISE_APPLICATION_ERROR (-20801, 'Nr. exemplarelor acestei carti creste peste 10 !')
;
END IF ;
END ;

Variantei i se poate reproa cel puin faptul c un mare numr de cri poate
implica timpi de execuie destul de greu de trecut cu vederea, deoarece SELECT
COUNT(*)-ul parcurge ntreaga tabel. Aa c ne-am putea gndi la o variant ce
pare, la prima vedere, mai laborioas: n tabel TITLURI introducem atributul
NrExemplare, atribut care va fi actualizat automat prin cele trei declanatoare ale
EXEMPLARE: TITLURI {ISBN, Titlu, Editura, AnApariie, NrExemplare}:

ALTER TABLE titluri ADD (NrExemplare NUMBER(4));

n continuare, crem o funcie f_NrExemplare creia i se paseaz un ISBN i


returneaz numrul de exemplare al acelui ISBN - vezi listing 7.6.

Listing 7.6. Funcie Oracle ce returnaz nr. de exemplare ale unei cri
CREATE OR REPLACE FUNCTION f_NrExemplare ( isbn_ titluri.isbn%TYPE)
RETURN NUMBER
IS
v_nr NUMBER(4) := 0 ;
BEGIN
SELECT nrexemplare INTO v_nr FROM titluri WHERE isbn=isbn_ ;
RETURN v_nr ;
END ;

Declanatoarele tabelei EXEMPLAREl, care folosesc funcia, se prezint astfel:

- Pentru inserare - vezi listing 7.7:


Listing 7.7. Versiunea a doua de declanatorului din listingul 7.5
CREATE OR REPLACE TRIGGER trg_exemplare_ins
BEFORE INSERT ON exemplare
FOR EACH ROW
BEGIN
IF f_NrExemplare (:NEW.isbn) > 3 THEN
RAISE_APPLICATION_ERROR (-20801,
'Nr. exemplarelor acestei carti creste peste 10 !') ;
ELSE
/* se incrementeaza numarul exemplarelor pentru ISBN-ul din linia inserata */
UPDATE titluri SET NrExemplare = NrExemplare + 1 WHERE isbn = :NEW.isbn ;
END IF ;
END ;

- Pentru modificare - vezi listing 7.8:


Listing 7.8. Declanatorul de modificare a valorii atributului EXEMPLARE.isbn
CREATE OR REPLACE TRIGGER trg_exemplare_upd
BEFORE UPDATE OF isbn ON exemplare
FOR EACH ROW
Proiectarea bazelor de date 13

BEGIN
IF f_NrExemplare (:NEW.isbn) > 3 THEN
RAISE_APPLICATION_ERROR (-20801,
'Nr. exemplarelor acestei carti creste peste 10 !') ;
ELSE
/* se scade cu 1 numarul exemplarelor pentru
vechiul ISBN (valoarea dinaintea modificarii) */
UPDATE titluri SET NrExemplare = NrExemplare - 1 WHERE isbn = :OLD.isbn ;

/* se incrementeaza numarul exemplarelor pentru ISBN-ul din linia inserata */


UPDATE titluri SET NrExemplare = NrExemplare + 1 WHERE isbn = :NEW.isbn ;
END IF ;
END ;
Un avantaj colateral al acestei variante de declanator, avantaj deloc neglijabil
pentru "oracliti", este c se elimin problema "mutanei" tabelei EXEMPLARE.

- Pentru tergere - vezi listing 7.9:


Listing 7.9. Declanatorul de tergere a unei linii din tabela EXEMPLARE
CREATE OR REPLACE TRIGGER trg_exemplare_del
BEFORE DELETE ON exemplare FOR EACH ROW
BEGIN
/* se scade cu 1 numarul exemplarelor pentru ISBN-ul sters */
UPDATE titluri SET NrExemplare = NrExemplare - 1 WHERE isbn = :OLD.isbn ;
END ;

Puini, probabil, vor fi nclinai s accepte c aceast ultim soluie ar fi mai


indicat dect prima, atta vreme ct declanatoarele de inserare/modificare folo-
sesc o funcie care execut fraza SELECT COUNT..., apoi, ele nsele, conin comenzi
UPDATE. Ei bine, odat pus n aplicare, lucrurile nu sunt aa de negre. Mai nti,
SELECT-ul din funcia f_NrExemplare are toate anele s se execute destul de
rapid, ntruct n EXEMPLARE atributul ISBN este cheie strin (atributul printe
fiind ISBN din TITLURI), iar la declararea unei asemenea restricii refereniale,
orice server de baze de date creaz un index. Acest index crete sensibil viteza att
n cazul interogrii SELECT COUNT..., ct i la UPDATE-uri. n al doilea rnd,
frecvena modificrii unui ISBN n EXEMPLARE, ca i tergerii unei linii n aceeai
tabel, este foarte sczut, astfel nct, statistic privind lucrurile, declanatorul cel
mai important este cel de inserare.

Marele merit al acestei ultime soluii este c o pregtete pe a treia, care e mult
mai simpl. Folosim atributul NrExemplare n TITLURI, atribut modificat prin
cele trei declanatoare ale tabelei EXEMPLARE, ns restricia o implementm n
baz printr-o regul de validare declarat pentru acest atribut, adic, dup sintaxa
SQL:

ALTER TABLE titluri ADD CONSTRAINT ck_nrexemplare


CHECK (NrExemplare <= 10) ;
14 Capitolul 7

Acum cele trei declanatoare ale tabelei EXEMPLARE se simplific sensibil,


dup cum se observ i n listingul 7.10.
Listing 7.10. Noile declanatoare ale tabelei EXEMPLARE
CREATE OR REPLACE TRIGGER trg_exemplare_ins
BEFORE INSERT ON exemplare FOR EACH ROW
BEGIN
/* se incrementeaza numarul exemplarelor pentru ISBN-ul din linia inserata */
UPDATE titluri SET NrExemplare = NrExemplare + 1
WHERE isbn = :NEW.isbn ;
END ;
/

CREATE OR REPLACE TRIGGER trg_exemplare_upd


BEFORE UPDATE OF isbn ON exemplare FOR EACH ROW
BEGIN
/* se scade cu 1 numarul exemplarelor pentru vechiul ISBN (valoarea dinaintea modificarii) */
UPDATE titluri SET NrExemplare = NrExemplare - 1 WHERE isbn = :OLD.isbn ;

/* se incrementeaza numarul exemplarelor pentru ISBN-ul din linia inserata */


UPDATE titluri SET NrExemplare = NrExemplare + 1 WHERE isbn = :NEW.isbn ;
END ;
/

CREATE OR REPLACE TRIGGER trg_exemplare_del


BEFORE DELETE ON exemplare FOR EACH ROW
BEGIN
/* se scade cu 1 numarul exemplarelor pentru ISBN-ul sters */
UPDATE titluri SET NrExemplare = NrExemplare - 1 WHERE isbn = :OLD.isbn ;
END ;

Firete, nici de funcie nu mai avem nevoie n noile condiii, iar soluia are mult
mai multe anse de a fi acceptat.

Atenie, ns ! Atunci cnd utilizm un asemenea atribut redundant, trebuie s


fim siguri c modul su de actualizare este bine pus la punct, astfel nct s nu
existe nici o modalitate prin care valoarea sa ar fi neconform cu realitatea din baza
de date. Ori, cel mai sigur mecanism este cel al declanatoarelor. Din pcate, muli
dezvoltatori folosesc asemenea artificii doar n interfa (formulare) sau logica
aplicaiei, situaii n care actualizarea atributelor calculate poate fi ocolit, cu sau
fr bun tiin.

Un caz i mai interesant


S lum n discuie i un alt exemplu. n primul paragraf din acest capitol am
modificat baza de date dedicat notelor contabile introducnd chei surogat, i, pe
baza grafului dependenelor din figura 7.1, am ajuns la o schem alctuit din trei
relaii: NOTE_CONTABILE {IdNotContabil, NrNota, Data, Explica-
iiNot}, OPERAIUNI {IdOperaiune, NrOp, IdNotContabil,
ExplicaiiOp} i DETALII_OPERAIUNI {IdOperaiune, ContDebitor,
ContCreditor, Suma}. Coninutul acestor relaii, obinut prin prelucrarea relaiei
Proiectarea bazelor de date 15

LINII_ARTICOLE_CONTABILE (figura 4.1) poate fi neles din cele cteva


nregistrri din figura 7.4 (listingul de populare a celor trei tabele - 7.11 - este
disponibil pe pagina web amintit).
NOTE_CONTABILE
IdNotaContabil NrNota Data ExplicaiiNot
1001 5 30.11.2004 ...
1002 6 01.12.2004 ...

OPERAIUNI
IdOperaiune NrOp IdNotaContabil ExplicaiiOp
10001 1 5 ...
10002 2 5 ...
10003 1 6 ...

DETALII_OPERAIUNI
IdOperaiune ContDebitor ContCreditor Suma
10001 300 401 10000000
10001 4426 401 1900000
10002 401 5311 7000000
10002 401 5121 4800000
10003 5121 700 5250000

Figura 7.4. Cteva linii din relaiile bazei de date CONTABILITATE


O restricie extrem de important n contabilite este c n orice nregistrare
contabil compus nu pot fi simultan dou sau mai multe conturi i pe debit i pe credit.
Altfel spus, ntr-o operaiune contabil, exist fie un cont debitor i unul sau mai
multe conturi creditoare, fie un singur cont creditor i unul sau mai multe
debitoare !
Soluia de nceput a acestei probleme ine de valorificarea declanatoarelor de
inserare i modificare. La fiecare inserare n DETALII_OPERAIUNI, declanatorul
trebuie s fac o verificare de genul celei din listingul 7.12.
Listing 7.12. Verificarea numrului de conturi pe debit i pe credit la inserare
CREATE OR REPLACE TRIGGER trg_do_ins
BEFORE INSERT ON detalii_operatiuni FOR EACH ROW
DECLARE
v_nrcontdebit NUMBER(4) := 0 ;
v_nrcontcredit NUMBER(4) := 0 ;
BEGIN
SELECT COUNT(DISTINCT ContDebitor), COUNT (DISTINCT ContCreditor)
INTO v_nrcontdebit, v_nrcontcredit
FROM
(SELECT ContDebitor, ContCreditor
FROM detalii_operatiuni
WHERE idoperatiune = :NEW.idoperatiune
UNION
SELECT :NEW.ContDebitor, :NEW.ContCreditor
FROM dual
)T;

IF v_nrcontdebit > 1 AND v_nrcontcredit > 1 THEN


16 Capitolul 7

RAISE_APPLICATION_ERROR (-20852,
'Nu pot fi simultan mai multe conturi si pe debit si pe credit') ;
END IF ;
END ;

A fost nevoie de un mic artificiu pentru c SELECT-ul nu ar fi luat n calcul


nregistrare ce tocmai se adaug n tabel (nregistrare care, n fond, determin
lansarea declanatorului): cele dou conturi din noua nregistre sunt "lipite" celor
deja introduse folosind reuniunea (i tabela DUAL).

Nici la modificarea aceleai tabele lucrurile nu sunt mai tihnite. Pentru a evita
problema mutanei, i pornind de la premisa c printr-un UPDATE nu se modific
niciodat simultan conturile si identificatoarele mai multor operaiuni, soluia
Oracle recurge la triada: variabil public (pachet), declanator de actualizare la
nivel de linie, declanator de actualizare la nivel de comand - vezi listingul 7.13.
Listing 7.13. Verificarea numrului de conturi pe debit i pe credit la modificare
CREATE OR REPLACE PACKAGE pac_trg IS
v_idoper operatiuni.idoperatiune%TYPE ;
END ;
/

CREATE OR REPLACE TRIGGER trg_do_upd1


BEFORE UPDATE OF IdOperatiune, ContDebitor, ContCreditor
ON detalii_operatiuni FOR EACH ROW
BEGIN
pac_trg.v_idoper := :NEW.idoperatiune ;
END ;
/

CREATE OR REPLACE TRIGGER trg_do_upd2


AFTER UPDATE OF IdOperatiune, ContDebitor, ContCreditor
ON detalii_operatiuni
DECLARE
v_nrcontdebit NUMBER(4) := 0 ;
v_nrcontcredit NUMBER(4) := 0 ;
BEGIN
SELECT COUNT(DISTINCT ContDebitor), COUNT (DISTINCT ContCreditor)
INTO v_nrcontdebit, v_nrcontcredit FROM detalii_operatiuni
WHERE idoperatiune = pac_trg.v_idoper ;

IF v_nrcontdebit > 1 AND v_nrcontcredit > 1 THEN


RAISE_APPLICATION_ERROR (-20852,
'Nu pot fi simultan mai multe conturi si pe debit si pe credit') ;
END IF ;
END ;
/

Nu foarte multe SGBD-uri sunt dispuse s ofere suport pentru asemenea gen de
artificii, aa c ne bate gndul s recurgem la unul sau mai multe atribute redun-
dante care s mai limpezeasc din ape. Aadar, introducem n OPERAIUNI dou
atribute, numite NrConturiDebitoare i NrConturiCreditoare, care s
furnizeze numrul conturilor din debitul i din creditul fiecrei operaiuni
Proiectarea bazelor de date 17

contabile. Astfel, relaia ar avea structura: OPERAIUNI {IdOperaiune,


IdNotContabil, ExplicaiiOp, NrConturiDebitoare, NrConturiCre-
ditoare}. Curnd ne dm seama c avem o problem, ntruct cele dou atribute
trebuie s ia n calcul fiecare cont o singur dat. S fim mai explicii. Prima
operaiune din nota 101, cea care are identificatorul 10001, se scrie n "limbaj"
contabil astfel:

% 401 11800000
300 10000000
4426 1800000

Operaiunii i corespund primele dou linii din DETALII_OPERAIUNI, deci


stocarea se face transformnd nregistrarea dup urmtorul calapod:

Debit Credit Suma


300 401 10000000
4426 401 1800000

Acest mod de stocare a nregistrrilor este fundamental pentru obinerea docu-


mentelor contabile ce prelucreaz notele contabile: fie de cont, cartea-mare,
balane de verificare. Adugnd cele dou atribute n OPERAIUNI, pe prima linie
a acestei tabele (corespunztoare nregistrrii contabile pe care o discutm)
valoarea atributului NrConturiDebitoare este 2, iar cea a NrConturiCredi-
toare este 1. Declanatorul de inserare al tabelei DETALII_OPERAIUNI nu
trebuie s incrementeze pur i simplu cele dou atribute, ci, n mod normal, numai
unul, deoarece cellalt se repet. Bine, dar cum tie declanatorul care cont se
repet, pe debit sau pe credit, fr a face SELECT-ul acela impresionant ?
ngrond gluma, mai introducem dou conturi, UnContDebitor i
UnContCreditor care vor avea valoarea iniial (implicit) NULL:

ALTER TABLE operaiuni ADD NrConturiDebitoare NUMBER (3) ;


ALTER TABLE operaiuni ADD NrConturiCreditoare NUMBER (3) ;
ALTER TABLE operaiuni ADD UnContDebitor VARCHAR(14) ;
ALTER TABLE operaiuni ADD UnContCreditor VARCHAR(14) ;

Valoarea implicit a acestor patru atribute va fi NULL. La prima introducere a


unei corespondene contabile pentru o operaiune, adic la prima inserare a unei
linii n DETALII_OPERAIUNI, NrConturiDebitoare i NrConturiCredi-
toare vor primi valoarea 1, UnContDebitor va lua valoarea :NEW.ContDebi-
tor (valoarea de pe linia inserat a atributului ContDebitor), iar UnContCre-
ditor pe :NEW.ContCreditor. La a doua inserare a unei corespondene cont
debitor - cont creditor pentru aceeai not contabil, atributul NrConturiDebitoa-
re trebuie incrementat cu 1 numai dac valoarea :NEW.ContDebitor este
diferit de cea a UnContDebitor, iar NrConturiCreditoare trebuie incremen-
tat cu 1 doar dac valoarea :NEW.ContCreditor este diferit de cea a atributului
UnContCreditor (s nu spunei c n-ai neles !).
18 Capitolul 7

Cu un pic de imaginaie, reducem ntreg declanatorul pentru inserare n tabela


DETALII_OPERAIUNI la o singur, dar generoas, comand UPDATE - vezi
listing 7.14
Listing 7.14. Noul declanator de inserare ce actualizeaz atributele "redundante"
CREATE OR REPLACE TRIGGER trg_do_ins
AFTER INSERT ON detalii_operatiuni FOR EACH ROW
BEGIN
UPDATE operatiuni
SET nrconturidebitoare = NVL(nrconturidebitoare, 1) +
CASE WHEN :NEW.contdebitor <> NVL(uncontdebitor,:NEW.contdebitor)
THEN 1 ELSE 0 END,
nrconturicreditoare = NVL(nrconturicreditoare, 1) + CASE
WHEN :NEW.contcreditor <> NVL(uncontcreditor,:NEW.contcreditor)
THEN 1 ELSE 0 END,
uncontdebitor = :NEW.contdebitor,
uncontcreditor = :NEW.contcreditor
WHERE idoperatiune = :NEW.idoperatiune ;
END ;

Acum putem declara linitii restricia sub forma regulii de validare:

ALTER TABLE operaiuni ADD CONSTRAINT ck_nrconturi


CHECK ( NOT (NVL(nrconturidebitoare,0) > 1 AND
NVL(nrconturicreditoare,0) > 1) ) ;

n noile condiii, i operaiunile derulate la modificri survenite n tabela DETA-


LII_OPERAIUNI se simplific sensibil. Putem renun la pachet i unul dintre
declanatoare, iar noua form trigger-ului de modificare este cea din listing 7.15.
Listing 7.15. Noul declanator de modificare n DETALII_OPERAIUNI
DROP TRIGGER trg_do_upd1 ;
DROP TRIGGER trg_do_upd2 ;
DROP PACKAGE pac_trg ;
/

CREATE OR REPLACE TRIGGER trg_do_upd


AFTER UPDATE OF IdOperatiune, ContDebitor, ContCreditor
ON detalii_operatiuni FOR EACH ROW
BEGIN
-- mai intii, se fac modificarile pentru vechile valori
UPDATE operatiuni
SET nrconturidebitoare = nrconturidebitoare -
CASE WHEN nrconturidebitoare > 1 THEN 1 ELSE 0 END,
nrconturicreditoare = nrconturicreditoare -
CASE WHEN nrconturicreditoare > 1 THEN 1 ELSE 0 END
WHERE idoperatiune = :OLD.idoperatiune ;

-- apoi, se fac modificarile pentru noile valori


UPDATE operatiuni
SET nrconturidebitoare = nrconturidebitoare +
CASE WHEN :NEW.contdebitor <> uncontdebitor THEN 1 ELSE 0 END,
nrconturicreditoare = nrconturicreditoare +
CASE WHEN :NEW.contcreditor <> uncontcreditor THEN 1 ELSE 0 END,
Proiectarea bazelor de date 19

uncontdebitor = :NEW.contdebitor,
uncontcreditor = :NEW.contcreditor
WHERE idoperatiune = :NEW.idoperatiune ;
END ;

Listingul 7.16 conine declanatorul de tergere care asigur decrementarea


numrului de conturi debitoare i a celui de conturi creditoare i, n caz c se terge
ultima linie dintr-o operaiune notabil, NULL-izarea atributelor UnContDebitor
i UnContCreditor.
Listing 7.16. Declanatorul de tergere n DETALII_OPERAIUNI
CREATE OR REPLACE TRIGGER trg_do_del
BEFORE DELETE ON detalii_operatiuni FOR EACH ROW
BEGIN
UPDATE operatiuni
SET nrconturidebitoare = nrconturidebitoare - CASE WHEN nrconturidebitoare > 1
OR nrconturidebitoare+nrconturicreditoare=2 THEN 1 ELSE 0 END,
nrconturicreditoare = nrconturicreditoare - CASE WHEN nrconturicreditoare > 1
OR nrconturidebitoare+nrconturicreditoare=2 THEN 1 ELSE 0 END,
uncontdebitor = CASE WHEN nrconturidebitoare+nrconturicreditoare=2
THEN NULL ELSE uncontdebitor END,
uncontcreditor = CASE WHEN nrconturidebitoare+nrconturicreditoare=2
THEN NULL ELSE uncontcreditor END
WHERE idoperatiune = :OLD.idoperatiune ;
END ;

7.2.3. Restricii i relaii noi

n paragraful 7.2.1 am enumerat cteva posibile reguli de validare la nivel de


atribut, printre care i una pentru relaia STUDENI prin care, n primii doi ani,
studenii urmeaz un trunchi comun de discipline, iar ncepnd cu anul 3 de studiu
i aleg specializarea, astfel nct atributul Spec trebuie s ia o valoarea pe dome-
niul : (Contabilitate i informatic de gestiune, Economie general, Finane-Bnci etc.).
Chiar dac privete atributul Spec, declararea acestei restricii printr-o clauz
CHECK este destul de anevoioas:

CREATE TABLE studeni (


matricol ...
...
Spec VARCHAR(50) NOT NULL CHECK
(Spec IN 'Contabilitate i informatic de gestiune', '',
'Informatic economic', ...)
...
);

Lista celor 10 specializri, plus Truchi comun, este una destul de mare, iar dac
se ia decizia implementrii aplicaiei la nivelul unei universiti precum Al. I. Cuza
din Iai, ale crei faculti patroneaz zeci de specializri, scrierea clauzei CHECK
devine o sarcin pe care o s-o execute doar cel care nu va avea cui s i-o paseze.
Ceea ce v propunem este o soluie cunoscut multor practicieni: constituirea unei
20 Capitolul 7

relaii dedicate specializrilor (SPECIALIZRI), urmnd ca regula s fie ulterior


implementat printr-o restricie referenial STUDENI-SPECIALIZRI.
Dac dorim s pstrm acelai numr de atribute, crem o tabel cu un singur
atribut:

CREATE TABLE specializri (


spec VARCHAR(50) NOT NULL PRIMARY KEY
);

i apoi
CREATE TABLE studeni (
matricol ...
...
,spec VARCHAR(50) NOT NULL
REFERENCES specializri (spec)
...
);

A doua variant este mai nimerit, chiar dac avem un atribut suplimentar de
tip cheie surogat prin care identificm fiecare specializare:

CREATE TABLE specializri (


idspec NUMBER(4) NOT NULL PRIMARY KEY
,spec VARCHAR(50)
);

CREATE TABLE studeni (


matricol ...
...
,idspec NUMBER(4) NOT NULL
REFERENCES specializri (idspec)
...
);

Preul ultimelor dou soluii ine de complexitatea presupus de gestionarea


unei chei strine, precum i timpul mai mare necesar interogrilor i verificrilor ce
decurg din restricia referenial. Marele avantaj ine de flexibilitate, orice speciali-
zare nou sau modificare a denumirii unei specializrii neatrgnd modificarea
schemei bazei, fiind suficient doar o comand DML.
ntruct folosirea clauzei CHECK prezint avantajul net al vitezei i simplitii
(s nu uitm c la restriciile refereniale apar proleme de genul DELETE RES-
TRICT/CASCADE i mai ales UPDATE CASCADE/RESTRICT), recomandabil
este ca, atunci cnd lista valorilor acceptate nu e prea lung, iar acestea (valorile)
sunt constante, s recurgem la o regul de validare la nivel de atribut, n timp ce
atunci cnd valorile sunt mai numeroase i/sau mai susceptibile a fi modificate, s
crem o nou relaie (i, implicit, o nou restricie referenial).
Pstrnd discuia n baza de date EXAMENE, s vedem ce implicaii ar avea o
restricie de genul: fiecare profesor poate preda oricte discipline, dar numai la
specializrile care i sunt repartizate. Exist, prin urmare, profesori ce predau la o
Proiectarea bazelor de date 21

singur specializare, profesori ce predau la dou specializri etc. Relaia


DISC_SPEC_PROFI {CodDisc, An, Modul, IdSpec, CodProf} este cea "mprici-
nat", ns aici implementarea acestei restricii printr-o regul de validare la nivel
de nregistrare este cu totul improbabil, aa c mai realist ar fi s crem o relaie
de genul: PROFI_SPECIALIZRI {An, Modul, IdSpec, CodProf}. Interesant c,
spre deosebire de exemplul precedent, cheia strin este compus ru de tot:

CREATE TABLE profi_specializri (


an NUMBER(1) NOT NULL
,modul VARCHAR(20) NOT NULL
,idspec NUMBER(5) NOT NULL REFERENCES specializari(idspec)
,codprof NUMBER(5) NOT NULL REFERENCES profesori (codprof)
,PRIMARY KEY (an, modul, spec, codprof)
);

CREATE TABLE disc_spec_profi (


coddisc CHAR(6) NOT NULL REFERENCES discipline (coddisc)
,an NUMBER(1) NOT NULL
,modul VARCHAR(20) NOT NULL
,idspec NUMBER(5) NOT NULL REFERENCES specializari(idspec)
,codprof NUMBER(5) NOT NULL REFERENCES profesori (codprof)
,PRIMARY KEY (coddisc, an, modul, spec)
,FOREIGN KEY (an, modul, spec, codprof)
REFERENCES profi_specializri(an, modul, idspec, codprof)
);

Oricrei alocri a unei specializri unui profesor i va corespunde o linie n


tabela PROFI_SPECIALIZRI. Forma final al script-ului de creare a tabelelor
poate de descrcat de pe pagina web cunoscut (listing 7.17).

7.3. Dependene de incluziune


Este greu de explicat motivl pentru care toate crile majore care trateaz
problema normalizrii i, n general, proiectarea bazelor de date, sunt att de
discrete n materie de dependene de incluziune. Nu-i vorb c noiunea ar fi din
cale-afar de dificil, ci mai deagrab c n aplicaiile economice, chiar de calibru
mediu, este aproape imposibil s nu se manifeste un asemenea gen de dependen.
ntr-o exprimare lejer, ntre dou atribute X i Y exist o dependen de
incluziune (DI) dac i numai dac orice valoare a lui X este obligatoriu i valoare a
lui Y i se noteaz simplu X Y. Mai general, o DI semnific faptul c proiecia pe
m atribute date ale relaiei R este un subset al proieciei pe m atribute date din
relaia S. De aici i caracterizarea DI ca dependene interrelaii 9. Definiia ns nu
oblig ca R i S s fie neaprat distincte, deci DI se poate institui ntre atribute i
grupe de atribute ale aceleai relaii. Dup Casanova s.a., DI semnalizeaz relaiile

9 [Fagin 81]
22 Capitolul 7

de tip este-un/o10, iar Sciore este i mai limpede cnd scrie c folosim DI pentru a
arta c dou atribute din relaii diferite se refer la acelai lucru 11. Am putea
amenda afirmaia lui Sciore prin precizarea faptului c atributele nu trebuie
neaprat s fie din relaii diferite.
Cel mai frecvent exemplu de DI este Manager Angajat12, care nseamn c
orice manager este un angajat al companiei. Graful din figura 7.5 ilustreaz o
situaie comun n firmele de pretutindeni: fiecare angajat al unei ntreprinderi este
arondat unui compartiment (Producie, Marketing, Personal, Financiar, Contabilitate
etc.); fiecare compartiment are un singur ef; fiecare ef este, i el, angajat, chiar
dac mai uit uneori de lucrul acesta.

Figura 7.5. O dependen de incluziune


Dependena de jonciune reprezentat prin sgeata punctat este, deci,
Marcef Marc. Decuparea relaiilor din graf nu ridic mari probleme. Avem
dou surse de DF, prin urmare cele dou relaii vor fi: PERSONAL {Marc,
NumePren, Adres, Compartiment} i COMPARTIMENTE {Compartiment,
Marcef}. Dependena de incluziune va fi ncorporat n schem sub forma
restriciei refereniale dintre COMPARTIMENTE.Marcef i PERSONAL.Marc.

Frunze i conturi
Ne ndeprtm de exemplele consacrate, apelnd la baza de date CONTABILI-
TATE, pe care, dup discuia din paragraful 7.2.2, am adus-o la schema: NO-
TE_CONTABILE {IdNotContabil, Data, ExplicaiiNot}, OPERAIUNI
{IdOperaiune, IdNotContabil, ExplicaiiOp, NrConturiDebitoare,
NrConturiCreditoare, UnContDebitor, UnContCreditor} i DETALII_O-
PERAIUNI {IdOperaiune, ContDebitor, ContCreditor, Suma}. Ei bine, a
sosit momentul pentru a introduce una din cele mai importante restricii ale unei
aplicaii de contabilitate general: conturile care apar n operaiunile contabile nu
trebuie s aib conturi sintetice/analitice subordonate !
S ncepem explicaiile mai din "amonte". Pentru reflectarea elementelor
patrimoniale, a capitalului, datoriilor i obligaiilor, contabilitatea folosete conturi

10 [Casanova s.a.82]
11 [Sciore83]
12 [Fagin81], [Casanova s.a.82], [Johnson & Klug82], [Sciore83]
Proiectarea bazelor de date 23

organizate n serii, clase i grupe13. De exemplu, n seria 1 sunt incluse conturile


organice de bilan, n seria 2 conturile de procese, n seria 3 conturile de rezultate,
n seria 4 conturile n afara bilanului (extrapatrimoniale), iar n seria 5 conturile
privind circuitul contabilitii manageriale. Seria 1 conine 7 clase, pentru:
capitaluri, active imobilizate, stocuri, teri, trezorerie, regularizare i conturi rectifi-
cative. Fiecare clas are una sau mai multe serii de conturi. Astfel, clasa Stocuri are
apte grupe: materii i materiale, obiecte de inventar; producie n curs; produse;
stocuri la teri; animale; mrfuri; ambalaje. n fiecare grup sunt conturile propriu-
zise care pot fi sintetice de ordinul 1 (alctuite din trei cifre) sau de ordinul 2 (patru
cifre). Fiecare sintetic de ordinul 2 aparine unui sintetic de ordinul 1. Tabelul 7.1
indic o poriune din planul de conturi al firmei X. Orice tip cont sintetic poate fi
descompus pe conturi analitice, n funcie de specificul i interesele ntreprinderii.
Tabel 7.1. Extras din planul de conturi al unei firme
Simbol cont TipCont Denumire cont

Clasa: Conturi de CAPITALURI


...
101 pasiv Capital social
1011 pasiv Capital social subscris ne-vrsat
1012 pasiv Capital social subscris vrsat
...
211 activ Terenuri i amenajri
2111 activ Terenuri
2112 activ Amenajri
....
301 activ Materii prime
301.01 activ Nisip
301.02 activ Ciment
301.03 activ Var
301.09 activ Alte materii prime
...
308 bifuncional Diferene de pre la materii prime
...
401 pasiv Furnizori
401.01 pasiv Furnizor A
401.02 pasiv Furnizor B
401.99 pasiv Ali furnizori
...
428 bifuncional Alte creane i datorii fa de salariai
4281 activ Alte creane fa de salariai
4282 pasiv Alte datorii fa de salariai
...
411 activ Clieni
...
4426 activ TVA deductibil
4427 pasiv TVA colectat
...
601 activ Cheltuieli cu materii prime

13 Vezi, spre exemplu, Horomnea, E. - Bazele contabilitii. Concepte, aplicaii, lexicon, Editura
Sedcom Libris, Iai, 2004, pp. 178-180
24 Capitolul 7

601.01 activ Cheltuieli cu nisipul


601.02 activ Cheltuieli cu cimentul
601.03 activ Cheltuieli cu varul
601.09 activ Cheltuieli cu alte materii prime
...

Astfel, contul 301 - Materii prime este decompus pe patru analitice. Primele trei,
301.01, 301.02 i 301.03, reprezint cele mai importante materii prime pentru firm,
iar ultimul, 301.09 le grupeaz pe toate celelalte. Mai sunt descompuse pe analitice
conturile de furnizori (401) i cheltuieli cu materiile prime (600). Interesant c un cont
sintetic de gradul I bifuncional (428) poate avea un "subordonat" de activ (4281), i
un altul de pasiv (4282) !
Din moment ce am devenit mai riguroi, este evident c, practic, putem
introduce n schem atributele SimbolCont, TipCont, DenumireCont, iar ntre
ContDebitor i SimbolCont, pe de o parte, i ContCreditor i Sim-
bolCont, pe de alt parte, avem de-a face cu o dependen de incluziune. Graful
dependenelor ia forma din figura 7.6.

Figura 7.6. Dependenele funcionale i de incluziune pentru BD CONTABILITATE


Relaia nou ce rezult din graf este evident: PLAN_CONTURI {SimbolCont,
TipCont, DenumireCont}, iar cele dou dependene de incluziune se vor
materializa la implementarea bazei n dou restricii refereniale (vezi listingul 7.18
de pe pagina web).
Revenind la restricia pe care am definit-o i caracterizat-o drept fundamental
ntr-o aplicaie destinat contabilitii generale, dac avem n vedere structura
arborescent a planului de conturi, putem reformula cerina astfel: orice cont
debitor i creditor ce apare ntr-o nregistrare contabil trebuie s fie "frunz", adic
nu poate avea conturi (sintetice de ordinul 2 sau analitice) subordonate. Aadar,
declanatoarele de inserare i modificare ale tabelei DETALII_OPERAIUNI
trebuie s verifice calitatea de frunz pentru ContDebitor i ContCreditor.
Continum nu doar declanatorul de inserare pe care l-am conturat iniial n
paragraful 7.2.2 - vezi listing 7.19.
Listing 7.19. Un declanator de inserare nou-nou pentru tabela DETALII_OPERAIUNI
CREATE OR REPLACE TRIGGER trg_do_ins
AFTER INSERT ON detalii_operatiuni FOR EACH ROW
DECLARE
Proiectarea bazelor de date 25

v_nr NUMBER(3) := 0 ;
BEGIN
-- se numara cte conturi ncep cu simbolurile contului
-- debitor, pentru a vedea daca acesta se descompune
SELECT COUNT(*) INTO v_nr FROM plan_conturi
WHERE SUBSTR(SimbolCont,1,LENGTH(:NEW.ContDebitor))
= :NEW.ContDebitor ;
IF v_nr > 1 THEN
RAISE_APPLICATION_ERROR(-20194,
'EROARE ! ContDebitor nu este FRUNZA !!!') ;
END IF ;

-- se numara cte conturi ncep cu simbolurile contului


-- creditor, pentru a vedea daca acesta se descompune
SELECT COUNT(*) INTO v_nr FROM plan_conturi
WHERE SUBSTR(SimbolCont,1,LENGTH(:NEW.ContCreditor))
= :NEW.ContCreditor ;
IF v_nr > 1 THEN
RAISE_APPLICATION_ERROR(-20195,
'EROARE ! ContCreditor nu este FRUNZA !!!') ;
END IF ;

UPDATE operatiuni
SET nrconturidebitoare = NVL(nrconturidebitoare, 1) +
CASE WHEN :NEW.contdebitor <>
NVL(uncontdebitor,:NEW.contdebitor)
THEN 1 ELSE 0 END,
nrconturicreditoare = NVL(nrconturicreditoare, 1) +
CASE WHEN :NEW.contcreditor <>
NVL(uncontcreditor,:NEW.contcreditor)
THEN 1 ELSE 0 END,
uncontdebitor = :NEW.contdebitor,
uncontcreditor = :NEW.contcreditor
WHERE idoperatiune = :NEW.idoperatiune ;
END ;

Rezolvarea poate fi ameliorat, dac de gndim c fiecare inserare a unei linii n


DETALII_OPERAIUNI atrage dup sine dou rnduri de scanare (pentru
numrare) a ntregului plan de conturi. Avem deja o oarecare experien n lucrul
cu atribute redundate, aa nct cel mai la ndemn ar fi s adugm n
PLAN_CONTURI un cmp de tip BOOLEAN (logic) pe care s-l numim chiar
EsteFrunz, astfel nct noua structur a tabelei va fi PLAN_CONTURI
{SimbolCont, TipCont, DenumireCont, EsteFrunz}. Soluia Oracle este ns:

ALTER TABLE plan_conturi ADD (EsteFrunza CHAR(1)


DEFAULT 'D' CHECK (EsteFrunza IN ('D', 'N'))) ;

Deoarece, din pcate, n Oracle un atribut nu poate avea tipul BOOLEAN, aa ca


am recurs la tipul CHAR(1), limitnd valorile la 'D' (da) i 'N' (nu).
Declanatoarele tabelei vor "veghea" la corectitudinea acestui atribut. Astfel,
cnd se insereaz o linie n PLAN_CONTURI se verific dac noul cont nu este este
sintetic de ordinul II sau analitic. Dac da (ex. 301.01), se verific dac superiorul
26 Capitolul 7

su (301 n exemplul nostru) era "frunz" nainte de inserare. n caz c exist mcar
o nregistrare contabil n care "superiorul" apare pe debit sau pe credit, inserarea
n planul de conturi este interzis, ntruct este imposibil de revenit asupra notelor
contabile deja existente i a le "re-conta" (vezi listing 7.20).
Listing 7.20. Declanatorul de inserare n PLAN_CONTURI
CREATE OR REPLACE TRIGGER trg_pc_ins
BEFORE INSERT ON plan_conturi FOR EACH ROW
DECLARE
v_parinte VARCHAR(15) ;
v_gata BOOLEAN := FALSE ;
v_nr NUMBER(3) := 0 ;
BEGIN
IF LENGTH(:NEW.SimbolCont) > 3 THEN
--contul este sintetic de ordin 2 sau analitic

-- se ncearca gasirea parintelui, taind cte un caracter


v_parinte := :NEW.SimbolCont ;
WHILE v_gata=FALSE AND LENGTH(v_parinte)>3 LOOP
v_parinte := SUBSTR(v_parinte,1, LENGTH(v_parinte) - 1);
SELECT COUNT(*) INTO v_nr FROM plan_conturi
WHERE SimbolCont=v_parinte ;
IF NVL(v_nr,0) = 1 THEN
v_gata := TRUE ;
EXIT ;
END IF ;
END LOOP ;
IF v_gata THEN
-- exista un parinte pentru noul cont
-- testam daca parintele este frunza
SELECT COUNT(ContDebitor)+ COUNT(ContCreditor)
INTO v_nr FROM detalii_operatiuni
WHERE ContDebitor=v_parinte OR ContCreditor=v_parinte ;
IF NVL(v_nr,0) >= 1 THEN
RAISE_APPLICATION_ERROR (-20196,
'EROARE ! Contul parinte apare deja in operatiuni,'||
' iar noul cont nu mai poate fi adaugat !!!') ;
END IF ;
UPDATE plan_conturi SET EsteFrunza = 'N'
WHERE SimbolCont = v_parinte ;
END IF;
END IF ;
:NEW.EsteFrunza := 'D' ;
END ;

Acum tim c, atunci cnd adugm un cont, se actualizeaz automat atributul


EsteFrunz pentru contul printe. Pentru asigurarea corectitudinii valorilor
acestui atribut, trebuie realizate i declanatoarele modificare i tergere.
Trecem acum la declanatoarele tabelei DETALII_OPERAIUNI. Mai nti,
crem o funcie (listing 7.21) care primete drept parametru simbolul unui cont i
returneaz valoarea atributului EsteFrunz pentru contul respectiv.
Listing 7.21. Funcia PL/SQL F_ESTE_FRUNZA
Proiectarea bazelor de date 27

CREATE OR REPLACE FUNCTION f_este_frunza (simbol_cont VARCHAR2)


RETURN BOOLEAN
AS
v_frunza CHAR(1) ;
BEGIN
SELECT EsteFrunza INTO v_frunza FROM plan_conturi
WHERE SimbolCont = simbol_cont ;
IF v_frunza = 'D' THEN
RETURN TRUE ;
ELSE
RETURN FALSE ;
END IF ;
END ;

Sarcina declanatoarelor se simplific sensibil. Iat-l pe cel de inserare (listing


7.22).
Listing 7.22. Noul declanator de inserare n DETALII_OPERAIUNI
CREATE OR REPLACE TRIGGER trg_do_ins
AFTER INSERT ON detalii_operatiuni FOR EACH ROW
DECLARE
v_nr NUMBER(3) := 0 ;
BEGIN
IF f_este_frunza (:NEW.ContDebitor)=FALSE OR
f_este_frunza (:NEW.ContCreditor)=FALSE THEN
-- EROARE! Cel putin unul dintre conturile
-- de pe debit sau credit nu este FRUNZA !!!
RAISE_APPLICATION_ERROR(-20194,
'EROARE ! Contul de pe debit sau credit nu este FRUNZA !!!') ;
END IF ;
UPDATE operatiuni
SET nrconturidebitoare = NVL(nrconturidebitoare, 1) +
CASE WHEN :NEW.contdebitor <>
NVL(uncontdebitor,:NEW.contdebitor)
THEN 1 ELSE 0 END,
nrconturicreditoare = NVL(nrconturicreditoare, 1) + CASE
WHEN :NEW.contcreditor <>
NVL(uncontcreditor,:NEW.contcreditor)
THEN 1 ELSE 0 END,
uncontdebitor = :NEW.contdebitor,
uncontcreditor = :NEW.contcreditor
WHERE idoperatiune = :NEW.idoperatiune ;
END ;

Proiecte vechi i noi


Am recurs la un exemplu privind proiectele derulate ntr-o firm cu ocazia
paragrafului 5.4, pentru a ilustra forma normalizat Boyce-Codd. Acum, ns, ne
apropiem de realitatea dintr-o companie, lund n calcul urmtoarele elemente:
- un angajat, identificat prin Marc este inclus n schema unui
compartiment funcional (Contabilitate, Marketing etc.);
- orice compartiment are un singur ef (Marcef);
28 Capitolul 7

- firma, datorit specificului activitii sale, lucreaz pe baz de proiecte;


fiecare proiect are un identificator (IdProiect), demareaz la o anumit
dat (DatProiect) i are un termen de realizare (Termen);
- un proiect este alctuit din una sau mai multe activiti (IdActivitate),
o activitate avnd o titulatur (DenActivit) i o scurt descriere
(DescActivit);
- orice angajat este capabil s desfoare una sau mai multe activiti;
- un angajat poate lucra la mai multe proiecte;
- la un proiect sunt angajate una sau mai multe persoane;
- o activitate poate fi inclus n unul sau mai multe proiecte;
- n cadrul unui proiect, fiecare activitate ncepe la o anumit dat (Data-
nceput), se finalizeaz la o alt dat (DataFinal) i const ntr-o serie
de operaii, responsabiliti i rezultate (Detalii);
- o aceeai persoan poate desfura, n acelai proiect, una, dou sau mai
multe activiti;
- o activitate ntr-un proiect poate fi desfurat de mai multe persoane.
Dependenele corepunztoare acestor cerine pot fi reprezentate sub form de
graf ca n figura 7.7.

Figura 7.7. Dependenele bazei de date PROIECTE


Paragraful 5.5.3 ne-a introdus ntr-una dintre cele mai delicate probleme ale
normalizrii, i anume imposibilitatea de a prelua sub form de dependene
anumite relaii semantice. i graful de mai sus are cteva asemenea dureri de cap.
Mai precis, exist o serie de informaii care nu pot fi furnizate de schema construit
pe baza decuprii relaiilor:
care sunt activitile pe care le poate desfura o persoan ?
n ce proiecte a fost implicat un angajat ?
care sunt persoanele implicate ntr-un anumit proiect ?
care este activitatea, sau activitile, desfurate de o anumit persoan
ntr-un proiect dat ?
care sunt persoanele care au desfurat o anumit activitate ntr-un anumit
proiect ?
Proiectarea bazelor de date 29

Sugestia din paragraful 5.5.3 era de a introduce n schema bazei i alte atribute
care s "fixeze" toate legturile neprinse n dependene. Astfel, un angajat poate fi
"legat" de o anumit activitate pe care o poate desfura printr-un atribut nou,
Competene, care s sugereze abilitile i experiena angajatului n privina
activitii cu pricina: (Marc, IdActivitate) Competene. Pe de alt parte,
participarea unui angajat ntr-o activitate din cadrul unui proiect poate fi descris
cu ajutorul a trei atribute - Atribuii, Probleme i Rezultate - care indic ce
sarcini a avut angajatul legat de acea activitate din cadrul proiectului, ce probleme
a ntmpinat i care au fost rezultatele muncii sale. Dependenele sunt evidente:
(Marc, IdProiect, IdActivitate) Atribuii, (Marc, IdProiect,
IdActivitate) Probleme i (Marc, IdProiect, IdActivitate)
Rezultate. Noul graf s-ar prezenta ca n figura 7.8, noile atribute fiind scrise
"aplecat" (italic).

Figura 7.8. Noul graf al dependenelor BD PROIECTE


Relaiile decupate din graf sunt urmtoarele:
PERSONAL {Marc, Numepren, Adres, Compartiment}

COMPARTIMENTE {Compartiment, Marcef}

ACTIVITI {IdActivitate, DenActivit, DescrActivit}

COMPETENE {Marc, IdActivitate, Competene}

PROIECTE {IdProiect, TitluProiect, MarcefProiect, DatLansare,


Termen}

ACTIVITI_PROIECTE {IdProiect, IdActivitate, Datanceput, DataFi-


nal, Detalii}

PERSOANE_ACTIVITI_PROIECTE {IdProiect, IdActivitate, Marc,


Atribuii, Probleme, Rezultate}
30 Capitolul 7

Alte frunze, dar aceleai conturi


Soluiei la care am ajuns n acest paragraf pentru schema bazei de date CONTA-
BILITATE i se poate reproa, printre altele, faptul c, uneori, la inserarea unei linii
n PLAN_CONTURI trebuie modificate alte linii din aceeai tabel, ceea ce nu este
posibil n toate SGBD-urile. Iar dac la inserare lucrurile mai pot funciona,
declanatorul de modificarea are anse considerabile s se mpotmoleasc.
Pentru a ncerca s ajungem la urmtoarea soluie, pornim de la dependenele
coninute n graful din figura 7.6. Dependenele de incluziune dintre ContDebi-
tor, pe de o parte, i ContCreditor, pe de alt parte, i SimbolCont sunt
discutabile. De fapt, restricia este ca toate conturile debitoare i creditoare din
orice nregistrare contabil s fie elementare (frunze), adic s nu fie descompuse
pe analitice (sau sintetice de ordinul 2). Putem defini, deci, un atribut denumit
ContElementar care identific (printr-o valoarea boolean TRUE sau ir de
caractere "Da") un cont "frunz". Putem vorbi de o "specializare" a conturilor, cele
dou dependene de incluziune iniiale fiind acum ContDebitor ContEle-
mentar, respectiv ContCreditor ContElementar, la care se adaug ContE-
lementar SimbolCont. Spre deosebire de celelate dou, ultima DI indic o
specializare. Noul graf al dependenelor este cel din figura 7.9.

Figura 7.9. Trei dependene de incluziune, dintre care una semnific o specializare
Decupnd relaiile din graf, obinem:

PLAN_CONTURI {SimbolCont, TipCont, DenumireCont}

CONTURI_ELEMENTARE {ContElementar }

NOTE_CONTABILE {IdNotContabil, Data, ExplicaiiNot}


Proiectarea bazelor de date 31

OPERAIUNI {IdOperaiune, IdNotContabil, ExplicaiiOp, NrContu-


riDebitoare, NrConturiCreditoare, UnContDebitor, UnContCreditor}

DETALII_OPERAIUNI {IdOperaiune, ContDebitor, ContCreditor,


Suma}.

Lucrurile sunt chiar interesante, deoarece acum restriciile refereniale se


stabilesc astfel:
- ntre DETALII_OPERAIUNI.ContDebitor (copil) i CONTURI_ELE-
MENTARE.ContElementar (printe);
- ntre DETALII_OPERAIUNI.ContCreditor (copil) i CONTURI_ELE-
MENTARE.ContElementar (printe);
- ntre CONTURI_ELEMENTARE.ContElementar (copil) i PLAN_CON-
TURI.SimbolCont.
Prin urmare, specializarea se poate traduce, n acest caz, printr-o nou relaie cu
un singur atribut. Listingul 7.23 pentru re-crearea bazei de date CONTABILITATE
poate fi descrcat de la adresa web menionat cu alte prilejuri.
i declanatoarele tabelei PLAN_CONTURI se simplific n mare msur.
Pentru comparaie, l vom discuta doar pe cel de inserare. Dar, mai nainte, s
crem o funcie (listing 7.24) care s primeasc drept parametru un cont i care s
returneze TRUE dac acest cont apare mcar ntr-o operaiune contabil (pe debit
sau credit) i FALSE n caz contrar:
Listing 7.24. Funcia F_APARE
CREATE OR REPLACE FUNCTION f_apare (
simbol_cont plan_conturi.SimbolCont%TYPE)
RETURN BOOLEAN
AS
v_unu NUMBER(1) ;
BEGIN
SELECT 1 INTO v_unu FROM dual WHERE EXISTS (
SELECT 1 FROM detalii_operatiuni
WHERE ContDebitor=simbol_cont OR ContCreditor=simbol_cont);
RETURN TRUE ;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN FALSE ;
END ;

Funcia ne este de folos n declanatorul de inserare n PLAN_CONTURI


(listing 7.25) pentru a verifica dac printele contului inserat a fost debitat sau
creditat n operaiuni contabile:
Listing 7.25. Noul declanator de inserare al PLAN_CONTURI
CREATE OR REPLACE TRIGGER trg_pc_ins
AFTER INSERT ON plan_conturi FOR EACH ROW
DECLARE
v_parinte VARCHAR(15) ;
v_gata BOOLEAN := FALSE ;
32 Capitolul 7

v_nr NUMBER(3) := 0 ;
BEGIN
v_parinte := SUBSTR(:NEW.SimbolCont,1, LENGTH(:NEW.SimbolCont)-
1);

WHILE LENGTH(v_parinte) > 3 LOOP


DELETE FROM conturi_elementare
WHERE ContElementar = v_parinte ;

IF SQL%ROWCOUNT > 0 THEN -- exista un parinte-frunza


IF f_apare (v_parinte) THEN
RAISE_APPLICATION_ERROR(-20193,
'EROARE ! Nu se poate adauga contul dorit !!!') ;
END IF ;
END IF;
v_parinte := SUBSTR(v_parinte,1,LENGTH(v_parinte)-1);
END LOOP ;
-- noul cont este frunza !!!
INSERT INTO conturi_elementare VALUES (:NEW.SimbolCont) ;
END ;

Cu noua structur se schimb i funcia f_este_frunz: - vezi listing 7.26.


Listing 7.26. Noul coninut al funciei F_ESTE_FRUNZ
CREATE OR REPLACE FUNCTION f_este_frunza (simbol_cont VARCHAR2)
RETURN BOOLEAN
AS
v_unu NUMBER(1) ;
BEGIN
SELECT 1 INTO v_unu FROM DUAL WHERE EXISTS
(SELECT 1 FROM conturi_elementare
WHERE ContElementar = simbol_cont) ;
RETURN TRUE ;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN FALSE ;
END ;

n schimb, declanatorul de inserare n DETALII_OPERAIUNI poate rmne


neschimbat.

7.4. Caz practic - centru de nchiriere casete video


Dei nu ntotdeauna la zi cu legislaia pentru protecia drepturilor de autor,
centrele de nchiriere de casete video au constituit o afacere tentant, mai ales n
anii '90 i, am putea spune, i are nc clientela sa, dei HBO-ul, n general,
televiziunea prin cablu constituie un concurent foarte puternic. Pentru a-l putea
diferenia de concuren, vrem ca baza de date a centrului pentru care lucrm s
ofere i date care s ajute la fidelizarea clienilor:
- informaii despre filme: genul de film, distribuie, realizare (scenariu,
regie) etc.;
Proiectarea bazelor de date 33

- premiile importante luate de filme: premiul, categoria, anul decernrii;


- date despre mprumuturi, restituiri i ntrzieri;
- informaii statistice, de genul: filmele cele mai cerute, clienii cei mai fideli,
clienii cu cele mai mari ntrzieri la returnarea casetelor;
- date despre clieni: zi de natere/onomastic, genuri preferate i, implicit,
structura pe vrste i ocupaii ale clienilor;
- un sistem de evaluare (rating, n termeni elevai) a filmelor de ctre clieni;

Ceea ce am discutat despre baza de date FILMOGRAFIE n capitolele anterioare


ne este deci de mare folos pentru cele ce urmeaz. n privina atributelor surogat,
de la nceput este evident c am avea nevoie de cel puin trei:
IdFilm - pentru identificarea fr ambiguitate a unui film;
IdCaset - un numr unic pentru diferenierea ntre ele a casetelor;
Idmprumut - numr atribuit fiecrui mprumut de una sau mai multe
casete, la un moment oarecare.
Pentru identificarea clienilor am putea s considerm codul numeric personal
(CNP-ul) ca fiind mulumitor.

Din paragraful 6.2 ne-au rmas cteva dependene memorabile, unele funci-
onale:
(1) IdFilm TitluOriginal
(2) IdFilm TitluRO
(3) IdFilm AnLans
(4) DenPremiu LocDecernare
(5) (IdFilm, Rol) Actor
(6) (DenPremiu, Categorie, IdFilm) AnPremiu.
(7) (DenPremiu, Categorie, IdFilm, Actor) AnPremiu

iar altele multi-valorice:


IdFilm Productor | Regizor
IdFilm Productor | Gen sau IdFilm Regizor | Gen
Adugm i scenaristul, aa c:
IdFilm Productor | Scenarist sau IdFilm Regizor | Gen

Caseta este identificat de atributul IdCaset, astfel c putem scrie:


(8) IdCaset DataCumprrii
(9) IdCaset ProductorCaset
(10) IdCaset AnProdCaset
(11) IdCaset PreCumprare

Anticipnd o cerere mai mare, un film poate fi comprat n mai multe exempla-
re, deci pe mai multe casete: IdFilm / IdCaset. Nici reciproca nu e prea
valabil, ntruct pe o caset pot fi adunate filme de mai mic dimensiune, scurt
34 Capitolul 7

metraje (doar trei exemple: filmuleele cu Stan i Bran, Chaplin i Tom i Jerry):
IdCaset / IdFilm.
Pentru a putea gestiona coerent toate fimele (scurt metraje sau nu) de pe o
caset, apelm la un atribut de genul FilmNr, adic numrul de ordine al filmului
de pe o caset (v amintii de atributul Linie pentru facturi ?). Aa c:
(12) (IdCaset, FilmNr) IdFilm

Am zice c problema raportului filme-casete este tranat. Da' de unde !


Persoanele mai simitoare poate-i amintesc de febra pre-telenovelist declanat
de un film epopee - Pasrea Spin. Ei bine, ca i alte filme-maraton, acesta se ntinde
pe mai multe casete. Astfel nct, spre exemplu, pe o caset poate fi partea a patra a
filmului XYZ. Din fericire, dependena de mai sus nu este compromis, ns am
avea nevoie de un atribut adiional - ParteFilm, iar dependena s-ar putea scrie:
(13) (IdCaset, FilmNr) ParteFilm

Ba chiar putem fi siguri c, pe o caset, casa productoare poate s introduc


ultima parte a unui film plus un scurt-metraj de acelai regizor sau un medalion
actoricesc etc.

Despre clieni, numai de bine:


(14) CNPClient NumeClient
(15) CNPClient AdresaClient
(16) CNPClient TelefonClient
(17) CNPClient DataNatereClient
(18) CNPClient NivelStudiiClient

Un client poate mprumuta una sau mai multe casete. Fiecare mprumut este
identificat printr-o cheie surogat - Idmprumut, aa c:
(19) Idmprumut DataOraImprumut
(20) Idmprumut CNPClient

Deoarece simultan se pot mprumuta mai multe casete, Idmprumut /


IdCaset, ceea ce e destul de nelinititor, atta vreme ct obiectivul central al
aplicaiei este de a gestiona mprumuturile de casete. Firete, prima tentanie este
de a recurge la un atribut suplimentar, ca n cazul liniilor din facturi, atribut creia
i putem spune chiar NrCrtmpr (adic un soi de numr curent al mprumutului,
numr care indic a cta caset din mprumut este cea curent), aa c, cu un pic de
crpeal, problema s-ar rezolva:
(Idmprumut, NrCrtmpr) IdCaset

Dac adugm c restituirea casetelor nu este ntotdeauna simultan, adic un


client poate mprumuta trei casete ntr-o smbt diminea i s returneze una
smbt seara i celelalte dou duminic, se poate scrie, fie (Idmprumut,
Proiectarea bazelor de date 35

NrCrtmpr) DataOraRestituirii, fie (Idmprumut, IdCaset)


DataOraRestituirii. Eu, unul, a alege varianta din urm.
Ei bine, dac privim mai atent ultima dependen, observm c, la o adic,
aceasta ar face de prisos atributul NrCrtmpr. Diferena dintre varianta folosirii
atributului NrCrtmpr (varianta 1) i cea fr (a doua) este ilustrat n figura 7.10.

Figura 7.10. Dou variante de dependene pentru mprumuturi i restituiri


Fiecare i are avantajele i dezavantajele sale. Prima pare mai complicat i,
totodat, are un uor aer de artificialitate indus de folosirea NrCrtmpr. Cel mai
important avantaj al su ine de faptul c preia foarte bine succesiunea temporal
mprumut (prima DF cu sursa compus) - restituire (a doua). Consecina esenial
este c n schem nu apar valori nule. Graful din dreapta figurii este mult mai
simplu. Din momentul mprumutului pn n cel al restituirii valoarea atributului
DataOraRestuirii ar fi NULL. Astfel, pentru a afla n orice moment casetele
aflate la clieni, condiia n SQL ar fi DataOraRestuirii IS NULL. Tocmai adversarii
folosirii valorilor nule ar dezaproba aceast a doua soluie. Noi, ns, nefiind
dumani (dar nici prieteni) ai nulitii n bazele de date relaionale, vom opta
pentru aceast a doua soluie, chiar dac lenea a cntrit mult n alegere. Aadar:
(21) (Idmprumut, IdCaset) DataOraRestituirii

S adunm tot ce-am discutat pn acum ntr-un graf. Figura 7.11 este reprezen-
tare destul de bun a ansamblului de dependene funcionale (1)-(21), plus cele
multivaloare. Este drept, aglomeraia este cam mare, dar arta cere sacrificii.
36 Capitolul 7

Figura 7.11. Graful DF pentru BD Centru de nchiriere - versiunea 1.0


Decupm relaiile din graf:

PREMII_DENUMIRI {DenPremiu, LocDecernare}

FILME {IdFilm, TitluOriginal, TitluRO, AnLans}

DISTRIBUIE {IdFilm, Rol, Actor}

PREMII_FILME {IdFilm, DenPremiu, Categorie,AnPremiu}

PREMII_INTERPRETARE {IdFilm, Actor , DenPremiu, Categorie, AnPre-


miu}

PRODUCTORI {IdFilm, Productor}

REGIZORI {IdFilm, Regizor}

SCENARITI {IdFilm, Scenarist}

FILME_GENURI {IdFilm, Gen}

CASETE {IdCaset, DataCumprrii, ProductorCaset, AnProdCaset,


PreCumprare}
Proiectarea bazelor de date 37

CASETE_FILME {IdCaset, FilmNr, IdFilm, ParteFilm}

CLIENI {CNPClient, NumeClient, AdresaClient, TelefonClient, Da-


taNateriiClient, NivelStudiiClient}

MPRUMUTURI {Idmpr, DataOrampr, CNPClient}

CASETE_MPRUMUTATE {Idmpr, IdCaset, DataOraRestituirii}

Schema pare a rspunde necesarului informaional pe care ni-l propusesem


iniial. Astfel, putem afla: numrul de casete mprumutate de un client pe o
anumit perioad; ponderea comediilor ntre filmele nchiriate de un client, sau pe
tot centrul; ce genuri de filme prefer tinerii ntre 30 i 35 de ani cu studii medii etc.
S ne ocupm ns de regulile scrise sau nescrise ale centrului:
un client nu poate mprumuta mai mult de patru casete simultan !
la fiecare 10 casete mprumutate, un client are dreptul la o caset
mprumutat gratuit;
mprumutul este pentru 48 de ore; la remiterea casetei se calculeaz o
penalizare de 75% din preul de nchiriere al casetei pentru fiecare zi de
ntrziere; iat i tarifele zilnice la nchiriere:
o 50 000 lei pentru casetele produse n anul calendaristic curent i cel
precedent;
o 40 000 lei pentru casetele produse n urm cu doi i trei ani;
o 30 000 lei pentru restul.
pierderea sau distrugerea unei casete antreneaz o amend care reprezint
dublul preului casetei.

Prima regul s-ar implementa n schema actual a bazei de maniera urmtoare:


atunci cnd n tabela CASETE_MPRUMUTATE se introduce o linie nou, pentru
care valorile ar fi notate cu (:NEW.IdImpr, :NEW.IdCaseta, NULL), dup un calapod
similar Oracle, se numr, pentru clientul aferent mprumutului curent, cte dintre
casetele mprumutate au valoarea atributului DataOraRestituirii nul:

SELECT COUNT(*)
INTO v_CaseteNerestituite
FROM casete_imprumutate c INNER JOIN imprumuturi i
ON c.idimpr=i.idimpr
WHERE DataOraRestituirii IS NULL AND CNPClient IN
(SELECT CNPClient
FROM imprumuturi
WHERE idimpr = :NEW.idimpr)

Ulterior, valoarea variabilei se testeaz dac este mai mic dect patru, caz n
care inserarea este permis; altminteri, inserarea se respinge. Fraza SELECT de mai
sus poate fi folosit n formularul de lucru (care ruleaz pe platforma client), ntr-o
38 Capitolul 7

secvena de cod de pe serverul de aplicaii (dac arhitectura este de tip Web, adic
pe trei sau mai multe straturi), dar cel mai sigur ar fi s se recurg la un
declanator pe tip INSERT pentru aceast tabel. Bineneles, numai ca SGBD-ul
folosit s "suporte" declanatoare (nu este cazul MySQL-ului, din pcate, dect n
cteva versiuni comerciale).
Ei bine, recomandarea noastr este nu numai s se foloseasc un SGBD ce
permite lucrul cu declanatoare, dar s se introduc un atribut redundant numit
NrCaseteNerestituite, atribut actualizabil la declanatoarele de inserare,
modificare i tergere n CASETE_MPRUMUTATE. Acest atribut ar depinde de
CNPClient: CNPClient NrCaseteNerestituite.
Ba chiar am putea exagera i mai mult, folosind dou atribute dependente de
CNPClient, unul NrCasetemprumutate i altul NrCaseteRestituite.
Paradoxal, dei ideea pare deplasat, exist argumente n favoarea sa. Iar, ntruct
a doua regul spune c la fiecare 10 casete mprumutate, un client are dreptul la o caset
mprumutat gratuit chiar ne convin ambele atribute, aa c, n dezaprobarea publi-
cului, ne precipitm i scriem:
(22) CNPClient NrCasetemprumutate
(23) CNPClient NrCaseteRestituite

Mecanismul de actualizare este unul comun pentru utilizatorii de declanatoare


(triggere): NrCasetemprumutate se incrementeaz la inserarea unei linii n
CASETE_MPRUMUTATE, iar NrCaseteRestituite la modificarea unei linii
n CASETE_MPRUMUTATE, atunci cnd condiia ndeplinit este :OLD.DataO-
raRestituirii IS NULL AND :NEW.DataOraRestituirii IS NOT NULL.
Explicaia condiiei este una simpl: restituirea unei casete mprumutate se
consemneaz n baza noastr de date prin, s-i zicem, "de-nulizarea" atributului
CASETE_MPRUMUTATE.DataOraRestituirii.
A doua regul e centrului reprezint o tentativ de a fideliza clienii, mai ales pe
cei dependeni: la 10 casete nchiriate, a 11-a este mprumutat gratuit. Noroc de
atributul NrCasetemprumutate de mai sus, pentru c, n condiiile n care
acesta este actualizat corect, putem ti n orice moment dac se cuvine s acordm
gratuitatea sau nu. Pentru a cunoate, totui, n timp, cte casete au fost
mprumutate cu titlu gratuit, am putea recurge la un atribut nou, Gratuit, care
s ia valorile D (da) sau N i s depind funcional astfel:
(24) (Idmprumut, IdCaset) Gratuit

A treia regul e ceva mai dur: mprumutul este pentru 48 de ore; la remiterea
casetei se calculeaz o penalizare de 75% din preul casetei pentru fiecare zi de ntrziere.
Implementarea sa ar presupune c, la "de-nulizarea" valorii unui atribut DataOra-
Restituirii s verificm dac diferena CASETE_MPRUMUTATE.DataO-
raRestituirii - MPRUMUTURI.Datampr este mai mare de dou zile
(ntruct cele dou atribute sunt de tip DATETIME, adic dat i or, diferena lor
furnizeaz, de obicei, numrul de zile dintre cele dou momente). Dac da, atunci
calculm penalizarea dup relaia: Penalizare := 0.75 * (CASETE_MPRU-
Proiectarea bazelor de date 39

MUTATE.DataOraRestituirii - MPRUMUTURI.Datampr -2). Este


evident nevoia de atributul Penalizare care va depinde funcional astfel:
(25) (Idmprumut, IdCaset) Penalizare

Cu aceast ocazie, ne putem propune s rezolvm o problem pe care trebuia s-


o avem n vedere mai demult, i anume Ct trebuie s achite clientul la fiecare
mprumut (nchirierea a una, dou, trei sau patra casete) ? Pentru acest scop apelm la
atributul Valoarenchir (de la valoare nchiriere):
(26) Idmprumut Valoarenchir

n fine, ultima regul, potrivit creia pierderea sau distrugerea unei casete
antreneaz o amend care reprezint dublul preului casetei, are o dubl implicaie: pe
de o parte, regretabilul eveniment trebuie consemnat la (un soi de) restituire, iar
atributul Penalizare va conine acum dublul contravalorii casetei; pe de alt
parte, trebuie s existe cumva n baza de date o indicaie precum c respectiva
caseta nu mai poate fi nchiriat, adic este pierdut sau distrus. Soluia tergerii
nregistrrii corespunztoare din tabela CASETE nu este una prea ingenioas,
ntruct ar trebui s pierdem toi "copiii" acestei nregistrri, deci inclusiv de cte
ori i cui a fost mprumutat, i, astfel, informaiile statistice privind filmele, clienii
i mprumuturile vor fi serios afectate. Cel mai nimerit pare s recurgem la un
atribut numit StareCaset care s aib o valoare implicit OK (sau nul, dei nu
suntem fani nulliti) i, dac este cazul, Pierdut, Distrus, ba chiar, dac tot l avem,
putem consemna i situaiile n care caseta a fost scoas din uz "de moarte bun"
(Casat) sau se apropie de aceast stare (Uzat):
(27) IdCaset StareCaset

Nu este ns suficient s cunoatem starea casetei, ci i de la ce mprumut i se


trage pierderea sa distrugerea. A c apelm la un atribut special, StareLaRes-
tituire, care indic halul n care un client ne-a restituit caseta (s fim nelei: e
vorba de halul casetei, nu al clientului !) i care depinde funcional astfel:
(28) (Idmprumut, IdCaset) StareLaRestituire

Ca un alt artificiu propus, ne imaginm c, n ton cu vremurile, clienii vor


putea s-i rezerve/comande casete pe web, iar centrul s aib un serviciu de
furnizare a casetelor la domiciliu (de tipul pizza delivery) sau, n cel mai ru caz,
datorit dimensiunii impresionante a centrului (d, Doamne !), vor fi amplasate
trei-patru terminale ntr-un col prin care clientul poate s caute filmele dup
actori, regizori, subiect etc. Tocmai pentru a evita execuia unei interogri care s
implice tabelele CASETE, MPRUMUTURI i CASETE_MPRUMUTATE prin care
s se afle dac o caset este disponibil sau mprumutat la un moment dat, se
poate recurge la un alt atribut redundant numit Disponibilitate, actualizabil
automat prin declanatoarele tabelei CASETE_MPRUMUTATE:
(29) IdCaset Disponibilitate
40 Capitolul 7

Apropo, ne propusesem i un mic mecanism de rating al filmelor de ctre


clieni, dei, din cte am vzut pe site-urile romneti dedicate vnzrilor (legale)
de carte i CD-uri, sistemul nu prea a prins la noi. La modul cel mai simplu, putem
alege o scal de punctare de la 0 la 5, de genul celei practicate de Laureniu Brtan
n revista 22, atributul Punctaj fiind "implicat" n dependena:
(30) (IdFilm, CNPClient) Punctaj.

Tot frmntndu-ne mintea cu mecanismul de cutare a informaiilor despre


filme, realizm c, la un moment dat, este posibil ca unii clieni s doreasc a
viziona filme n care joac actori spanioli, sau filme regizate de cineati nscui n
perioada 1930-1940 sau site-ul/pagina Web dedicat unei anumite personaliti
cinematografice. Putem vorbi de generalizare: actorii, regizorii i scenaritii sunt
toi cineati, ca s nu amintim numeroi actori care sunt i regizori, sau regizori ce
sunt i productori i scenariti. Aa c am putea introduce o serie de atribute
pentru identificarea i caracterizarea oricrui actor/scenarist/productor/regizor:
IdCineast, NumeCineast, DataNaterii, DataMorii, Naionalitate,
PaginWeb. Fr a schimba numele celor patru atribute (ar fi fost mai nimerite
titulaturile IdScenarist, IdProductor, IdRegizor i IdActor), reprezen-
tm cele patru dependene de incluziune Scenarist IdCineast, Productor
IdCineast, Regizor IdCineast, Actor IdCineast.
Punnd cap la cap tot ceea ca am discutat despre legturile semantice dintre
atributele bazei de date, obinem reprezentarea sub form de graf din figura 7.12.
Pentru un plus de lizibilitate, am grupat toate destinaiile surselor IdFilm,
IdCineast, IdCaset i CNPClient.
Proiectarea bazelor de date 41

Figura 7.12. Graful DF pentru BD Centru de nchiriere - versiunea 2.0


Pe baza grafului, schema final a bazei de date este urmtoarea:

PREMII_DENUMIRI {DenPremiu, LocDecernare}

FILME {IdFilm, TitluOriginal, TitluRO, AnLans}

CINEATI {IdCineast, NumeCineast, DataNaterii, DataMorii, Naio-


nalitate, PaginWeb}

DISTRIBUIE {IdFilm, Rol, Actor*}

PREMII_FILME {IdFilm, DenPremiu, Categorie,AnPremiu}

PREMII_INTERPRETARE {IdFilm, Actor* , DenPremiu, Categorie, AnPre-


miu}

PRODUCTORI {IdFilm, Productor*}

REGIZORI {IdFilm, Regizor*}

SCENARITI {IdFilm, Scenarist*}


42 Capitolul 7

FILME_GENURI {IdFilm, Gen}

CASETE {IdCaset, DataCumprrii, ProductorCaset, AnProdCaset,


PreCumprare, StareCaset, Disponibilitate}

CASETE_FILME {IdCaset, FilmNr, IdFilm, ParteFilm}

CLIENI {CNPClient, NumeClient, AdresaClient, TelefonClient, Da-


taNateriiClient, NivelStudiiClient, NrCasetemprumutate, NrCa-
seteRestituite}

MPRUMUTURI {Idmpr, DataOrampr, CNPClient, Valoarenchir}

CASETE_MPRUMUTATE {Idmpr, IdCaset, DataOraRestituirii,


Gratuit, Penalizare, StareLaRestituire }

APRECIERI_FILME {IdFilm, CNPClient, Punctaj}

Atributele marcate cu asterisc sunt chei strine, printele comun fiind cmpul
IdCineast din tabela CINEATI.

Fiind vorba de un caz practic pe care dorim s-l ducem ct mai aproape de
pnzele albe, vom urma promisiunea din deschiderea capitolul, discutnd i
cteva chestiuni de implementare a schemei propuse. ncepem cu crearea tabelelor
i definirea restriciilor de non nulitate, cheilor primare, restriciilor de integritate
referenial, precum i regulilor de validare la nivel de atribut i nregistrare vezi
listing 7.27. Atributele pentru care au fost definite restricii sunt:
AnLans din FILME (valori mai mari dect 1900);
AnPremiu din PREMII_FILME i PREMII_INTERPRETARE (valori mai
mari dect 1920);
DataCumprrii din CASETE (valori mai mari de 1 ianuarie 1998);
AnProdCaseta din CASETE (valori mai mari dect 1980);
StareCaseta din CASETE valori limitate la lista: Ok, Pierdut, Distrus,
Casat i Uzat;
Disponibilitate din CASETE numai valorile D (da) i N (nu);
FilmNr din CASETE_FILME: deoarece pe o caset nu pot ncpea mai
mult de 30 (clipuri/scurt metraje), valorile acestui atribut trebuie s se
ncadreze n plaja 1-30;
ParteFilm din CASETE_FILME: deoarece un film poate fi rupt n
maximum 30 de pri (nu credem ca vreun centru s achiziioneze pe
casete Tnr i nelinitit !), valorile acestui atribut trebuie s se ncadreze n
plaja 1-30;
Proiectarea bazelor de date 43

NrCaseteImprumutate i NrCaseteRestituite din CLIENI


valorile trebuie s fie ntregi pozitive;
StareLaRestituire din CASETE_MPRUMUTATE valori limitate la
lista: Ok, Pierdut, Distrus;
Gratuit din CASETE_MPRUMUTATE numai valorile D (da) i N
(nu);
Punctaj din APRECIERI_FILME trebuie s se ncadreze ntre 0 i 10.

Listing 7.27. Scriptul Oracle de creare a tabelelor


-- creare BD centru de inchirieri VIDEO
--
DROP TABLE aprecieri_filme ;
DROP TABLE casete_imprumutate ;
DROP TABLE imprumuturi ;
DROP TABLE clienti ;
DROP TABLE casete_filme ;
DROP TABLE casete ;
DROP TABLE premii_interpretare ;
DROP TABLE premii_filme ;
DROP TABLE premii_denumiri ;
DROP TABLE scenaristi ;
DROP TABLE regizori ;
DROP TABLE producatori ;
DROP TABLE distributie ;
DROP TABLE filme_genuri ;
DROP TABLE cineasti ;
DROP TABLE filme ;

CREATE TABLE cineasti (


IdCineast NUMBER(6) NOT NULL PRIMARY KEY,
NumeCineast VARCHAR2(40) NOT NULL,
DataNasterii DATE,
DataMortii DATE,
Naionalitate VARCHAR2(30),
PaginaWeb VARCHAR2(200),
CHECK (NVL(DataNasterii, DATE'1970-01-01') <
NVL2(DataMortii, DataNasterii, DATE'1970-01-02'))
);

CREATE TABLE filme (


IdFilm NUMBER(10) NOT NULL PRIMARY KEY,
TitluOriginal VARCHAR2(100),
TitluRO VARCHAR2(100) NOT NULL,
AnLans NUMBER(4) CHECK (AnLans > 1900)
) ;

CREATE TABLE filme_genuri (


IdFilm NUMBER(10) NOT NULL REFERENCES filme (IdFilm),
Gen VARCHAR2(25) NOT NULL,
PRIMARY KEY (IdFilm, Gen)
) ;

CREATE TABLE distributie (


44 Capitolul 7

IdFilm NUMBER(10) NOT NULL REFERENCES filme (IdFilm),


Rol VARCHAR2(30) NOT NULL,
Actor NUMBER(6) REFERENCES cineasti (IdCineast),
PRIMARY KEY (IdFilm, Rol)
);

CREATE TABLE producatori (


IdFilm NUMBER(10) NOT NULL REFERENCES filme (IdFilm),
Producator NUMBER(6) NOT NULL REFERENCES cineasti (IdCineast),
PRIMARY KEY (IdFilm, Producator)
);

CREATE TABLE regizori (


IdFilm NUMBER(10) NOT NULL REFERENCES filme (IdFilm),
Regizor NUMBER(6) NOT NULL REFERENCES cineasti (IdCineast),
PRIMARY KEY (IdFilm, Regizor)
) ;

CREATE TABLE scenaristi (


IdFilm NUMBER(10) NOT NULL REFERENCES filme (IdFilm),
Scenarist NUMBER(6) NOT NULL REFERENCES cineasti (IdCineast),
PRIMARY KEY (IdFilm, Scenarist)
);

CREATE TABLE premii_denumiri (


DenPremiu VARCHAR2(40) NOT NULL PRIMARY KEY,
LocDecernare VARCHAR2(50)
);

CREATE TABLE premii_filme (


IdFilm NUMBER(10) NOT NULL REFERENCES filme (IdFilm),
DenPremiu VARCHAR2(40) NOT NULL REFERENCES premii_denumiri
(DenPremiu),
Categorie VARCHAR2(30) NOT NULL,
AnPremiu NUMBER(4) CHECK (Anpremiu > 1920),
PRIMARY KEY (IdFilm, DenPremiu, Categorie)
);

CREATE TABLE premii_interpretare (


IdFilm NUMBER(10) NOT NULL REFERENCES filme (IdFilm),
Actor NUMBER(6) REFERENCES cineasti (IdCineast),
DenPremiu VARCHAR2(40) NOT NULL REFERENCES premii_denumiri
(DenPremiu),
Categorie VARCHAR2(30) NOT NULL,
AnPremiu NUMBER(4) CHECK (AnPremiu > 1920),
PRIMARY KEY (IdFilm, Actor, DenPremiu, Categorie)
) ;

CREATE TABLE casete (


IdCaseta NUMBER(12) NOT NULL PRIMARY KEY,
DataCumpararii DATE CHECK (DataCumpararii > DATE'1998-01-01'),
ProductorCaseta VARCHAR2(30),
AnProdCaseta NUMBER(4) DEFAULT EXTRACT (YEAR FROM CURRENT_DATE)
CHECK (AnProdCaseta > 1980),
PreCumparare NUMBER(8),
StareCaseta VARCHAR2(20) DEFAULT 'Ok' NOT NULL
Proiectarea bazelor de date 45

CHECK (StareCaseta IN ('Ok', 'Pierduta', 'Distrusa', 'Casata',


'Uzata')),
Disponibilitate CHAR(1) DEFAULT 'D' NOT NULL
CHECK (Disponibilitate IN ('D','N'))
);

CREATE TABLE casete_filme (


IdCaseta NUMBER(12) NOT NULL REFERENCES casete (Idcaseta),
FilmNr NUMBER(2) DEFAULT 1 NOT NULL
CHECK (FilmNr BETWEEN 1 AND 30),
IdFilm NUMBER(10) NOT NULL REFERENCES filme (IdFilm),
ParteFilm NUMBER(2) DEFAULT 1 NOT NULL
CHECK (ParteFilm BETWEEN 1 AND 30),
PRIMARY KEY (IdCaseta, FilmNr)
) ;

CREATE TABLE clienti (


CNPClient NUMBER(13) NOT NULL PRIMARY KEY,
NumeClient VARCHAR2(45) NOT NULL,
AdresaClient VARCHAR2(100),
TelefonClient VARCHAR2(25),
DataNasteriiClient DATE,
NivelStudiiClient VARCHAR2(15) DEFAULT 'Medii',
NrCaseteImprumutate NUMBER(6) DEFAULT 0
CHECK (NrCaseteImprumutate >= 0),
NrCaseteRestituite NUMBER(6) DEFAULT 0
CHECK (NrCaseteRestituite >= 0),
CHECK (NrCaseteImprumutate - NrCaseteRestituite BETWEEN 0 AND 4)
);

CREATE TABLE imprumuturi (


IdImpr NUMBER(15) NOT NULL PRIMARY KEY,
DataOraImpr DATE DEFAULT CURRENT_DATE,
CNPClient NUMBER(13) NOT NULL REFERENCES clienti(CNPClient),
ValoareInchir NUMBER(7) DEFAULT 0 NOT NULL ;
);

CREATE TABLE casete_imprumutate (


IdImpr NUMBER(15) NOT NULL REFERENCES imprumuturi (IdImpr),
IdCaseta NUMBER(12) NOT NULL REFERENCES casete (Idcaseta),
DataOraRestituirii DATE,
StareLaRestituire VARCHAR2(20) DEFAULT 'Ok'
CHECK (StareLaRestituire IN ('Ok', 'Pierduta', 'Distrusa')),
Gratuita CHAR(1) DEFAULT 'N' NOT NULL
CHECK (Gratuita IN ('D','N')),
Penalizare NUMBER(9) DEFAULT 0,
PRIMARY KEY (IdImpr, IdCaseta)
);

CREATE TABLE aprecieri_filme (


IdFilm NUMBER(10) NOT NULL REFERENCES filme (IdFilm),
CNPClient NUMBER(13) NOT NULL REFERENCES clienti(CNPClient),
Punctaj NUMBER(2) DEFAULT 0 NOT NULL
CHECK (Punctaj BETWEEN 0 AND 10),
PRIMARY KEY (IdFilm, CNPClient)
);
46 Capitolul 7

Regulile de validare la nivel de nregistrare prezente n script privesc


urmtoarele restricii:
n tabela CINEATI: eventuala dat a decesului s succead datei de
natere; ntruct, pe de o parte, pentru cineatii n via, DataMorii este
nul, iar, pe de alt parte, pentru o serie de cineati nu se cunoate, nc,
una sau ambele date, trebuie avute n vedere problemele ce apot aprea
din manevrarea valorilor nule; cea mai nimerit este, n acest context,
funcia NVL214;
n CLIENI, avnd n vedere c nici un client nu poate nchiria, la orice
moment dat, mai mult de patru casete, regula este una simpl: NrCase-
teImprumutate - NrCaseteRestituite BETWEEN 0 AND 4; mai
dificil va fi mecanismul de asigurare a corectitudinii celor dou atribute;

Continum implementarea n Oracle a regulilor stabilite, rezolvnd problema


cheilor surogat, care prespune folosirea secvenelor i declanatoarelor de inserare,
dup modelul din listing 7.28. Dup cum spuneam i cu alte ocazii, majoritatea
SGBD-urilor disponibile permit declararea de atribute auto-incrementate.
Listing 7.28. Secvene i declanatoare de inserare PL/SQL pentru generarea cheilor
surogat
-- autoincrementarea atributului CINEASTI.IdCineast
DROP SEQUENCE seq_IdCineast ;
CREATE SEQUENCE seq_IdCineast START WITH 1
MINVALUE 1 MAXVALUE 999999
NOCYCLE NOCACHE ORDER ;

CREATE OR REPLACE TRIGGER trg_cineasti_ins


BEFORE INSERT ON cineasti FOR EACH ROW
BEGIN
SELECT seq_IdCineast.NextVal INTO :NEW.IdCineast FROM dual ;
END;
/

-- autoincrementarea atributului FILME.IdFilm


DROP SEQUENCE seq_IdFilm ;
CREATE SEQUENCE seq_IdFilm START WITH 1
MINVALUE 1 MAXVALUE 9999999999
NOCYCLE NOCACHE ORDER ;

CREATE OR REPLACE TRIGGER trg_filme_ins


BEFORE INSERT ON filme FOR EACH ROW
BEGIN
SELECT seq_IdFilm.NextVal INTO :NEW.IdFilm FROM dual ;
END;
/

-- autoincrementarea atributului CASETE.IdCaseta

14 Pentru cteva detalii despre funcia NVL2, vezi i [Fotache s.a.03], p.190
Proiectarea bazelor de date 47

DROP SEQUENCE seq_IdCaseta ;


CREATE SEQUENCE seq_IdCaseta START WITH 10000000001
MINVALUE 1 MAXVALUE 999999999999
NOCYCLE NOCACHE ORDER ;

CREATE OR REPLACE TRIGGER trg_casete_ins


BEFORE INSERT ON casete FOR EACH ROW
BEGIN
SELECT seq_IdCaseta.NextVal INTO :NEW.IdCaseta FROM dual ;
END;
/

-- autoincrementarea atributului IMPRUMUTURI.IdImpr


DROP SEQUENCE seq_IdImpr ;
CREATE SEQUENCE seq_IdImpr START WITH 1000000000001
MINVALUE 1 MAXVALUE 99999999999999
NOCYCLE NOCACHE ORDER ;

CREATE OR REPLACE TRIGGER trg_imprumuturi_ins


BEFORE INSERT ON imprumuturi FOR EACH ROW
BEGIN
SELECT seq_IdImpr.NextVal INTO :NEW.IdImpr FROM dual ;
END;
/

Tabela CASETE_FILME conine un caz special de atribut a crui valoare trebuie


gestionat automat. Astfel, atributul FilmNr trebuie iniializat cu 1 la primul film
de pe fiecare caset, apoi trebuie incrementat cu 1 pentru orice alt film declarat
ulterior ca fiind nregistrat pe caseta respectiv. Necazul este c, la tergerea unui
film de pe o caset, trebuie re-verificat ordinea filmelor pe caseta respectiv. De
exemplu, dac pe o caset sunt 5 filme (scurt metraje) i se terge linia care se refer
la al treilea film de pe caset, atunci filmul al patrulea devine al treilea, iar al
cincilea devine al patrulea.
Iar ca delectarea s fie total, trebuie s se interzic modificarea interactiv (prin
UPDATE), att a atributului CASETE_FILME.FilmNr, ct i atributelor CASE-
TE_FILME.IdCaset i CASETE_FILME.IdFilm. Aceste ultime dou atribute
pot fi, eventual, modificate doar prin declanatoarele de tip UPDATE CASCADE la
actualizarea atributelor CASETE.IdCaset i FILME.IdFilm.
Iat n listing 6.29 cele trei declanatoare ale tabelei CASETE_FILME, mpreun
cu pachetul necesar declarrii variabilelor globale.
Listing 7.29. Un pachet i patru declanatoare PL/SQL pentru gestionarea valorilor
atributului FilmNr din CASETE_FILME
-------------------------------------------------------------------
-- declansatorul de inserare
CREATE OR REPLACE TRIGGER trg_casete_filme_ins
BEFORE INSERT ON casete_filme FOR EACH ROW
DECLARE
v_filmnr casete_filme.FilmNr%TYPE ;
BEGIN
SELECT MAX(FilmNr) INTO v_filmnr FROM casete_filme
WHERE IdCaseta = :NEW.IdCaseta ;
48 Capitolul 7

:NEW.FilmNr := NVL(v_filmnr,0) + 1 ;
END ;
/

-------------------------------------------------------------------
-- pachetul cu variabile publice
CREATE OR REPLACE PACKAGE pac_centru AS
-- variabila pentru semnalizarea locului de actualizare
-- in CASETE_FILME
vp_trg_casete_filme_del BOOLEAN := FALSE ;
vp_idcaseta casete.IdCaseta%TYPE ;
vp_filmnr casete_filme.FilmNr%TYPE ;

-- variabile pentru semnalizarea modificarilor in cascada


vp_trg_casete_upd BOOLEAN := FALSE ;
vp_trg_filme_upd BOOLEAN := FALSE ;

END pac_centru ;
/

-------------------------------------------------------------------
-- declansator de stergere LA NIVEL DE LINIE
CREATE OR REPLACE TRIGGER trg_casete_filme_del_after_row
AFTER DELETE ON casete_filme FOR EACH ROW
DECLARE
v_filmnrcrt casete_filme.FilmNr%TYPE ;
BEGIN
pac_centru.vp_trg_casete_filme_del := TRUE ;
pac_centru.vp_idcaseta := :OLD.IdCaseta ;
pac_centru.vp_filmnr := :OLD.FilmNr ;
pac_centru.vp_trg_casete_filme_del := TRUE ;
END;
/

-- declansatorul de stergere LA NIVEL DE COMANDA


CREATE OR REPLACE TRIGGER trg_casete_filme_del_stat
AFTER DELETE ON casete_filme
DECLARE
v_filmcrt casete_filme.FilmNr%TYPE ;

CURSOR c_casete_filme (idcaseta_ casete.IdCaseta%TYPE,


filmcrt_ casete_filme.FilmNr%TYPE)
IS SELECT * FROM casete_filme
WHERE IdCaseta = idcaseta_ AND FilmNr >= filmcrt_
ORDER BY FilmNr ;
BEGIN
pac_centru.vp_trg_casete_filme_del := TRUE ;
v_filmcrt := pac_centru.vp_filmnr ;

FOR rec_casete_filme IN c_casete_filme (pac_centru.vp_idcaseta,


pac_centru.vp_filmnr) LOOP
IF rec_casete_filme.FilmNr = v_filmcrt THEN
UPDATE casete_filme
SET FilmNr = 29
WHERE IdCaseta=rec_casete_filme.IdCaseta AND
FilmNr=rec_casete_filme.FilmNr ;
Proiectarea bazelor de date 49

ELSE
UPDATE casete_filme
SET FilmNr = v_filmcrt
WHERE IdCaseta = rec_casete_filme.IdCaseta AND
FilmNr=rec_casete_filme.FilmNr ;
v_filmcrt := v_filmcrt + 1 ;
END IF ;
END LOOP;

pac_centru.vp_trg_casete_filme_del := FALSE ;
END;
/

-------------------------------------------------------------------
-- declansator de modificare
CREATE OR REPLACE TRIGGER trg_casete_filme_upd
BEFORE UPDATE ON casete_filme FOR EACH ROW
DECLARE
v_filmnrcrt casete_filme.FilmNr%TYPE ;
BEGIN
IF :NEW.IdCaseta <> :OLD.IdCaseta AND
pac_centru.vp_trg_casete_upd = FALSE THEN
RAISE_APPLICATION_ERROR(-20789,
'Nu puteti modifica interactiv IdCaseta !');
END IF;
IF :NEW.IdFilm <> :OLD.IdFilm AND
pac_centru.vp_trg_filme_upd = FALSE THEN
RAISE_APPLICATION_ERROR(-20788,
'Nu puteti modifica interactiv IdFilm !');
END IF;
IF :OLD.FilmNr <> :NEW.FilmNr AND
pac_centru.vp_trg_casete_filme_del = FALSE THEN
RAISE_APPLICATION_ERROR(-20787,
'Nu puteti modifica interactiv FilmNr !');
END IF;
END;
/

Nici n-am apucat bine s ne desprim de tratarea cheilor surogat, c ne i


ntoarcem la ele, de data aceasta punndu-ne problema actualizrii lor. n mod
obinuit, ntr-o restricie referenial modificarea valorii cheii primare (alternative)
atrage dup sine necesitatea modificrii automate a valorilor tuturor cheilor strine
ale acesteia. n stardardele SQL i n unele SGBD-uri exist, la creare, opiunea ON
UPDATE CASCADE. Oracle, ns, este mai zgrcit n privina asta, aa c
operaiunea de modificare n cascad trebuie ntreprins prin declanatoarele de
modificare ale tabelei printe.
Dac n cazul atributui CLIENTI.CNPClient nu se ntrezrete nici o
problem, pentru tabelele ce folosesc chei surogate gestionate prin secven apare
un necaz. S presupunem c n tabela CINEATI, ultima linie introdus are valoa-
rea IdCineast egat cu 344, iar secvena SEQ_IDCINEAST s-a oprit pe aceast
poziie. n acest moment se lanseaz o comand UPDATE prin care Id-ul 256 este
fcut 345:
50 Capitolul 7

UPDATE cineasti SET IdCineast = 345 WHERE IdCineast=256;

Necazul apare la urmtoarea comand INSERT n CINEATI, deoarece


valoarea secretat de secven se suprapune pentru valoarea stabilit prin
UPDATE, iar restricia de cheia primar va fi nclcat. De aceea, n
declanatoarele de modificare n cascad a atributelor cheilor surogat se va testa n
prealabil dac noua valoare depete limita actual a secvenei vezi listing 7.30.
Listing 7.30. Declanatoarele pentru propagarea modificrilor cheilor primare n tabelele
copil
-------------------------------------------------------------------
CREATE OR REPLACE TRIGGER trg_cineasti_upd
AFTER UPDATE OF IdCineast ON cineasti FOR EACH ROW
DECLARE
v_urmatorulid cineasti.IdCineast%TYPE ;
BEGIN
SELECT last_number INTO v_urmatorulid
FROM user_sequences
WHERE sequence_name='SEQ_IDCINEAST' ;
IF :NEW.IdCineast >= v_urmatorulid THEN
RAISE_APPLICATION_ERROR(-20779,
'Valoarea IdCineast depaseste valoarea curenta a secventei !');
ELSE
UPDATE distributie SET Actor=:NEW.IdCineast
WHERE Actor=:OLD.IdCineast ;
UPDATE premii_interpretare SET Actor=:NEW.IdCineast
WHERE Actor=:OLD.IdCineast ;
UPDATE producatori SET Producator=:NEW.IdCineast
WHERE Producator=:OLD.IdCineast ;
UPDATE regizori SET Regizor=:NEW.IdCineast
WHERE Regizor=:OLD.IdCineast ;
UPDATE scenaristi SET Scenarist=:NEW.IdCineast
WHERE Scenarist=:OLD.IdCineast ;
END IF ;
END ;
/

-------------------------------------------------------------------
CREATE OR REPLACE TRIGGER trg_filme_upd
AFTER UPDATE OF IdFilm ON filme FOR EACH ROW
DECLARE
v_urmatorulid filme.IdFilm%TYPE ;
BEGIN
SELECT last_number INTO v_urmatorulid
FROM user_sequences
WHERE sequence_name='SEQ_IDFILM' ;
IF :NEW.IdFilm >= v_urmatorulid THEN
RAISE_APPLICATION_ERROR(-20778,
'Valoarea IdFilm depaseste valoarea curenta a secventei !');
ELSE
pac_centru.vp_trg_filme_upd := TRUE ;
UPDATE distributie SET IdFilm=:NEW.IdFilm
WHERE IdFilm=:OLD.IdFilm ;
UPDATE distributie SET IdFilm=:NEW.IdFilm
Proiectarea bazelor de date 51

WHERE IdFilm=:OLD.IdFilm ;
UPDATE premii_filme SET IdFilm=:NEW.IdFilm
WHERE IdFilm=:OLD.IdFilm ;
UPDATE premii_interpretare SET IdFilm=:NEW.IdFilm
WHERE IdFilm=:OLD.IdFilm ;
UPDATE producatori SET IdFilm=:NEW.IdFilm
WHERE IdFilm=:OLD.IdFilm ;
UPDATE regizori SET IdFilm=:NEW.IdFilm
WHERE IdFilm=:OLD.IdFilm ;
UPDATE scenaristi SET IdFilm=:NEW.IdFilm
WHERE IdFilm=:OLD.IdFilm ;
UPDATE filme_genuri SET IdFilm=:NEW.IdFilm
WHERE IdFilm=:OLD.IdFilm ;
UPDATE casete_filme SET IdFilm=:NEW.IdFilm
WHERE IdFilm=:OLD.IdFilm ;
UPDATE aprecieri_filme SET IdFilm=:NEW.IdFilm
WHERE IdFilm=:OLD.IdFilm ;
pac_centru.vp_trg_filme_upd := FALSE ;
END IF ;
END ;
/

-------------------------------------------------------------------
CREATE OR REPLACE TRIGGER trg_casete_upd
AFTER UPDATE OF IdCaseta ON casete FOR EACH ROW
DECLARE
v_urmatorulid casete.IdCaseta%TYPE ;
BEGIN
SELECT last_number INTO v_urmatorulid
FROM user_sequences
WHERE sequence_name='SEQ_IDCASETA' ;
IF :NEW.Idcaseta >= v_urmatorulid THEN
RAISE_APPLICATION_ERROR(-20778,
'Valoarea IdCaseta depaseste valoarea curenta a secventei !');
ELSE
pac_centru.vp_trg_casete_upd := TRUE ;
UPDATE casete_filme SET IdCaseta=:NEW.IdCaseta
WHERE Idcaseta=:OLD.Idcaseta ;
UPDATE casete_imprumutate SET IdCaseta=:NEW.IdCaseta
WHERE Idcaseta=:OLD.Idcaseta ;
pac_centru.vp_trg_casete_upd := FALSE ;
END IF ;
END ;
/

-------------------------------------------------------------------
CREATE OR REPLACE TRIGGER trg_clienti_upd
AFTER UPDATE OF CNPClient ON clienti FOR EACH ROW
BEGIN
UPDATE imprumuturi SET CNPClient=:NEW.CNPClient
WHERE CNPClient=:OLD.CNPClient ;
UPDATE aprecieri_filme SET CNPClient=:NEW.CNPClient
WHERE CNPClient=:OLD.CNPClient ;
END ;
/

-------------------------------------------------------------------
52 Capitolul 7

CREATE OR REPLACE TRIGGER trg_imprumuturi_upd


AFTER UPDATE OF IdImpr ON imprumuturi FOR EACH ROW
DECLARE
v_urmatorulid imprumuturi.IdImpr%TYPE ;
BEGIN
SELECT last_number INTO v_urmatorulid
FROM user_sequences
WHERE sequence_name='SEQ_IDIMPR' ;
IF :NEW.IdImpr >= v_urmatorulid THEN
RAISE_APPLICATION_ERROR(-20778,
'Valoarea IdImpr depaseste valoarea curenta a secventei !');
ELSE
UPDATE casete_imprumutate SET IdImpr=:NEW.IdImpr
WHERE IdImpr=:OLD.IdImpr ;
END IF ;
END ;
/

Putem bifa linitii acum problemele ce in de restriciile refereniale din baz.


Ne apropiem uor de miezul problemei, anume de mecanismul de actualizare a
atributelor legate de casetele mprumutate (nchiriate) i restituirea acestora,
inclusiv regulile formulate cu privire la gratuiti i penalizri. Mecanismul de care
vorbim se nvrte n jurul declanatoarelor tabelei CASETE_MPRMUTATE. Din
raiuni de spaiu, ne vom opri doar la declanatorul de inserare n aceast tabel
vezi listing 7.31 prefaat de antetul i specificaiile pachetului PAC_CENTRU.
Dup cum se poate observa, n pachet au fost introduse o serie de funcii:
F_NRCASETENCHIRIATE1 - furnizeaz numrul de casete nchiriate de
un client dat;
F_NRCASETENCHIRIATE2 - furnizeaz numrul de casete nchiriate de
un client, cunoscnd ns doar identificatorul mprumutui;
F_CNPCLIENT returneaz codul numeric personal al unui client,
cunoscnd identificatorul mprumutului;
F_CHIRIEZI calculeaz tariful zilnic de nchiriere a unei casete (pe baza
anului n care a fost produs);
F_PRECASETA returneaz preul la care a fost cumprat caseta;
F_DATAMPR returneaz data (i ora) la care a avut loc un mprumut;
F_CASETADISPONIBILA returneaz TRUE n cazul n care caseta nu
este mprumutat, i FALSE n caz contrar.
Declararea acestor funcii are ca scop att micorarea dimensiunii declana-
toarelor tabelei, i, implicit, o mai buna lizibilitate i vitez de execuie, ct i
modularizarea aplicaiei, ntruct acestea vor fi necesare i n (cel puin) celalte
dou declanatoare de modificare i tergere - pe care nu le vom mai discuta.

Listing 7.31. Declanatoarul de inserare n CASETE_MPRUMUTATE, precedat de


pachetul PAC_CENTRU
-------------------------------------------------------------------
-- pachetul cu variabile publice se intoarce
CREATE OR REPLACE PACKAGE pac_centru AS
Proiectarea bazelor de date 53

-- variabila pentru semnalizarea locului de actualizare


-- in CASETE_FILME
vp_trg_casete_filme_del BOOLEAN := FALSE ;
vp_idcaseta casete.IdCaseta%TYPE ;
vp_filmnr casete_filme.FilmNr%TYPE ;

-- variabile pentru semnalizarea modificarilor in cascada


vp_trg_casete_upd BOOLEAN := FALSE ;
vp_trg_filme_upd BOOLEAN := FALSE ;

-- funcii
FUNCTION f_nrcaseteinchiriate1
(cnpclient_ clienti.CNPClient%TYPE)
RETURN clienti.NrCaseteImprumutate%TYPE;
FUNCTION f_nrcaseteinchiriate2 (idimpr_ imprumuturi.IdImpr%TYPE)
RETURN clienti.NrCaseteImprumutate%TYPE;
FUNCTION f_cnpclient (idimpr_ imprumuturi.IdImpr%TYPE)
RETURN clienti.CNPClient%TYPE;
FUNCTION f_chiriezi (idcaseta_ casete.Idcaseta%TYPE)
RETURN NUMBER ;
FUNCTION f_pretcaseta (idcaseta_ casete.IdCaseta%TYPE)
RETURN casete.PretCumparare%TYPE ;
FUNCTION f_dataimpr (idimpr_ casete_imprumutate.IdImpr%TYPE)
RETURN imprumuturi.DataOraImpr%TYPE ;
FUNCTION f_casetadisponibila (idcaseta_ casete.IdCaseta%TYPE)
RETURN BOOLEAN ;

END pac_centru ;
/
-------------------------------------------------
-- corpul pachetului
CREATE OR REPLACE PACKAGE BODY pac_centru AS
FUNCTION f_nrcaseteinchiriate1 (
cnpclient_ clienti.CNPClient%TYPE)
RETURN clienti.NrCaseteImprumutate%TYPE
IS
V_nrci clienti.NrCaseteImprumutate%TYPE ;
BEGIN
SELECT NrCaseteImprumutate INTO v_nrci
FROM clienti
WHERE CNPClient = cnpclient_ ;
RETURN v_nrci ;
END f_nrcaseteinchiriate1 ;
--------------------------
FUNCTION f_nrcaseteinchiriate2 (idimpr_ imprumuturi.IdImpr%TYPE)
RETURN clienti.NrCaseteImprumutate%TYPE
IS
V_nrci clienti.NrCaseteImprumutate%TYPE ;
BEGIN
SELECT NrCaseteImprumutate INTO v_nrci
FROM clienti
WHERE CNPClient = f_cnpclient(idimpr_) ;
RETURN v_nrci ;
END f_nrcaseteinchiriate2 ;
---------------------------
FUNCTION f_cnpclient (idimpr_ imprumuturi.IdImpr%TYPE)
RETURN clienti.CNPClient%TYPE
54 Capitolul 7

IS
v_cnpclient clienti.CNPClient%TYPE ;
BEGIN
SELECT CNPClient INTO v_cnpclient
FROM imprumuturi
WHERE IdImpr = idimpr_ ;
RETURN v_cnpclient ;
END f_cnpclient ;
---------------------------
FUNCTION f_chiriezi (idcaseta_ casete.Idcaseta%TYPE)
RETURN NUMBER
IS
v_anprodcaseta casete.AnProdCaseta%TYPE ;
BEGIN
SELECT NVL(AnProdCaseta, EXTRACT (YEAR FROM SYSDATE))
INTO v_anprodcaseta
FROM casete WHERE Idcaseta=idcaseta_ ;
CASE
WHEN EXTRACT (YEAR FROM SYSDATE) - v_anprodcaseta
BETWEEN 0 AND 1 THEN
RETURN 50000 ;
WHEN EXTRACT (YEAR FROM SYSDATE) - v_anprodcaseta
BETWEEN 2 AND 3 THEN
RETURN 40000 ;
ELSE
RETURN 30000 ;
END CASE ;
END f_chiriezi ;
---------------------------
FUNCTION f_pretcaseta (idcaseta_ casete.IdCaseta%TYPE)
RETURN casete.PretCumparare%TYPE
IS
v_pret casete.PretCumparare%TYPE ;
BEGIN
SELECT PretCumparare INTO v_pret FROM casete
WHERE Idcaseta=idcaseta_ ;
RETURN v_pret ;
END f_pretcaseta ;
---------------------------
FUNCTION f_dataimpr (idimpr_ casete_imprumutate.IdImpr%TYPE)
RETURN imprumuturi.DataOraImpr%TYPE
IS
v_data imprumuturi.DataOraImpr%TYPE ;
BEGIN
SELECT DataOraImpr INTO v_data
FROM imprumuturi WHERE IdImpr = idimpr_ ;
RETURN v_data ;
END f_dataimpr ;
----------------------------
FUNCTION f_casetadisponibila (idcaseta_ casete.IdCaseta%TYPE)
RETURN BOOLEAN
IS
v_disponibilitate CHAR(1);
BEGIN
SELECT Disponibilitate INTO v_disponibilitate
FROM casete WHERE IdCaseta=idcaseta_ ;
IF v_disponibilitate='D' THEN
Proiectarea bazelor de date 55

RETURN TRUE;
ELSE
RETURN FALSE ;
END IF ;
END f_casetadisponibila ;

END pac_centru ;
/

-------------------------------------------------------------------
CREATE OR REPLACE TRIGGER trg_casete_imprumutate_ins
BEFORE INSERT ON casete_imprumutate FOR EACH ROW

DECLARE
v_rest NUMBER(2) ; -- restul impartirii la 11 (pt. caseta
-- inchiriata gratuit)
BEGIN
-- se inregistreaza imprumutul unei casete

-- se creste numarul casetelor imprumutate clientului respectiv


UPDATE clienti
SET NrCaseteImprumutate = NrCaseteImprumutate + 1
WHERE CNPClient = pac_centru.f_cnpclient(:NEW.IdImpr) ;

v_rest := MOD(pac_centru.f_nrcaseteinchiriate2(:NEW.IdImpr),11) ;

IF v_rest = 0 THEN
:NEW.Gratuita := 'D' ;
ELSE
:NEW.Gratuita := 'N' ;
UPDATE imprumuturi SET ValoareInchir = ValoareInchir +
pac_centru.f_chiriezi(:NEW.IdCaseta)
WHERE IdImpr = :NEW.IdImpr ;
END IF ;

IF :NEW.DataOraRestituirii IS NULL THEN


-- se verifica disponibilitatea casetei
IF pac_centru.f_casetadisponibila(:NEW.IdCaseta) = FALSE THEN
RAISE_APPLICATION_ERROR(-20678, 'Caseta indisponibila') ;
END IF ;

-- caseta se declara indisponibila (pina la returnare)


UPDATE casete SET Disponibilitate='N'
WHERE IdCaseta = :NEW.IdCaseta ;
:NEW.penalizare := 0 ;
ELSE
-- caseta e deja returnata (se opereaza simultan
-- inchirierea si returnarea)
UPDATE casete SET Disponibilitate='D'
WHERE IdCaseta = :NEW.IdCaseta ;

-- se incrementeaza numarul casetelor retrnate


-- de clientul respectiv
UPDATE clienti
SET NrCaseteRestituite = NrCaseteRestituite + 1
WHERE CNPClient IN
(SELECT CNPClient FROM imprumuturi
56 Capitolul 7

WHERE IdImpr=:NEW.IdImpr) ;

-- se verifica intirzierile la returnare (mai mult de 2 zile)


IF :NEW.DataOraRestituirii
pac_centru.f_dataimpr (:NEW.IdImpr) > 2 THEN
:NEW.penalizare := 0.75 *
pac_centru.f_chiriezi(:NEW.IdCaseta) *
(:NEW.DataOraRestituirii
pac_centru.f_dataimpr (:NEW.IdImpr) - 2) ;
ELSE
:NEW.penalizare := 0 ;
END IF ;
END IF ;

IF :NEW.StareLaRestituire IN ('Pierduta', 'Distrusa') THEN


UPDATE casete
SET StareCaseta = :NEW.StareLaRestituire
WHERE IdCaseta=:NEW.Idcaseta ;
:NEW.Penalizare := :NEW.Penalizare +
2 * pac_centru.f_pretcaseta(:NEW.IdCaseta) ;
END IF ;

END ;
/

n privina declanatorului propriu-zis, pornim de la premisa c inserarea se


poate referi att la operarea on-line a nchirierii unei casete, dar i la operarea
unui mprumut mai vechi, pentru care se nregistreaz att nchirierea, ct i
restituirea. Logica declanatorului este urmtoarea:
se incrementeaz numrul casetelor mprumutate de clientul respectiv;
se calculeaz dac numrul respectivei casete mprumutate clientului este
multiplu de 11, caz n care nchirierea sa este gratuit;
dac nu avem cazul de gratuitate de mai sus, se adaug chiria casetei
curente la valoarea pe care trebuie s o plteasc clientul pentru mprumu-
tul din care face parte i caseta curent;
dac se opereaz mprumutul unei casete ce nu a fost restituit pn n
momentul inserrii (acesta este, de fapt, cazul cel mai frecvent), se verific
dac se poate nchiria caseta (dac este disponibil); dup operarea
mprumutului, caseta se declar indisponibil (pn la restituirea sa);
dac inserarea privete o caset mprumutat i restituit, atunci se
incrementeaz numrul de casete restituite de client, i apoi se trece la
calcularea eventualelor penaliti de ntrziere sau eventualei penaliti de
pierdere sau distrugere a casetei.

Din pcate, mai este destul de mult de lucru. Mai trebuie create:
- declanatoarele de tergere i modificare pentru CASETE_MPRUMUTA-
TE;
- declanatorul de modificare pentru CASETE, ntruct o caset poate fi
pierdut sau distrus i de personalul propriu sau datorit unui incendiu,
Proiectarea bazelor de date 57

inundaii etc.; pe de alt parte, pentru atributele calculate nu trebuie


permis modificarea interactiv (altfel dect prin declanatoare);
- declanatorul de modificare pentru CLIENI i MPRUMUTURI: pentru
atributele calculate nu trebuie permis modificarea interactiv (altfel dect
prin declanatoare);
Pentru detalii privitoare la aceste operaiuni, putei consulta i lucrarea [Fotache
s.a.03].