Documente Academic
Documente Profesional
Documente Cultură
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.
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.
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.
Dac inem cont de dependenele multivaloare, s-ar mai aduga acestor relaii
i:
inciden direct n schema final a bazei de date ! Graful din partea dreapt a
figurii rmne drept tem pentru acas.
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.
2 [Lonigro98]
3 [Harrington02], pp.77-82
4 [Simsion01], pp.282-285
8 Capitolul 7
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
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
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
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 ;
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}:
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 ;
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 ;
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:
Firete, nici de funcie nu mai avem nevoie n noile condiii, iar soluia are mult
mai multe anse de a fi acceptat.
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
RAISE_APPLICATION_ERROR (-20852,
'Nu pot fi simultan mai multe conturi si pe debit si pe credit') ;
END IF ;
END ;
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 ;
/
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
% 401 11800000
300 10000000
4426 1800000
uncontdebitor = :NEW.contdebitor,
uncontcreditor = :NEW.contcreditor
WHERE idoperatiune = :NEW.idoperatiune ;
END ;
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
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:
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.
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
13 Vezi, spre exemplu, Horomnea, E. - Bazele contabilitii. Concepte, aplicaii, lexicon, Editura
Sedcom Libris, Iai, 2004, pp. 178-180
24 Capitolul 7
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.
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 ;
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 ;
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
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.9. Trei dependene de incluziune, dintre care una semnific o specializare
Decupnd relaiile din graf, obinem:
CONTURI_ELEMENTARE {ContElementar }
v_nr NUMBER(3) := 0 ;
BEGIN
v_parinte := SUBSTR(:NEW.SimbolCont,1, LENGTH(:NEW.SimbolCont)-
1);
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
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
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
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
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
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
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
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
14 Pentru cteva detalii despre funcia NVL2, vezi i [Fotache s.a.03], p.190
Proiectarea bazelor de date 47
: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 ;
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;
/
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;
/
-------------------------------------------------------------------
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
-- 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
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 ;
WHERE IdImpr=:NEW.IdImpr) ;
END ;
/
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