kako sprijeČiti zaČarani krug (rješavanje određenog tipa ... fileposlovne logike). danas postoje...
TRANSCRIPT
KAKO SPRIJEČITI "ZAČARANI KRUG"
(rješavanje određenog tipa poslovnih pravila u Oracle bazi podataka)
SAŽETAK
U radu je prikazano kako se u Oracle bazi podataka može spriječiti pojava "začaranog kruga"
(zatvorene petlje) u podacima koji imaju stablastu strukturu, i to bez pomoći klijentskog programa ili
programa na aplikacijskom serveru (dakle, rješenje je u cijelosti na strani baze).
Prikazano je rješenje za jednokorisnički rad i za višekorisnički rad. Za rješenje u višekorisničkom radu
koristi se (naša) simulacija "ROLLBACK TO SAVEPOINT ponašanja" u okidaču baze, koja
(simulacija) koristi kvazi-udaljene procedure (lokalne procedure koje se pozivaju kao da se nalaze na
drugoj bazi).
ABSTRACT
In this article, it is shown how to avoid the problem of a so-called "vicious cycle" (closed loop) in data
with tree structure by doing that completely on the data base side, without writing any client software
or application server software.
Single user and multi user solution is given. In multi user solution, our simulation of "ROLLBACK TO
SAVEPOINT feature" is used in data base trigger, which involve quasi-remote procedures (it means
that local procedures has been called like they are from some other data base).
1. UVOD
Često želimo spriječiti pojavu "začaranog kruga" (zatvorene petlje) u podacima koji imaju višestruku
stablastu strukturu (gdje svaki čvor stabla može imati više čvorova-djece i najviše jedan čvor-roditelj)
ili jednostruku stablastu strukturu (gdje točno jedan čvor nema roditelja, a svi ostali čvorovi imaju
točno jednog roditelja). Npr. u tablici "djelatnici" (poznata Oracle tablica "emp") želimo spriječiti da
jedan djelatnik bude šef drugom djelatniku, a da istovremeno taj drugi djelatnik bude (direktno ili
indirektno) šef prvom djelatniku.
Ovaj zahtjev je, očito, relativno lako definirati, a dosta je čest u praksi. Međutim, nije ga lako realizirati
(isključivo) u bazi podataka, tj. bez pomoći klijentske strane. Ovdje se prikazuje rješenje tog zahtjeva
u Oracle bazi podataka, bez pomoći programa na klijentu ili aplikacijskom serveru (dakle, rješenje je u
cijelosti na strani baze).
Rješenje u jednokorisničkom radu je relativno jednostavno (komplicira ga problem mutirajućih tablica).
Međutim, za rješenje u višekorisničkome radu morali smo primijeniti simulaciju "ROLLBACK TO
SAVEPOINT ponašanja" u okidaču baze, koristeći kvazi-udaljene procedure (procedure koje
pozivamo kao da se nalaze na drugoj bazi, iako se nalaze u lokalnoj bazi).
2. POSLOVNA PRAVILA
Već krajem 70-tih godina pojavili su se značajni opisi slojevite arhitekture informacijskih sustava, tzv.
troslojne arhitekture (three-tier architecture). Kako se navodi u [Larman 2002], u to je vrijeme pojam
"tier" imao poprilično drugačije značenje od onoga kojeg (pretežno) ima danas. Danas se "tier"
(obično) upotrebljava kao oznaka za fizički čvor (node), dok je nekad to bila oznaka za logički sloj.
Kad se govori o logičkim slojevima, danas se obično upotrebljava termin "layer". Troslojna arhitektura
poprimila je veliku popularnost tek 90-tih godina, dobrim dijelom zahvaljujući promociji koju je
napravila Gartner grupa.
"Klasičan" opis troslojne arhitekture navodio je tri sloja: sloj korisničkog sučelja (User Interface), sloj
aplikacijske logike (Application Logic) i podatkovni sloj (Storage). Tijekom vremena se broj (logičkih)
slojeva povećavao, pa npr. J2EE specifikacija navodi četiri sloja: sloj korisničkog sučelja (Client Tier),
sloj prezentacijske logike na serveru (Web Tier), sloj poslovne logike, tj. poslovnih objekata i
poslovnih pravila (EJB Tier) i podatkovni sloj (Data Tier). Neke klasifikacije navode pet ili više slojeva.
U svakom slučaju, poslovna pravila (business rules) čine značajan dio aplikacijskog sloja (ili sloja
poslovne logike).
Danas postoje i takva mišljenja da je pristup razvoju aplikacija na temelju poslovnih pravila (Business
Rules Approach to Application Development) sljedeći glavni evolucijski korak u razvoju aplikacija, koji
slijedi iza objektno-orijentiranog pristupa (Object-Oriented Approach). Prethodnici objektno-
orijentiranom pristupu su strukturna analiza sustava (Structured Systems Analysis) i informacijski
inženjering (Information Engineering). Jedan od zastupnika takvog mišljenja je i autoritet na području
relacijskih baza podataka i pisac brojnih knjiga sa tog područja C.J.Date, koji je popularizirao relacijski
model čiji je autor E.F.Codd. Date naglašava da su današnji sustavi za upravljanje relacijskim
bazama podataka (RDBMS) još poprilično daleko od teorijskog modela koji je postavio Codd, i to
naročito u jednoj od tri glavne komponente relacijskog modela – komponenti integriteta podataka
(Data Integrity), dok su komponenta za definiranje strukture podataka (Data Structures) i komponenta
za manipulaciju podacima (Data Manipulation) puno potpunije realizirane.
Postoje brojne definicije pojma "poslovno pravilo". Npr. u [von Halle 2002] može se naći nekoliko
definicija različitih autora. Za potrebe rada sa Oracle bazom, nama se čini dobra (što ne znači da su
druge loše) definicija (i klasifikacija) iz Oracle CDM (Custom Development Method) metodike:
"Poslovna pravila su ograničenja koja se primjenjuju na stanje sustava ili na promjenu stanja sustava
(Constraint Rules), autorizacijska pravila, koja određuju koji korisnici mogu raditi i što mogu raditi u
sustavu (Authorization Rules), ili akcije koje se automatski pokreću nakon promjene stanja sustava
(Change Event Rules)".
Oracle navodi da se njihova klasifikacija može sagledati i u terminima UML modeliranja. Invarijantama
(Invariant) se mogu nazvati ograničenja na stanje sustava, pretkondicijama (PreCondition) se mogu
zajedno nazvati ograničenja na promjenu stanja sustava i autorizacijska pravila, a postkondicijama
(PostCondition) se mogu nazvati akcije koje se automatski pokreću nakon promjene stanja sustava.
Ova (alternativna) Oracle klasifikacija je vrlo bliska konceptu Design By Contract (DBC) čiji je autor
B.Meyer, autor OOPL jezika Eiffel [Meyer 1997].
Oracle CDM klasifikacija dalje razlaže navedena tri (glavna) tipa poslovnih pravila. Poslovno pravilo
čije rješavanje ovdje prikazujemo spada po Oracle klasifikaciji u tip Constraint Rules, podtip Entity
Rules i vrstu Other Entity Rules. Riječ je o tome da često želimo zabraniti "začarani krug", tj. pojavu
zatvorene petlje u jednostablastim ili višestablastim rekurzivnim strukturama podataka.
Jednostablasta rekurzivna struktura je ona u kojoj točno jedan objekt nema "roditelja" (on je "vrh"
stabla), a svi ostali imaju jednog "roditelja". Višestablasta rekurzivna struktura je sastavljena od više
stabala, tj. barem dva objekta nemaju "roditelja", a svi ostali objekti imaju točno jednog "roditelja".
Jedan primjer (jedno)stablaste strukture je dat poznatom Oracle tablicom djelatnika – "emp"
(employes). Najjednostavniji opis tablice "emp" je:
CREATE OR REPLACE TABLE emp (
empno NUMBER (4),
ename VARCHAR2 (20)
mgr NUMBER (4))
/
Želimo da baza spriječi "začarani krug" u tablici "emp", odnosno želimo onemogućiti da dobijemo
podatke u kojima bi neki djelatnik bio menadžer drugom djelatniku, a drugi djelatnik bi (direktno ili
indirektno) bio menadžer prvom djelatniku.
Za potrebe izlaganja, napunit ćemo tablicu "emp" sa 7 redaka. Djelatnik sa brojem 1 bit će "glavni
šef", djelatnici sa brojevima 2 i 3 bit će "šefovi" (podređeni "glavnom šefu"), djelatnici 4 i 5, odnosno 6
i 7, bit će podređeni "šefu 2", odnosno "šefu 3":
INSERT INTO emp (empno, ename, mgr) VALUES (1, 'EMP 1', NULL);
INSERT INTO emp (empno, ename, mgr) VALUES (2, 'EMP 2', 1);
INSERT INTO emp (empno, ename, mgr) VALUES (3, 'EMP 3', 1);
INSERT INTO emp (empno, ename, mgr) VALUES (4, 'EMP 4', 2);
INSERT INTO emp (empno, ename, mgr) VALUES (5, 'EMP 5', 2);
INSERT INTO emp (empno, ename, mgr) VALUES (6, 'EMP 6', 3);
INSERT INTO emp (empno, ename, mgr) VALUES (7, 'EMP 7', 3);
3. SPREČAVANJE "ZAČARANOG KRUGA" U JEDNOKORISNIČKOM RADU
Rješenje u jednokorisničkom radu je relativno jednostavno. Zapravo, rješenje bi bilo vrlo jednostavno
kad ne bi dolazilo do jednog problema - problema mutirajućih tablica (mutating tables).
Mutirajuća tablica je ona tablica koja se trenutačno modificira pomoću naredbe INSERT, UPDATE ili
DELETE, ili ona tablica koja bi trebala biti ažurirana zbog efekta DELETE CASCADE deklarativnog
ograničenja. Oracle ne dozvoljava da se mutirajuće tablice čitaju (niti ažuriraju) u "row" okidačima
baze (database triggers), tj. okidačima koji se okidaju za svaki redak tablice, jer bismo kao rezultat
(čitanja) mogli dobiti neku neočekivanu vrijednost. Pojednostavljeno rečeno, Oracle ne dozvoljava da
čitamo tablicu dok traje proces njene izmjene u istoj sesiji baze.
Međutim, za razliku od "row" okidača, čitanje se može raditi u "statement" okidačima, tj. okidačima
koji se okidaju jedanput za svaku naredbu INSERT, UPDATE ili DELETE. Klasično rješenje problema
mutirajućih tablica jeste da se u "row" okidaču zapamti (npr. u PL/SQL memorijsku tablicu) koji su
redovi ažurirani, a onda se u "after statement" okidačima čita PL/SQL tablica i primjenjuje se provjera
poslovnog pravila na retke koji su zapamćeni u PL/SQL tablici. Obično želimo da okidači sadrže što
manje programskog koda, tako da okidači najčešće samo pozivaju pohranjene (a najčešće i pakirane)
procedure ili funkcije.
Napravit ćemo okidače navedene u nastavku (naravno, ako ih propustimo na bazu prije nego
propustimo pakirane procedure koje oni koriste, okidači će biti u stanju "INVALID").
Okidač "bus_emp" ("before update statement" nad tablicom "emp" - okida se jedanput prije naredbe
UPDATE) poziva (pakiranu) proceduru za čišćenje PL/SQL tablice:
CREATE OR REPLACE TRIGGER bus_emp
BEFORE UPDATE ON emp
BEGIN
emp_closed_loop.clear_plsql_tab;
END;
/
Okidač "bir_emp" ("before insert row" - okida se jedanput za svaki uneseni redak) provjerava da li su
u stupcima "empno" i "mgr" različite vrijednosti (inače javlja grešku):
CREATE OR REPLACE TRIGGER bir_emp
BEFORE INSERT ON emp
FOR EACH ROW
BEGIN
IF :NEW.empno = :NEW.mgr THEN
RAISE_APPLICATION_ERROR
(-20002, 'Djelatnik ne može biti nadređen samome sebi!');
END IF;
END;
/
Okidač "bur_emp" ("before update row") zabranjuje mijenjanje šifra djelatnika, zabranjuje da djelatnik
bude nadređen samome sebi i poziva proceduru koja pamti redak u PL/SQL tablicu:
CREATE OR REPLACE TRIGGER bur_emp
BEFORE UPDATE ON emp
FOR EACH ROW
BEGIN
IF :NEW.empno <> :OLD.empno THEN
RAISE_APPLICATION_ERROR (-20001, 'NEW EMPNO <> OLD EMPNO');
END IF;
IF :NEW.empno = :NEW.mgr THEN
RAISE_APPLICATION_ERROR
(-20002, 'Djelatnik ne može biti nadređen samome sebi!');
END IF;
IF :NEW.mgr IS NOT NULL AND :NEW.mgr <> NVL (:OLD.mgr, 0) THEN
emp_closed_loop.write_plsql_tab (
p_empno => :OLD.empno,
p_mgr => :NEW.mgr);
END IF;
END;
/
Okidač "aus_emp" ("after update statement" nad tablicom "emp" - okida se jedanput nakon naredbe
UPDATE) poziva (pakiranu) proceduru za provjeru poslovnog pravila (ta je procedura krucijalni dio
programskog koda):
CREATE OR REPLACE TRIGGER aus_emp
AFTER UPDATE ON emp
BEGIN
emp_closed_loop.test;
END;
/
Slijedi paket "emp_closed_loop", sa tri (već navedene) procedure:
CREATE OR REPLACE PACKAGE emp_closed_loop IS
PROCEDURE clear_plsql_tab;
PROCEDURE write_plsql_tab (
p_empno emp.empno%TYPE,
p_mgr emp.mgr%TYPE);
PROCEDURE test;
END emp_closed_loop;
/
CREATE OR REPLACE PACKAGE BODY emp_closed_loop IS
TYPE rec_t IS RECORD (
empno emp.empno%TYPE,
mgr emp.mgr%TYPE);
TYPE plsql_tab_t IS TABLE OF rec_t INDEX BY BINARY_INTEGER;
m_plsql_tab plsql_tab_t;
m_rows BINARY_INTEGER;
PROCEDURE clear_plsql_tab IS
BEGIN
m_rows := 0;
END;
PROCEDURE write_plsql_tab (
p_empno emp.empno%TYPE,
p_mgr emp.mgr%TYPE)
IS
BEGIN
m_rows := m_rows + 1;
m_plsql_tab (m_rows).empno := p_empno;
m_plsql_tab (m_rows).mgr := p_mgr;
END;
PROCEDURE test IS
l_mgr emp.mgr%TYPE;
l_empno emp.empno%TYPE;
BEGIN
FOR i IN 1..m_rows LOOP
l_empno := m_plsql_tab (i).empno;
l_mgr := m_plsql_tab (i).mgr;
WHILE l_mgr IS NOT NULL LOOP
SELECT mgr INTO l_mgr
FROM emp
WHERE empno = l_mgr;
IF l_mgr = l_empno THEN
RAISE_APPLICATION_ERROR
(-20003, 'Greška - zatvorena petlja!');
END IF;
END LOOP;
END LOOP;
END;
END emp_closed_loop;
/
Vidljivo je da procedura "test" čita tablicu "emp", pa tu proceduru nismo mogli pozvati u "row" okidaču
"bur_emp", već smo to mogli napraviti samo u "statement" okidaču "aus_emp".
Primijetimo da smo proceduru "test" mogli napisati i drugačije (konciznije), tako da koristimo klauzulu
CONNECT BY naredbe SELECT (međutim, u nastavku ćemo dograđivati prethodnu verziju, bez
klauzule CONNECT BY):
PROCEDURE test IS
closed_loop EXCEPTION;
-- ORA-01436: CONNECT BY loop in user data
PRAGMA EXCEPTION_INIT (closed_loop, -1436);
l_mgr emp.mgr%TYPE;
l_empno emp.empno%TYPE;
l_dummy NUMBER;
BEGIN
FOR i IN 1..m_rows LOOP
l_empno := m_plsql_tab (i).empno;
l_mgr := m_plsql_tab (i).mgr;
SELECT COUNT (*) INTO l_dummy
FROM emp
WHERE empno = l_empno
START WITH empno = l_mgr CONNECT BY PRIOR mgr = empno;
END LOOP;
EXCEPTION
WHEN closed_loop THEN
RAISE_APPLICATION_ERROR
(-20003, 'Greška - zatvorena petlja!');
END;
Sada možemo testirati rješenje. Ako nad početnim podacima primijenimo sljedeću UPDATE naredbu,
dobit ćemo poruku o grešci (i to je u redu):
UPDATE emp SET mgr = 4 WHERE empno = 1
/
ERROR at line 1:
ORA-20003: Greška - zatvorena petlja!
ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 40
ORA-06512: at "SCOTT.AUS_EMP", line 2
ORA-04088: error during execution of trigger 'SCOTT.AUS_EMP'
Nažalost, navedeno rješenje radi dobro samo u jednokorisničkom radu! U višekorisničkome radu (ili,
što je isto, u jednokorisničkom radu u kojem korisnik ima više sesija baze), može se desiti greška, kao
u sljedećem primjeru:
-- 1. SESIJA
UPDATE emp SET mgr = 3 WHERE empno = 2
/
-- 2. SESIJA
UPDATE emp SET mgr = 2 WHERE empno = 3
/
Obje naredbe su prošle, pa smo dobili zatvorenu petlju!
Prije nastavka, nemojmo zaboraviti da vratimo podatke na početno stanje (pomoću ROLLBACK) u
obje sesije. Pretpostavit ćemo da to ubuduće uvijek radimo.
4. POKUŠAJ SPREČAVANJA "ZAČARANOG KRUGA" U VIŠEKORISNIČKOM RADU
POMOĆU AUTONOMNE TRANSAKCIJE
Glavna ideja za sprečavanje "začaranog kruga" u višekorisničkom radu je da, istovremeno dok
provjeravamo da li je došlo do zatvorene petlje, gledamo da li je tekući redak (tj. redak koji trenutačno
provjeravamo) zaključan. Ako je zaključan, možemo pretpostaviti da bi moglo doći do zatvorene
petlje, te javiti grešku.
Međutim, kako provjeriti da li je redak (koji provjeravamo) zaključan? Ako za tu namjenu koristimo
SELECT FOR UPDATE, zaključat ćemo redak sve do kraja transakcije, zato što u okidaču Oracle
baze ne možemo koristiti naredbu ROLLBACK TO SAVEPOINT (napomenimo da ovo ograničenje,
kao ni ograničenje vezano za mutirajuće tablice, nije mana Oracle baze, već prednost, jer ta
ograničenja sprečavaju da dođe do programskih grešaka koje bi se vrlo teško mogle otkriti). No, ako
redak ostane zaključan sve do kraja transakcije, to će spriječiti druge da rade sa takvim retkom, što je
neprihvatljivo.
Budući da od verzije 8i Oracle baza podržava autonomne transakcije, možemo razmišljati da ih
primijenimo za rješenje problema zaključavanja. Naime, u autonomnoj transakciji možemo koristiti
ROLLBACK (zapravo, autonomna transakcija i mora na kraju imati ROLLBACK ili COMMIT). U (tijelo)
paketa "emp_closed_loop" dodat ćemo novu autonomnu proceduru "test_lock":
PROCEDURE test_lock (p_mgr emp.mgr%TYPE) IS
PRAGMA AUTONOMOUS_TRANSACTION;
l_dummy NUMBER;
BEGIN
SELECT 1 INTO l_dummy
FROM emp
WHERE empno = p_mgr
FOR UPDATE NOWAIT;
ROLLBACK;
EXCEPTION
WHEN OTHERS THEN
-- ORA-00054: resource busy and acquire with NOWAIT specified
IF SQLCODE = -54 THEN
RAISE_APPLICATION_ERROR
(-20004, 'Greška - moguća zatvorena petlja!');
ELSE
RAISE;
END IF;
END;
Novu proceduru pozvat ćemo iz mijenjane procedure "test":
PROCEDURE test IS
l_mgr emp.mgr%TYPE;
l_empno emp.empno%TYPE;
BEGIN
FOR i IN 1..m_rows LOOP
l_empno := m_plsql_tab (i).empno;
l_mgr := m_plsql_tab (i).mgr;
WHILE l_mgr IS NOT NULL LOOP
test_lock (l_mgr);
SELECT mgr INTO l_mgr
FROM emp
WHERE empno = l_mgr;
IF l_mgr = l_empno THEN
RAISE_APPLICATION_ERROR
(-20003, 'Greška - zatvorena petlja!');
END IF;
END LOOP;
END LOOP;
END;
Naredbe koje su uzrokovale grešku u točci 2 sad neće uspjeti, jer će baza upozoriti da bi moglo doći
do zatvorene petlje ("moguća greška" a ne "sigurna greška", jer to što je redak zaključan ne znači da
bi do greške sigurno došlo):
-- 1. SESIJA
UPDATE emp SET mgr = 3 WHERE empno = 2
/
-- 2. SESIJA
UPDATE emp SET mgr = 2 WHERE empno = 3
/
ERROR at line 1:
ORA-20004: Greška - moguća zatvorena petlja!
ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 64
ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 37
ORA-06512: at "SCOTT.AUS_EMP", line 2
ORA-04088: error during execution of trigger 'SCOTT.AUS_EMP'
Nažalost, rješenje sa autonomnom transakcijom općenito ne radi dobro, zato što su autonomnoj
transakciji (baš zato što je autonomna, tj. nezavisna od "glavne" transakcije) zaključani oni redovi koje
je zaključala "glavna" transakcija. Zato autonomna procedura "zaključuje" da je došlo do zatvorene
petlje i onda kad je očito da nije došlo do zatvorene petlje. Evo takvog slučaja, u kojem autonomna
transakcija "pogrešno zaključuje":
-- dvije UPDATE naredbe u istoj sesiji
UPDATE emp SET mgr = 2 WHERE empno = 6
/
UPDATE emp SET mgr = 6 WHERE empno = 7
/
ERROR at line 1:
ORA-20004: Greška - moguća zatvorena petlja!
ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 64
ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 37
ORA-06512: at "SCOTT.AUS_EMP", line 2
ORA-04088: error during execution of trigger 'SCOTT.AUS_EMP'
Iako bi bilo sasvim u redu da djelatnik broj 6 postane (direktno) nadređen djelatniku 7, druga naredba
UPDATE javlja grešku zato jer autonomna procedura "test_lock" nalazi da je djelatnik 6 zaključan
(zaključala ga je "glavna" transakcija, kroz prvu naredbu UPDATE).
5. SIMULACIJA "ROLLBACK TO SAVEPOINT PONAŠANJA" U OKIDAČU BAZE
Vidjeli smo da autonomna transakcija nije dobra za rješavanje problema zatvorene petlje u
višekorisničkom radu. Nažalost (kako smo već rekli u točci 3.) naredbe SAVEPOINT / ROLLBACK TO
SAVEPOINT ne možemo koristiti u okidaču baze, jer se javljaju greške:
ORA-04092: cannot SET SAVEPOINT in a trigger
ORA-04092: cannot ROLLBACK in a trigger
Međutim, našli smo da je u Oracle bazi moguće simulirati SAVEPOINT / ROLLBACK TO SAVEPOINT
naredbe u okidaču baze. Pokažimo to na jednom jednostavnom primjeru (napomena: budući da i ovaj
primjer koristi tablicu "emp", poželjno je probati ga na nekoj drugoj shemi, a ne na onoj na kojoj
rješavamo problem zatvorene petlje).
Pretpostavimo da želimo imati transakciju koja se sastoji od 3. dijela:
(1) unos jednog DEPT
(2) unos dva EMP sa job = MANAGER (koji pripadaju prethodno unesenom DEPT)
(3) unos dva EMP sa job = PROGRAMER (koji pripadaju prethodno unesenom DEPT).
Pretpostavimo dalje da želimo da transakcija uspije i ako 3.dio ne uspije, ali samo tako da se poništi
ono što je 3.dio djelomično napravio (tj. unos samo jednog EMP). Napravimo prvo paket koji ne radi
dobro:
CREATE OR REPLACE PACKAGE example_pkg IS
PROCEDURE insert_emps_for_dept (p_deptno dept.deptno%TYPE);
END;
/
CREATE OR REPLACE PACKAGE BODY example_pkg IS
PROCEDURE insert_managers (p_deptno dept.deptno%TYPE) IS
BEGIN
INSERT INTO emp (empno, ename, job, mgr, sal, deptno)
VALUES (1, 'EMP 1', 'MANAGER', NULL, 5000, p_deptno);
INSERT INTO emp (empno, ename, job, mgr, sal, deptno)
VALUES (2, 'EMP 2', 'MANAGER', 1, 4000, p_deptno);
END;
PROCEDURE insert_programmers (p_deptno dept.deptno%TYPE) IS
BEGIN
INSERT INTO emp (empno, ename, job, mgr, sal, deptno)
VALUES (3, 'EMP 3', 'PROGRAMER', 1, 1000, p_deptno);
RAISE_APPLICATION_ERROR
(-20001, 'Simulirana greška u sredini 3.dijela transakcije');
INSERT INTO emp (empno, ename, job, mgr, sal, deptno)
VALUES (4, 'EMP 4', 'PROGRAMER', 1, 1000, p_deptno);
END;
PROCEDURE insert_emps_for_dept (p_deptno dept.deptno%TYPE) IS
BEGIN
-- 2. dio transakcije
insert_managers (p_deptno);
-- 3. dio transakcije
BEGIN
insert_programmers (p_deptno);
EXCEPTION
WHEN OTHERS THEN NULL;
END;
END;
END;
/
Pozovimo sada proceduru iz neimenovanog PL/SQL bloka (bez okidača baze), sa:
BEGIN
INSERT INTO dept (deptno, dname) VALUES (1, 'DEPT 1');
example_pkg.insert_emps_for_dept (1);
END;
/
Pogledajmo što smo dobili, sa SELECT upitom:
SELECT emp.empno, emp.ename, dept.deptno, dept.dname
FROM emp, dept
WHERE empno BETWEEN 1 AND 4
AND emp.deptno = dept.deptno
ORDER BY empno
/
EMPNO ENAME DEPTNO DNAME
---------- --------------- ---------- --------------
1 EMP 1 1 DEPT 1
2 EMP 2 1 DEPT 1
3 EMP 3 1 DEPT 1
Naravno, transakcija nije dobra, jer je ostao upisan EMP 3!
Napravimo ROLLBACK i mijenjajmo proceduru "insert_emps_for_dept" tako da dodamo
naredbe SAVEPOINT / ROLLBACK TO SAVEPOINT:
PROCEDURE insert_emps_for_dept (p_deptno dept.deptno%TYPE) IS
BEGIN
-- 2. dio transakcije
insert_managers (p_deptno);
-- 3. dio transakcije
BEGIN
SAVEPOINT before_insert_programmers;
insert_programmers (p_deptno);
EXCEPTION
WHEN OTHERS THEN ROLLBACK TO before_insert_programmers;
END;
END;
Ako sad ponovimo prethodni SELECT, dobit ćemo ispravan rezultat.
Napravimo opet ROLLBACK i kreirajmo okidač:
CREATE OR REPLACE TRIGGER air_dept
AFTER INSERT ON dept
FOR EACH ROW
BEGIN
example_pkg.insert_emps_for_dept (:NEW.deptno);
END;
/
te pokušajmo izvesti naredbu:
INSERT INTO dept (deptno, dname) VALUES (1, 'DEPT 1')
/
Naravno, dešava se greška ORA-04092: cannot ROLLBACK in a trigger.
Napravimo opet ROLLBACK. Sada ćemo (konačno) primijeniti trik. On se temelji na činjenici da ako
pozivamo udaljenu proceduru (pomoću database linka) i ako se u njoj desi neobrađena greška, njeni
se efekti u cijelosti poništavaju (za razliku od lokalne procedure). Nama ne treba udaljena procedura,
ali napravit ćemo kvazi-udaljenu proceduru, koristeći "lokalni" database link (link baze na sebe samu):
CREATE DATABASE LINK local_db_link
CONNECT TO scott IDENTIFIED BY tiger
USING 'local_alias' -- alias na lokalnu bazu
/
Mijenjajmo sada proceduru "insert_emps_for_dept" tako da poziva "insert_programmers" kao
udaljenu proceduru (pomoću database linka):
CREATE OR REPLACE PACKAGE example_pkg IS
PROCEDURE insert_emps_for_dept (p_deptno dept.deptno%TYPE);
-- mora biti u specifikaciji (poziva se pomoću database linka)
PROCEDURE insert_programmers (p_deptno dept.deptno%TYPE);
END;
/
CREATE OR REPLACE PACKAGE BODY example_pkg IS
...
PROCEDURE insert_emps_for_dept (p_deptno dept.deptno%TYPE) IS
BEGIN
-- 2. dio transakcije
insert_managers (p_deptno);
-- 3. dio transakcije
BEGIN
example_pkg.insert_programmers@local_db_link (p_deptno);
EXCEPTION
WHEN OTHERS THEN NULL;
END;
END;
END;
/
Ako sada ponovno izvedemo (već navedenu) naredbu INSERT, naredba SELECT dat će (ispravno)
samo dva retka:
EMPNO ENAME DEPTNO DNAME
---------- --------------- ---------- --------------
1 EMP 1 1 DEPT 1
2 EMP 2 1 DEPT 1
Primijetimo i to da se ova simulacija još u nečemu ponaša kao "pravi" ROLLBACK TO SAVEPOINT.
Naime, ako druga transakcija pokuša zaključati redak koji je već zaključala prva transakcija, i ako
prva transakcija otključa taj redak sa ROLLBACK TO SAVEPOINT, redak i dalje ostaje zaključan za
drugu transakciju (međutim, neka treća transakcija bi sad mogla bez problema zaključati otključani
redak).
6. SPREČAVANJE "ZAČARANOG KRUGA" U VIŠEKORISNIČKOM RADU
Dakle, kako smo vidjeli prethodnoj točci, prvo ćemo napraviti "lokalni" database link:
CREATE DATABASE LINK local_db_link
CONNECT TO scott IDENTIFIED BY tiger
USING 'local_alias' -- alias na lokalnu bazu
/
Sada možemo mijenjati paket "emp_closed_loop". U odnosu na verziju iz točke 4, paket sada ima
proceduru "test_lock" navedenu (i) u specifikaciji, zato jer se procedura "test_lock" poziva iz
procedure "test" kao udaljena procedura. Procedura "test_lock" koristi naredbu
"RAISE_APPLICATION_ERROR (-20999, ...)" (koju procedura "test" ignorira, tj. ne smatra ju
greškom), da bi otključala redak koji je prethodno zaključala (sa SELECT ... FOR UPDATE):
CREATE OR REPLACE PACKAGE emp_closed_loop IS
PROCEDURE clear_plsql_tab;
PROCEDURE write_plsql_tab (
p_empno emp.empno%TYPE,
p_mgr emp.mgr%TYPE);
PROCEDURE test;
PROCEDURE test_lock (p_mgr emp.mgr%TYPE);
END emp_closed_loop;
/
CREATE OR REPLACE PACKAGE BODY emp_closed_loop IS
... KAO PRIJE ...
PROCEDURE test IS
l_mgr emp.mgr%TYPE;
l_empno emp.empno%TYPE;
BEGIN
FOR i IN 1..m_rows LOOP
l_empno := m_plsql_tab (i).empno;
l_mgr := m_plsql_tab (i).mgr;
WHILE l_mgr IS NOT NULL LOOP
BEGIN
emp_closed_loop.test_lock@local_db_link (l_mgr);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = -20999 THEN NULL;
ELSE RAISE;
END IF;
END;
SELECT mgr INTO l_mgr
FROM emp
WHERE empno = l_mgr;
IF l_mgr = l_empno THEN
RAISE_APPLICATION_ERROR
(-20004, 'Greška - zatvorena petlja!');
END IF;
END LOOP;
END LOOP;
END;
PROCEDURE test_lock (p_mgr emp.mgr%TYPE) IS
l_dummy NUMBER;
BEGIN
SELECT 1 INTO l_dummy
FROM emp
WHERE empno = p_mgr
FOR UPDATE NOWAIT;
RAISE_APPLICATION_ERROR (-20999, 'Nije važno');
EXCEPTION
WHEN OTHERS THEN
-- ORA-00054: resource busy and acquire with NOWAIT specified
IF SQLCODE = -54 THEN
RAISE_APPLICATION_ERROR
(-20004, 'Greška - moguća zatvorena petlja!');
ELSE
RAISE;
END IF;
END;
END emp_closed_loop;
/
No, moramo reći da i ovo rješenje ponekad može javiti "lažnu uzbunu", tj. javiti da je (možda) došlo do
zatvorene petlje, iako do toga nije došlo, kao npr. u sljedećem primjeru:
-- 1.SESIJA
UPDATE emp SET mgr = 6 WHERE empno = 2
/
-- 2.SESIJA
UPDATE emp SET mgr = 5 WHERE empno = 7
/
ERROR at line 1:
ORA-20004: Greška - moguća zatvorena petlja!
ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 70
ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 42
ORA-06512: at "SCOTT.AUS_EMP", line 2
ORA-04088: error during execution of trigger 'SCOTT.AUS_EMP'
Nažalost, ovakva "lažna uzbuna" ne može se spriječiti, jer sesija baze ne može točno "znati" što
druge sesije baze rade.
7. ZAKLJUČAK
Vidjeli smo da je sprečavanje pojave zatvorene petlje (u podacima koji imaju stablastu strukturu)
isključivo na strani Oracle baze (tj. bez pomoći programa na klijentu ili aplikacijskom serveru) "sve
samo ne jednostavno". Za neke bi to bio dovoljan razlog da odustanu od realizacije poslovnih pravila
na bazi i da, umjesto toga, realizaciju poslovnih pravila naprave na klijentu ili (još bolje) na
aplikacijskom serveru. Mi smo, međutim, mišljenja da treba pokušati realizirati poslovna pravila na
strani baze, jer se na taj način osigurava da su podaci u bazi konzistentni, neovisno od "vanjskih"
programa (programa na klijentu ili aplikacijskom serveru).
Takvo mišljenje zastupamo onda kad aplikacija radi samo sa jednom bazom podataka (npr. Oracle).
Ukoliko naša aplikacija mora raditi sa više različitih baza podataka, tada je vjerojatno bolji pristup da
se sloj poslovnih pravila implementira na aplikacijskom serveru, jer je (danas) teško napisati rješenja
koja bi bila primjenljiva za različite baze. Rješenje prikazano u ovom radu je strogo vezano za Oracle
bazu. Ako bi se o tom rješenju govorilo kao o predlošku ili uzorku (pattern), moglo bi se reći da ono ne
spada u arhitekturne predloške ili predloške za dizajniranje (architectural pattern, design pattern), već
u tzv. idiome (idioms), tj. rješenja koja su ovisna o određenom programskom jeziku [Larman 2002].
No, ako vjerujemo u tvrdnje i vizije koje navodi C.J.Date, možemo se nadati da će proizvođači
sustava za upravljanje bazama podataka u budućnosti veću pažnju posvetiti poslovnim pravilima,
odnosno donekle zanemarenoj komponenti relacijskog modela – komponenti integriteta podataka.
Ako se to ostvari, nama koji radimo aplikacije nad bazama podataka bit će olakšana realizacija
poslovnih pravila. Kako kaže Date (naslov jedne njegove knjige): "What Not How: The Business Rules
Approach to Application Development".
Literatura:
1 Standardni Oracle priručnici za baze 7, 8i, 9i i 10g
2 Steven Feuerstein: Oracle PL/SQL Programming, O'Reilly, 2002.
3 Barbara von Halle: Business Rules Applied, John Wiley & Sons, 2002.
4 Craig Larman: Applying UML and Patterns, 2. izdanje, Prentice Hall, 2002.
5 Bertrand Meyer: Object-Oriented Software Construction, 2.izdanje, Prentice Hall, 1997.
Neke web stranice vezane za poslovna pravila:
otn.oracle.com/consulting/idelivery/cdma/index.html (Oracle CDM RuleFrame)
www.brcommunity.com (uređuje Ron Ross, autoritet na području poslovnih pravila)
www.dulcian.com (produkt BRIM)
Podaci o autoru:
Zlatko Sirotić (Istra informatički inženjering d.o.o., Pula, e-mail: [email protected])