Jak kontrolować wersję rekordu w bazie danych

176

Powiedzmy, że mam rekord w bazie danych i że zarówno administrator, jak i zwykli użytkownicy mogą dokonywać aktualizacji.

Czy ktoś może zasugerować dobre podejście / architekturę, jak kontrolować wersję każdej zmiany w tej tabeli, aby można było przywrócić rekord do poprzedniej wersji.

Niels Bosma
źródło

Odpowiedzi:

164

Powiedzmy, że masz plik FOO tabelę, którą mogą aktualizować administratorzy i użytkownicy. W większości przypadków możesz pisać zapytania do tabeli FOO. Szczęśliwe dni.

Następnie utworzyłbym FOO_HISTORYtabelę. Zawiera wszystkie kolumny FOOtabeli. Klucz podstawowy jest taki sam jak FOO plus kolumna RevisionNumber. Istnieje klucz obcy od FOO_HISTORYdo FOO. Możesz również dodać kolumny związane z wersją, takie jak UserId i RevisionDate. Wypełniaj numery wersji w coraz większym stopniu we wszystkich *_HISTORYtabelach (tj. Z sekwencji Oracle lub jej odpowiednika). Nie należy polegać na jednej zmianie w ciągu sekundy (tj. Nie należy wprowadzać RevisionDateklucza podstawowego).

Teraz za każdym razem, gdy aktualizujesz FOO, tuż przed aktualizacją wstawiasz stare wartościFOO_HISTORY . Robisz to na pewnym podstawowym poziomie w swoim projekcie, aby programiści nie mogli przypadkowo przegapić tego kroku.

Jeśli chcesz usunąć wiersz FOO, masz kilka możliwości. Albo wykonaj kaskadę i usuń całą historię, albo wykonaj usuwanie logiczne, oznaczając FOOjako usunięte.

To rozwiązanie jest dobre, gdy w dużej mierze interesują Cię aktualne wartości, a tylko sporadycznie historia. Jeśli zawsze potrzebujesz historii, możesz umieścić efektywne daty rozpoczęcia i zakończenia oraz zachować wszystkie zapisy w FOOsobie. Każde zapytanie musi następnie sprawdzić te daty.

W W.
źródło
1
Możesz przeprowadzić aktualizację tabeli inspekcji za pomocą wyzwalaczy bazy danych, jeśli warstwa dostępu do danych nie obsługuje jej bezpośrednio. Ponadto nie jest trudno zbudować generator kodu, który tworzy wyzwalacze wykorzystujące introspekcję z systemowego słownika danych.
ConcernedOfTunbridgeWells
44
Poleciłbym faktycznie wstawić nowe dane, a nie poprzednie, aby tabela historii zawierała wszystkie dane. Mimo, że przechowuje dane, które są ponownie gromadzone, eliminuje specjalne przypadki wymagane przy wyszukiwaniu w obu tabelach, gdy wymagane są dane historyczne.
Nerdfest
6
Osobiście radziłbym nie usuwać niczego (odłożyć to do określonej czynności porządkowej) i mieć kolumnę „typ akcji”, aby określić, czy jest to wstawianie / aktualizowanie / usuwanie. Aby usunąć, kopiujesz wiersz w normalny sposób, ale wstawiasz „usuń” w kolumnie typu działania.
Neil Barnwell
3
@Hydrargyrum Tabela zawierająca aktualne wartości będzie działać lepiej niż widok tabeli historycznej. Możesz również chcieć zdefiniować klucze obce odwołujące się do bieżących wartości.
WW.
2
There is a foreign key from FOO_HISTORY to FOO': zły pomysł, chciałbym usunąć wpisy z foo bez zmiany historii. Tabela historii powinna być przeznaczona tylko do wstawiania podczas normalnego użytkowania.
Jasen
46

Myślę, że szukasz wersjonowania zawartości rekordów bazy danych (tak jak robi to StackOverflow, gdy ktoś edytuje pytanie / odpowiedź). Dobrym punktem wyjścia może być przyjrzenie się modelowi bazy danych, który używa wersji śledzenie .

Najlepszym przykładem, jaki przychodzi na myśl, jest MediaWiki, silnik Wikipedii. Porównaj tutaj diagram bazy danych , zwłaszcza tabelę zmian .

W zależności od technologii, z których korzystasz, będziesz musiał znaleźć dobre algorytmy porównywania / scalania.

Sprawdź to pytanie, jeśli dotyczy .NET.

CMS
źródło
30

W świecie BI możesz to osiągnąć, dodając startDate i endDate do tabeli, którą chcesz wersjonować. Po wstawieniu pierwszego rekordu do tabeli, startDate jest wypełniana, ale endDate ma wartość null. Wstawiając drugi rekord, aktualizujesz również datę końcową pierwszego rekordu o wartość startDate drugiego rekordu.

Jeśli chcesz wyświetlić bieżący rekord, wybierz ten, w którym endDate ma wartość null.

Jest to czasami nazywane wolno zmieniającym się wymiarem typu 2 . Zobacz także TupleVersioning

Dave Neeley
źródło
Czy przy takim podejściu mój stół nie powiększy się całkiem?
Niels Bosma
1
Tak, ale możesz sobie z tym poradzić poprzez indeksowanie i / lub partycjonowanie tabeli. Poza tym będzie tylko kilka dużych stołów. Większość będzie znacznie mniejsza.
ConcernedOfTunbridgeWells
Jeśli się nie mylę, jedyną wadą jest to, że ogranicza zmiany do jednej sekundy, prawda?
pimbrouwers
@pimbrouwers tak, ostatecznie zależy to od precyzji pól i funkcji, która je zapełnia.
Dave Neeley
9

Uaktualnij do SQL 2008.

Spróbuj użyć funkcji śledzenia zmian SQL w SQL 2008. Zamiast oznaczania czasu i hacków kolumn reliktów, możesz użyć tej nowej funkcji do śledzenia zmian danych w bazie danych.

MSDN SQL 2008 Śledzenie zmian

D3vtr0n
źródło
7

Chciałem tylko dodać, że dobrym rozwiązaniem tego problemu jest użycie bazy danych Temporal . Wielu dostawców baz danych oferuje tę funkcję po wyjęciu z pudełka lub za pośrednictwem rozszerzenia. Z powodzeniem korzystałem z tymczasowego rozszerzenia tabeli w PostgreSQL, ale inni też je mają. Za każdym razem, gdy aktualizujesz rekord w bazie danych, baza danych zachowuje również poprzednią wersję tego rekordu.

wuher
źródło
6

Dwie opcje:

  1. Miej tabelę historii - wstaw stare dane do tej tabeli historii, gdy oryginał zostanie zaktualizowany.
  2. Tabela audytu - przechowuj wartości przed i po - tylko dla zmodyfikowanych kolumn w tabeli audytu wraz z innymi informacjami, takimi jak kto i kiedy zaktualizował.
alok
źródło
5

Możesz przeprowadzić inspekcję tabeli SQL za pomocą wyzwalaczy SQL. Z wyzwalacza można uzyskać dostęp do 2 specjalnych tabel ( wstawionych i usuniętych ). Tabele te zawierają dokładne wiersze, które zostały wstawione lub usunięte przy każdej aktualizacji tabeli. W wyzwalaczu SQL możesz pobrać te zmodyfikowane wiersze i wstawić je do tabeli kontroli. Takie podejście oznacza, że ​​audyt jest przejrzysty dla programisty; nie wymagające od nich żadnego wysiłku ani wiedzy wdrożeniowej.

Dodatkową zaletą tego podejścia jest to, że inspekcja będzie odbywać się niezależnie od tego, czy operacja sql odbyła się za pośrednictwem bibliotek DLL dostępu do danych, czy też poprzez ręczne zapytanie SQL; (ponieważ audyt jest przeprowadzany na samym serwerze).

Doktorze Jones
źródło
3

Nie mówisz, jaka baza danych, a ja nie widzę tego w tagach postów. Jeśli chodzi o Oracle, mogę polecić podejście wbudowane w Projektant: użyj tabel dziennika . Jeśli jest to dla jakiejkolwiek innej bazy danych, cóż, w zasadzie polecam ten sam sposób ...

Sposób, w jaki to działa, w przypadku, gdy chcesz go powielić w innej bazie danych, lub jeśli chcesz to zrozumieć, jest taki, że dla tabeli jest również utworzona tabela-cień, po prostu zwykła tabela bazy danych, z tymi samymi specyfikacjami pól , plus kilka dodatkowych pól: np. jaka czynność została ostatnio podjęta (ciąg znaków, typowe wartości „INS” dla wstawienia, „UPD” dla aktualizacji i „DEL” dla usunięcia), data i godzina, kiedy akcja miała miejsce i identyfikator użytkownika, który to zrobił to.

Poprzez wyzwalacze każde działanie do dowolnego wiersza w tabeli wstawia nowy wiersz w tabeli dziennika z nowymi wartościami, określającą, jakie działanie zostało podjęte, kiedy i przez jakiego użytkownika. Nigdy nie usuwasz żadnych wierszy (przynajmniej nie w ciągu ostatnich kilku miesięcy). Tak, urosną duże, łatwo miliony wierszy, ale możesz łatwo śledzić wartość dowolnego rekordu w dowolnym momencie od rozpoczęcia kronikowania lub ostatniego usunięcia starych wierszy dziennika i tego, kto dokonał ostatniej zmiany.

W Oracle wszystko, czego potrzebujesz, jest generowane automatycznie jako kod SQL, wszystko, co musisz zrobić, to skompilować / uruchomić; i jest dostarczany z podstawową aplikacją CRUD (właściwie tylko "R") do sprawdzenia.

Bart
źródło
2

Ja też robię to samo. Tworzę bazę danych dla planów lekcji. Plany te wymagają atomowej elastyczności wersjonowania zmian. Innymi słowy, każda zmiana, nieważne jak mała, w planach lekcji musi być dozwolona, ​​ale stara wersja musi również pozostać nienaruszona. W ten sposób twórcy lekcji mogą edytować plany lekcji, gdy uczniowie ich używają.

Sposób, w jaki to działałoby, polega na tym, że po zakończeniu lekcji przez ucznia wyniki są dołączane do ukończonej wersji. Jeśli zostanie wprowadzona zmiana, ich wynik będzie zawsze wskazywał na ich wersję.

W ten sposób, jeśli kryteria lekcji zostaną usunięte lub przeniesione, ich wyniki nie ulegną zmianie.

Sposób, w jaki obecnie to robię, polega na obsłudze wszystkich danych w jednej tabeli. Normalnie miałbym tylko jedno pole id, ale w tym systemie używam id i sub_id. Sub_id zawsze pozostaje w wierszu, podczas aktualizacji i usuwania. Identyfikator jest automatycznie zwiększany. Oprogramowanie planu lekcji będzie łączyło się z najnowszym sub_id. Wyniki uczniów będą prowadziły do ​​identyfikatora. Dołączyłem również sygnaturę czasową do śledzenia zmian, ale nie jest to konieczne do obsługi wersjonowania.

Jedna rzecz, którą mógłbym zmienić po przetestowaniu, to użyć wspomnianej wcześniej idei endDate null. W moim systemie, aby znaleźć najnowszą wersję, musiałbym znaleźć max (id). Drugi system po prostu szuka endDate = null. Nie jestem pewien, czy korzyści wychodzą z innego pola daty.

Moje dwa centy.

Jordania
źródło
2

Podczas gdy @WW. odpowiedź jest dobrą odpowiedzią innym sposobem jest utworzenie kolumny wersji i trzymanie wszystkich wersji w tej samej tabeli.

Przy jednym stoliku podejdź do Ciebie:

  • Użyj flagi, aby wskazać najnowszy ala Word Press
  • LUB zrób paskudną wersję większą niż outer join.

Przykładowy kod SQL outer joinmetody używającej numerów wersji to:

SELECT tc.*
FROM text_content tc
LEFT OUTER JOIN text_content mc ON tc.path = mc.path
AND mc.revision > tc.revision
WHERE mc.revision is NULL 
AND tc.path = '/stuff' -- path in this case is our natural id.

Zła wiadomość jest taka, że ​​powyższe wymaga, outer joina łączenia zewnętrzne mogą być powolne. Dobra wiadomość jest taka, że ​​tworzenie nowych wpisów jest teoretycznie tańsze, ponieważ można to zrobić w jednej operacji zapisu bez transakcji (zakładając, że baza danych jest atomowa).

Przykładem tworzenia nowej wersji dla '/stuff'może być:

INSERT INTO text_content (id, path, data, revision, revision_comment, enabled, create_time, update_time)
(
SELECT
(md5(random()::text)) -- {id}
, tc.path
, 'NEW' -- {data}
, (tc.revision + 1)
, 'UPDATE' -- {comment}
, 't' -- {enabled}
, tc.create_time
, now() 
FROM text_content tc
LEFT OUTER JOIN text_content mc ON tc.path = mc.path
AND mc.revision > tc.revision
WHERE mc.revision is NULL 
AND tc.path = '/stuff' -- {path}
)

Wstawiamy przy użyciu starych danych. Jest to szczególnie przydatne, jeśli chcesz zaktualizować tylko jedną kolumnę i uniknąć optymistycznego blokowania i / lub transakcji.

Podejście z flagami i podejście do tabeli historii wymagają wstawienia / aktualizacji dwóch wierszy.

Inną zaletą outer joinpodejścia opartego na numerach wersji jest to, że zawsze możesz później zmienić podejście do podejścia z wieloma tabelami z wyzwalaczami, ponieważ wyzwalacz powinien zasadniczo wykonywać coś podobnego do powyższego.

Adam Gent
źródło
2

Alok zasugerował Audit tablepowyżej, chciałbym to wyjaśnić w moim poście.

W moim projekcie zaadoptowałem ten bezschematowy projekt jednej tabeli.

Schemat:

  • id - INTEGER AUTO INCREMENT
  • nazwa użytkownika - STRING
  • nazwa tabeli - STRING
  • stara wartość - TEKST / JSON
  • nowa wartość - TEKST / JSON
  • utworzony - DATETIME

Ta tabela może przechowywać historyczne rekordy dla każdej tabeli w jednym miejscu, z pełną historią obiektu w jednym rekordzie. Tę tabelę można wypełnić za pomocą wyzwalaczy / zaczepów, w których zmieniają się dane, przechowując stare i nowe migawki wartości wiersza docelowego.

Plusy z tym projektem:

  • Mniejsza liczba tabel do zarządzania w celu zarządzania historią.
  • Przechowuje pełną migawkę każdego starego i nowego stanu każdego wiersza.
  • Łatwe wyszukiwanie na każdym stole.
  • Potrafi tworzyć partycje według tabeli.
  • Może definiować zasady przechowywania danych dla każdej tabeli.

Wady tego projektu:

  • Rozmiar danych może być duży, jeśli system często się zmienia.
Hassan Farid
źródło