Zine.net online

Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc
w Szukaj

ucel.net

Modelowanie relacji n:m z zachowaniem historii danych

Czasami stosowany model danych wymaga, by nieaktualne rekordy nie zostawały usuwane z bazy danych, a zostały zachowane do późniejszej analizy. Zazwyczaj rozwiązuje się ten problem poprzez wersjonowanie rekordów. Są różne metody zachowywanie informacji o wersji, ja przedstawię tutaj metodę polegającą na rozszerzeniu tabeli o dwa pola określające okres ważności rekordu: ValidFrom i ValidTo.

Uwaga: w całym tekście stosuję założenie, że wszystkie operacje na danych odbywają się po stronie klienta. Oznacza to, że nie mogę stosować ani procedur ani triggerów. Ot takie dodatkowe utrudnienie, ale nic na to nie poradzę... Wszystkie klucze są elementami IDENTITY i są generowane automatycznie po stronie serwera.

 

Dla tak zdefiniowanej tabeli opreacje SELECT, INSERT, UPDATE i DELETE są zdefiniowane nieco inaczej niż w przypadku standardowym, poniżej przedstawiam odpowiedni kod:

private SqlDataAdapter GetAdapterTable1(DateTime version)
{
   SqlDataAdapter ad = new SqlDataAdapter();
   SqlCommand cmdSelect = new SqlCommand(
        @"SELECT Id, Name, ValidFrom, ValidTo
          FROM Table1
          WHERE @versionTime BETWEEN ValidFrom AND ValidTo"
, cnn);
   cmdSelect.Parameters.AddWithValue("@versionTime", version);
   SqlCommand cmdInsert = new SqlCommand(
        @"INSERT INTO Table1(Name, ValidFrom, ValidTo)
          VALUES (@Name, @ValidFrom)
          SELECT @Id=@@IDENTITY"
, cnn);
   cmdInsert.Parameters.Add("@Id", SqlDbType.Int, 4, "Id");
   cmdInsert.Parameters.Add("@Name", SqlDbType.VarChar, 50, "Name");
   cmdInsert.Parameters.AddWithValue("@ValidFrom", version);
   SqlCommand cmdDelete = new SqlCommand(
        @"UPDATE Table1 SET ValidTo=@ValidTo WHERE Id=@Id", cnn);
   cmdDelete.Parameters.Add("@Id", SqlDbType.Int, 4, "Id");
   cmdDelete.Parameters.AddWithValue("@ValidTo", version);
   SqlCommand cmdUpdate = new SqlCommand(
        @"UPDATE Table1 SET ValidTo=@ValidTo WHERE Id=@Id
          INSERT INTO Table1(Name, ValidFrom)
          VALUES (@Name, @ValidFrom)
          SELECT Id, Name, ValidFrom, ValidTo FROM Table1
          WHERE Id=SCOPE_IDENTITY()"
, cnn);
   cmdUpdate.Parameters.Add("@Id", SqlDbType.Int, 4, "Id");
   cmdUpdate.Parameters.Add("@Name", SqlDbType.VarChar, 50, "Name");
   cmdUpdate.Parameters.AddWithValue("@ValidFrom", version);
   cmdUpdate.Parameters.AddWithValue("@ValidTo", version);
   ad.SelectCommand = cmdSelect;
   ad.InsertCommand = cmdInsert;
   ad.UpdateCommand = cmdUpdate;
   ad.DeleteCommand = cmdDelete;
   return ad;
}


SELECT i INSERT chyba nie wymagają komentarza, DELETE polega tylko na zmianie daty ważności rekordu, UPDATE to skombinowane DELETE/INSERT/SELECT. W kodzie korzystam z tego, że w tabeli ustawione są predefiniowane wartości dla pól określających ważność rekordu.

Tak mimochodem wspomnę jeszcze (a’ propos tekstu Pawła), że użycie metody AddWithValue nie jest w tym miejscu „niebezpieczne”, jako że parametr version zostanie przez SQL Server przy kompilacji planu wykonania przedstawiony zawsze jako @version datetime.

Ale miało być o relacjach, prawda? No więc co się dzieje, jeśli nasz schemat nieco skomplikujemy:

 

Pierwsza sprawa: właściwości relacji należy tak skonfigurować, by oprócz relacji definiowany był również klucz obcy, a to dlatego, że tylko dla klucza można wymusić kaskadową aktualizację danych – dla reguły Update. Kiedy skorzystamy z kaskadowej aktualizacji? Przy wstawianiu nowego wiersza z tabeli Table1 bądź Table2 – po wstawieniu zawartość kolumny ID odpowiedniego wiersza zostanie zaktualizowana i rozpropagowana do odpowiednich pozycji w tabeli Relation.

Pozostaje przypadek najtrudniejszy do zrealizowania – mianowicie UPDATE. Zakładając, że aktualizowane są tylko dane w tabeli, zachowane pomiędzy wersjami muszą zostać informacje o strukturze relacji. Innymi słowy update Table1 lub Table2 wiąże się ze zduplikowaniem odpowiednich pozycji w tabeli Relation i wstawieniem tam odpowiednich wartości ID. Zadanie wydawałoby się nietrywialne ale wykonywane praktycznie automatycznie.

Po operacji UPDATE zdefiniowanej jak wyżej stary rekord „znika” z tabeli a pojawia się nowy, z poprawionym ID. Co ciekawe, dla wiersza jest to traktowane jako operacja UPDATE, więc odpowiedni wiersz w tabeli relacji zostaje także automatycznie zaktualizowany. I tu kryje się cały trick aktualizacji danych: dla synchronizacji tabeli relacji definiujemy tylko dwie komendy (nie definiujemy DELETE bo z definicji nie chcemy tracić informacji), ale obie składniowo są identyczne, tj. zawierają komendę INSERT:

SqlCommand cmdUpdate = new SqlCommand(
     @"INSERT INTO Relation([OldId], [NewId])
       VALUES (@OldId, @NewId)
       SELECT [Id], [OldId], [NewId] FROM Relation
       WHERE Id=@@IDENTITY"
, cnn);


W ten sposób stare wiersze określające strukturę relacji w bazie nie zostaną nadpisane i będzie do nich dostęp kiedy przyjdzie taka potrzeba.
Nie opisuję tutaj jakiegoś konkretnego systemu, ale mam nadzieję, że powyższe rozważania mogą okazać się pomocne dla kogoś, kto będzie potrzebował stworzyć własny system wersjonowania danych w bazie.

Opublikowane 6 grudnia 2007 14:04 przez ucel

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

 

brejk said:

Przydałoby się jeszcze wiedzieć, które rekordy są wersjami tego samego rekordu ;-) Chyba, że takim jednoznacznym znacznikiem-niezmiennikiem jest Name... W hurtowniach danych struktury tabel z przechowywaniem historii mają dwa klucze: sztuczny z identity (u Ciebie ID) i biznesowy - identyfikujący wiersz (identyczny dla różnych wersji tego samego wiersza).

grudnia 6, 2007 14:58
 

ucel said:

Dokładnie tak jest. Ten schemat (tylko w postaci bardziej złożonej) użyłem do modelowania metadanych dla tabel (Table1) i kolumn (Table2) bazy danych. Wystarczy, że niezmiennikiem jest Name w pierwszej tabeli. Name2 mogą się powtarzać, ale z założeniem, że pozostają unikalne na poziomie relacji z Table1. W ten sposób jesteś w stanie śledzić historię obiektu (wiersza) poprzez jego powiązanie z unikalnie identyfikowalnym obiektem nadrzędnym.

grudnia 6, 2007 15:26
 

ucel said:

Historia dla wiersza z Table1:

SELECT *  FROM Table1 WHERE Name=@Name ORDER BY ValidFrom

Historia dla wiersza z Table2

SELECT * FROM Table2 T2

INNER JOIN Relation R ON T2.Id=R.Id2

INNER JOIN Table1 T1 ON R.Id1=T1.Id

WHERE T1.Name = @t1Name AND T2.Name2 = @t2Name

ORDER BY T2.ValidFrom

grudnia 6, 2007 15:32

Co o tym myślisz?

(wymagane) 
(opcjonalne)
(wymagane) 

  
Wprowadź kod: (wymagane)
Wyślij
W oparciu o Community Server (Personal Edition), Telligent Systems