Podaci vrede onoliko koliko su tacni i bezbedni. Pokusajmo da obezbedimo tacnost podataka. Videcemo kako iz tacnosti proizlazi i dobar deo bezbednosti.
Code:
DROP TABLE Status_History
GO
CREATE TABLE Status_History
(
ID int NOT NULL -- ID je id nekog entiteta za koga pratimo promenu statusa
, StatusID int NOT NULL
, DatumOd DateTime NOT NULL
, StariStatus int NULL
, StariDatumOD DateTime NULL
, CONSTRAINT PK_Status_History PRIMARY KEY (ID, DatumOd, StatusID)
, CONSTRAINT ck_Status_History_StariDatumPreNovog CHECK (StariDatumOD < DatumOd)
)
GO
Ako hocemo da nam (ID, StariStatus, StariDatumOD) zavisi od prethodnog reda, mozemo da postavimo FOREIGN KEY, tako da StariStatus i StariDatumOD mora da postoji vec u tabeli da bismo ga dodelili novom redu. Ovo neodoljivo podseca na jedan od nacina prikazivanja hijerarhija u relacionim sistemima. I jeste, svaki red je roditelj prethodnom redu. U 'obicnoj' hijerrahiji jedan red moze imati vise dece, a pokazacemo da ovde jedan red ne treb da ima vise od jednog deteta.
Code:
ALTER TABLE Status_History
ADD CONSTRAINT Status_History_vea_Izmedju_Redova
FOREIGN KEY (ID, StariDatumOD, StariStatus )
REFERENCES Status_History (ID,DatumOd, StatusID )
Sada tabela nece prihvatiti unos u StariStatus ili StariDatumOd koji ne postoje u nekom od prethodnih redova, a sve za posmatrani entitet.
Unesimo neke podatke, ispravne ili neispravne
Code:
-- Za prvi red, nemamo StariStatus niti StariDatumOD pa cemo ih ostaviti da budu NULL
INSERT INTO Status_History
( ID, StatusID, DatumOD, StariStatus, StariDatumOD)
VALUES ( 1, 0, '20110117' , NULL , NULL )
;
-- Sve je OK -- (1 row(s) affected)
SELECT * FROM Status_History;
ID StatusID DatumOd StariStatus StariDatumOD
----------- ----------- ----------------------- ----------- -----------------------
1 0 2011-01-17 00:00:00.000 NULL NULL
(1 row(s) affected)
Sta ce se desiti ako pokusamo da unesemo nekakv StariStatus i/ili StariDatumOD?
Code:
-- Pocetni status za entitet ID = 2
INSERT INTO Status_History
( ID, StatusID, DatumOD, StariStatus, StariDatumOD)
VALUES ( 2, 1, '20110117' , 0 , '20110112' )
;
-- nece moci:
Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the FOREIGN KEY SAME TABLE constraint "Status_History_vea_Izmedju_Redova". The conflict occurred in database "DMA", table "dbo.Status_History".
The statement has been terminated.
FOREIGN KEY nas je spasao od loseg unosa. tek da se zna

Medjutim , daleko od toga da smo bezbedni. Pogledajte ovo;
Code:
-- medjutim, nepotpuna kombinacija prolazi, a ne bi trebalo:
INSERT INTO Status_History
( ID, StatusID, DatumOD, StariStatus, StariDatumOD)
VALUES ( 2, 1, '20110117' , 0 , NULL )
;
-- proslo je, a ne bi trebalo (1 row(s) affected)
SELECT * FROM Status_History;
ID StatusID DatumOd StariStatus StariDatumOD
----------- ----------- ----------------------- ----------- -----------------------
1 0 2011-01-17 00:00:00.000 NULL NULL
2 1 2011-01-17 00:00:00.000 0 NULL
(2 row(s) affected)
-- eliminsacu red za Id = 2, da nam ne smeta u daljem testiranju.
-- Treba nam nesto da sprecimo unos 'starih' podataka za prvi red entiteta.
DELETE Status_History
WHERE ID = 2
SELECT * FROM Status_History;
ID StatusID DatumOd StariStatus StariDatumOD
----------- ----------- ----------------------- ----------- -----------------------
1 0 2011-01-17 00:00:00.000 NULL NULL
(1 row(s) affected)
Mozemo reci ovako pravilo: StariStatus i StariDatumOD su ili ona NULL ili oba su NOT NULL. To bi sprecilo anomalije koje smo upravo videli.
Code:
ALTER TABLE Status_History
ADD CONSTRAINT ck_Status_History_StariStatusDatumOD_NULLability
CHECK (
(StariStatus IS NULL AND StariDatumOD IS NULL)
OR
(StariStatus IS NOT NULL AND StariDatumOD IS NOT NULL)
)
-- Pokusajmo nekompletan unos za StariStatus StariDatumOD,
-- Prokusajmo isti INSERT koji je bio prosao, a nije trebao
INSERT INTO Status_History
( ID, StatusID, DatumOD, StariStatus, StariDatumOD)
VALUES ( 2, 1, '20110117' , 0 , NULL )
;
-- Ovaj put nije proslo:
Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the CHECK constraint "ck_Status_History_StariStatusDatumOD_NULLability". The conflict occurred in database "DMA", table "dbo.Status_History".
The statement has been terminated.
-- Ovo naravno prolazi
INSERT INTO Status_History
( ID, StatusID, DatumOD, StariStatus, StariDatumOD)
VALUES ( 2, 1, '20110117' , NULL , NULL )
;
-- (1 row(s) affected)
SELECT * FROM Status_History;
ID StatusID DatumOd StariStatus StariDatumOD
----------- ----------- ----------------------- ----------- -----------------------
1 0 2011-01-17 00:00:00.000 NULL NULL
2 1 2011-01-17 00:00:00.000 NULL NULL
(2 row(s) affected)
Pokusajmo jos nekoliko redova:
Code:
-- DatumOd = Staridatum, ocekujem gresku
INSERT INTO Status_History
( ID, StatusID, DatumOD, StariStatus, StariDatumOD)
VALUES ( 1, 1 , '2011-01-17', 0, '2011-01-17')
;
-- Naravno da ne moze:
Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the CHECK constraint "ck_Status_History_StariDatumPreNovog". The conflict occurred in database "DMA", table "dbo.Status_History".
The statement has been terminated.
-- DatumOd < Staridatum, StariStatus izpravan
INSERT INTO Status_History
( ID, StatusID, DatumOD , StariStatus, StariDatumOD)
VALUES ( 1, 1 , '2011-01-18', 0, '2011-01-17')
;
-- naravno da prolazi
SELECT * FROM Status_History ORDER BY ID, DatumOD
;
ID StatusID DatumOd StariStatus StariDatumOD
----------- ----------- ----------------------- ----------- -----------------------
1 0 2011-01-17 00:00:00.000 NULL NULL
1 1 2011-01-18 00:00:00.000 0 2011-01-17 00:00:00.000
2 1 2011-01-17 00:00:00.000 NULL NULL
(3 row(s) affected)
FOREIGN KEY smo vec testirali, ne dozvoljava da se unese nepostojeca kombinacija (ID, StariStatus, StariDatumOD). Medjutim, jos uvek mogu da unesem pogresan podatak. Evo:
Code:
-- datumi su OK, Stari status postoji, ali nije dobar,
-- trebalo bi da bude StariStatus = 1
-- jer poslednji uneti red za ID=1 ima StatusID = 1
-- Ovo prolazi, a ne bi trebalo:
INSERT INTO Status_History
( ID, StatusID, DatumOD , StariStatus, StariDatumOD)
VALUES ( 1, 3 , '2011-01-19', 0, '2011-01-17')
;
-- proslo je, a ne bi trebalo -- (1 row(s) affected)
SELECT * FROM Status_History ORDER BY ID, DatumOD
;
ID StatusID DatumOd StariStatus StariDatumOD
----------- ----------- ----------------------- ----------- -----------------------
1 0 2011-01-17 00:00:00.000 NULL NULL
1 1 2011-01-18 00:00:00.000 0 2011-01-17 00:00:00.000
1 3 2011-01-19 00:00:00.000 0 2011-01-17 00:00:00.000
2 1 2011-01-17 00:00:00.000 NULL NULL
(4 row(s) affected)
Za ID =1, StaiStaus ima vrednost 0 za dva reda, a ne bi trebalo. Ako malo bolje razmislimo, kombinacija (ID, StariStatus, StariDatumOD) treba da je UNIQUE. U tom slucaju, svaki red moze da ima najvise jednog sledbenika.
Ovo je srce resenja. Vec znamo da svaki red ima tacno jednog roditelja - FK se brine o tome, kao u hijerarhiji. Ako istovremeo svaki red moze da ima najvise jednog sledbenika i svaki red ima tacno jednog prethodnika, ond gar antujemo tacan redosled i neprekidnost istorijskog niza. A to je ono sto smo hteli da psotignemo na pocetku.
Code:
-- Obrisacu poslednji red, pa da postavimo jos jedno ogranicenje:
DELETE Status_History
WHERE ID=1 AND DatumOd = '2011-01-19'
-- Opet imamo ciste podatke:
SELECT * FROM Status_History ORDER BY ID, DatumOD
;
ID StatusID DatumOd StariStatus StariDatumOD
----------- ----------- ----------------------- ----------- -----------------------
1 0 2011-01-17 00:00:00.000 NULL NULL
1 1 2011-01-18 00:00:00.000 0 2011-01-17 00:00:00.000
2 1 2011-01-17 00:00:00.000 NULL NULL
(3 row(s) affected)
Ovo je kljucno granicenje. Uz prethodno postavljene uslove ovim kompletiramo zadatak.
Code:
-- Dodajmo ogranicenje:
ALTER TABLE Status_History
ADD CONSTRAINT unique_StariDatumOD_StariStatus
UNIQUE (ID, StariDatumOD, StariStatus ) -- iste kolone, isti redosled kao u FK
Ovo resenje sa UNIQUE ogranicenjem vazi samo za MS SQL. MS SQL dozvoljava NULL vrednosti u UNIQUE ogranicenju, ali najvise jednu NULL vrednost, sto nama i treba. SQL standard dozvoljava visestruke NULL vrednosti i UNIQUE ogranicenjima, MS SQL ovde odstupa od standarda, ali nam to olaksava zivot u ovom slucaju. U relacionim sistemima koji u ovom slucaju prate ANSI standard za SQL ovo se mora resiti nekako drugacije. Nije nemoguce, cak smo to u Accesu uspeli da napravimo. Ako moze u Accessu, moze sigurno i u ORACLE ili Postgress ili Firebird. To ce neko iz te ekipe da razradi.
Da testiramo sta imamo:
Code:
-- Pokusajmo sada da unesmo pogresan podatak:
-- datumi su OK, Stari status postoji, ali nije dobar,
-- trebalo bi da bude StariStatus = 1
-- jer poslednji uneti red za ID=1 ima StatusID = 1
-- Ovo ne bi trebalo: da prodje ovog puta:
INSERT INTO Status_History
( ID, StatusID, DatumOD , StariStatus, StariDatumOD)
VALUES ( 1, 3 , '2011-01-19', 0, '2011-01-17')
;
-- i nije proslo:
Msg 2627, Level 14, State 1, Line 1
Violation of UNIQUE KEY constraint 'unique_StariDatumOD_StariStatus'. Cannot insert duplicate key in object 'dbo.Status_History'.
The statement has been terminated.
Tako se prati istorija promene odabranog atributa za odabrani entitet. U ovom slucaju atribut je bio StatusID, entitet je bilo sta, predstavljeno kolonom ID. DatumOd i StariDatumOd pripadaju metodi i oni ce uvek biti priosutni, bez obzira na entitet i atribut. Entitet ne mora da bude predstavljen jednom kolonom, moze biti i slozeni kljuc.