MongoDB / NoSQL: prowadzenie historii zmian dokumentu

137

Dość powszechnym wymaganiem w aplikacjach baz danych jest śledzenie zmian w jednej lub większej liczbie określonych jednostek w bazie danych. Słyszałem, że nazywa się to wersjonowaniem wierszy, tabelą dziennika lub tabelą historii (jestem pewien, że istnieją inne nazwy). Istnieje wiele sposobów podejścia do tego w RDBMS - możesz zapisać wszystkie zmiany ze wszystkich tabel źródłowych w jednej tabeli (więcej dziennika) lub mieć oddzielną tabelę historii dla każdej tabeli źródłowej. Masz również możliwość zarządzania logowaniem w kodzie aplikacji lub za pomocą wyzwalaczy bazy danych.

Zastanawiam się, jak wyglądałoby rozwiązanie tego samego problemu w bazie danych NoSQL / dokumentów (w szczególności MongoDB) i jak można by to rozwiązać w jednolity sposób. Czy byłoby to tak proste, jak utworzenie numerów wersji dokumentów i nigdy ich nadpisywanie? Tworzysz oddzielne kolekcje dokumentów „rzeczywistych” i „zarejestrowanych”? Jak wpłynie to na zapytania i wydajność?

W każdym razie, czy jest to typowy scenariusz z bazami danych NoSQL, a jeśli tak, czy istnieje wspólne rozwiązanie?

Phil Sandler
źródło
Z jakiego języka sterownika korzystasz?
Joshua Partogi
Jeszcze nie zdecydowano - wciąż majstruję i nawet nie sfinalizowaliśmy jeszcze wyboru zaplecza (chociaż MongoDB wygląda wyjątkowo prawdopodobne). Majstrowałem przy NoRM (C #) i podoba mi się niektóre nazwy związane z tym projektem, więc wydaje się, że jest to wybór.
Phil Sandler
2
Wiem, że to stare pytanie, ale dla każdego, kto szuka wersji z MongoDB, to pytanie SO jest powiązane i moim zdaniem z lepszymi odpowiedziami.
AWolf

Odpowiedzi:

110

Dobre pytanie, sam też się tym przyjrzałem.

Utwórz nową wersję przy każdej zmianie

Natknąłem się na moduł wersjonowania sterownika Mongoid dla Rubiego. Sam go nie używałem, ale z tego, co udało mi się znaleźć , dodaje numer wersji do każdego dokumentu. Starsze wersje są osadzone w samym dokumencie. Główną wadą jest to, że cały dokument jest duplikowany przy każdej zmianie , co spowoduje przechowywanie wielu zduplikowanych treści, gdy masz do czynienia z dużymi dokumentami. Takie podejście jest dobre, gdy masz do czynienia z dokumentami o małych rozmiarach i / lub nie aktualizujesz dokumentów zbyt często.

Przechowuj zmiany tylko w nowej wersji

Innym podejściem byłoby przechowywanie tylko zmienionych pól w nowej wersji . Następnie możesz „spłaszczyć” swoją historię, aby zrekonstruować dowolną wersję dokumentu. Jest to jednak dość złożone, ponieważ musisz śledzić zmiany w modelu oraz przechowywać aktualizacje i usuwać w taki sposób, aby aplikacja mogła zrekonstruować aktualny dokument. Może to być trudne, ponieważ masz do czynienia z dokumentami strukturalnymi zamiast płaskich tabel SQL.

Przechowuj zmiany w dokumencie

Każde pole może mieć również indywidualną historię. W ten sposób odtworzenie dokumentów do danej wersji jest znacznie łatwiejsze. W swojej aplikacji nie musisz jawnie śledzić zmian, po prostu utwórz nową wersję właściwości, gdy zmienisz jej wartość. Dokument mógłby wyglądać mniej więcej tak:

{
  _id: "4c6b9456f61f000000007ba6"
  title: [
    { version: 1, value: "Hello world" },
    { version: 6, value: "Foo" }
  ],
  body: [
    { version: 1, value: "Is this thing on?" },
    { version: 2, value: "What should I write?" },
    { version: 6, value: "This is the new body" }
  ],
  tags: [
    { version: 1, value: [ "test", "trivial" ] },
    { version: 6, value: [ "foo", "test" ] }
  ],
  comments: [
    {
      author: "joe", // Unversioned field
      body: [
        { version: 3, value: "Something cool" }
      ]
    },
    {
      author: "xxx",
      body: [
        { version: 4, value: "Spam" },
        { version: 5, deleted: true }
      ]
    },
    {
      author: "jim",
      body: [
        { version: 7, value: "Not bad" },
        { version: 8, value: "Not bad at all" }
      ]
    }
  ]
}

Oznaczanie części dokumentu jako usuniętej w wersji jest jednak nadal nieco niewygodne. Możesz wprowadzić statepole dla części, które można usunąć / przywrócić z aplikacji:

{
  author: "xxx",
  body: [
    { version: 4, value: "Spam" }
  ],
  state: [
    { version: 4, deleted: false },
    { version: 5, deleted: true }
  ]
}

Przy każdym z tych podejść można przechowywać aktualną i spłaszczoną wersję w jednej kolekcji, a dane historyczne w osobnej kolekcji. Powinno to skrócić czas wykonywania zapytań, jeśli interesuje Cię tylko najnowsza wersja dokumentu. Ale gdy potrzebujesz zarówno najnowszej wersji, jak i danych historycznych, musisz wykonać dwa zapytania zamiast jednego. Zatem wybór użycia jednej kolekcji lub dwóch oddzielnych kolekcji powinien zależeć od tego, jak często aplikacja potrzebuje wersji historycznych .

Większość z tych odpowiedzi to tylko zrzut mózgu z moich myśli, właściwie jeszcze tego nie próbowałem. Patrząc wstecz, pierwsza opcja jest prawdopodobnie najłatwiejszym i najlepszym rozwiązaniem, chyba że obciążenie związane z duplikowaniem danych jest bardzo istotne dla aplikacji. Druga opcja jest dość złożona i prawdopodobnie nie jest warta wysiłku. Trzecia opcja jest w zasadzie optymalizacją opcji drugiej i powinna być łatwiejsza do wdrożenia, ale prawdopodobnie nie jest warta wysiłku wdrożeniowego, chyba że naprawdę nie możesz skorzystać z opcji pierwszej.

Czekam na opinie na ten temat i rozwiązania problemu przez innych :)

Niels van der Rest
źródło
A co z przechowywaniem gdzieś delt, żebyś musiał spłaszczyć, żeby dostać dokument historyczny i zawsze mieć pod ręką aktualny?
jpmc26
@ jpmc26 Jest to podobne do drugiego podejścia, ale zamiast zapisywać delty, aby dostać się do najnowszych wersji, zapisujesz delty, aby dostać się do wersji historycznych. To, które podejście zastosować, zależy od tego, jak często będziesz potrzebować wersji historycznych.
Niels van der Rest
Możesz dodać akapit o używaniu dokumentu jako widoku do aktualnego stanu rzeczy i posiadaniu drugiego dokumentu jako dziennika zmian, który będzie śledził każdą zmianę, w tym sygnaturę czasową (początkowe wartości muszą pojawić się w tym dzienniku) - możesz następnie odtworzyć 'do dowolnego punktu w czasie i np. skoreluj, co się działo, gdy twój algorytm go dotknął lub zobacz, jak element był wyświetlany, gdy użytkownik go kliknął.
Manuel Arwed Schmidt
Czy wpłynie to na wydajność, jeśli indeksowane pola są reprezentowane jako tablice?
DmitriD,
@Wszystkie - czy mógłbyś udostępnić jakiś kod, aby to osiągnąć?
Pra_A
8

Częściowo zaimplementowaliśmy to w naszej witrynie i używamy „Wersji sklepu w oddzielnym dokumencie” (i osobnej bazie danych). Napisaliśmy niestandardową funkcję zwracającą różnice i ją przechowujemy. Nie jest to trudne i umożliwia automatyczne odzyskiwanie.

Amala
źródło
2
Czy mógłbyś udostępnić jakiś kod na ten sam temat? To podejście wygląda obiecująco
Pra_A
1
@smilyface - Integracja Spring Boot Javers jest najlepsza, aby to osiągnąć
Pra_A
@PAA - zadałem pytanie (prawie ta sama koncepcja). stackoverflow.com/questions/56683389/… Czy masz jakieś informacje na ten temat?
smilyface
7

Dlaczego nie zmiana w sklepie zmienia się w dokumencie ?

Zamiast przechowywać wersje dla każdej pary kluczy, bieżące pary kluczy w dokumencie zawsze reprezentują najnowszy stan, a „dziennik” zmian jest przechowywany w tablicy historii. Tylko te klucze, które uległy zmianie od czasu utworzenia, będą miały wpis w dzienniku.

{
  _id: "4c6b9456f61f000000007ba6"
  title: "Bar",
  body: "Is this thing on?",
  tags: [ "test", "trivial" ],
  comments: [
    { key: 1, author: "joe", body: "Something cool" },
    { key: 2, author: "xxx", body: "Spam", deleted: true },
    { key: 3, author: "jim", body: "Not bad at all" }
  ],
  history: [
    { 
      who: "joe",
      when: 20160101,
      what: { title: "Foo", body: "What should I write?" }
    },
    { 
      who: "jim",
      when: 20160105,
      what: { tags: ["test", "test2"], comments: { key: 3, body: "Not baaad at all" }
    }
  ]
}
Paul Taylor
źródło
2

Można mieć aktualną bazę danych NoSQL i historyczną bazę danych NoSQL. Codziennie będzie prowadzony nocny ETL. Ten ETL zarejestruje każdą wartość z sygnaturą czasową, więc zamiast wartości zawsze będą to krotki (pola wersjonowane). Nowa wartość zarejestruje tylko wtedy, gdy nastąpi zmiana wartości bieżącej, oszczędzając miejsce w procesie. Na przykład ten historyczny plik json bazy danych NoSQL może wyglądać następująco:

{
  _id: "4c6b9456f61f000000007ba6"
  title: [
    { date: 20160101, value: "Hello world" },
    { date: 20160202, value: "Foo" }
  ],
  body: [
    { date: 20160101, value: "Is this thing on?" },
    { date: 20160102, value: "What should I write?" },
    { date: 20160202, value: "This is the new body" }
  ],
  tags: [
    { date: 20160101, value: [ "test", "trivial" ] },
    { date: 20160102, value: [ "foo", "test" ] }
  ],
  comments: [
    {
      author: "joe", // Unversioned field
      body: [
        { date: 20160301, value: "Something cool" }
      ]
    },
    {
      author: "xxx",
      body: [
        { date: 20160101, value: "Spam" },
        { date: 20160102, deleted: true }
      ]
    },
    {
      author: "jim",
      body: [
        { date: 20160101, value: "Not bad" },
        { date: 20160102, value: "Not bad at all" }
      ]
    }
  ]
}
Paul Kar.
źródło
0

Dla użytkowników Pythona (python 3+ i wyżej oczywiście) istnieje HistoricalCollection , będący rozszerzeniem obiektu Collection pymongo.

Przykład z dokumentów:

from historical_collection.historical import HistoricalCollection
from pymongo import MongoClient
class Users(HistoricalCollection):
    PK_FIELDS = ['username', ]  # <<= This is the only requirement

# ...

users = Users(database=db)

users.patch_one({"username": "darth_later", "email": "[email protected]"})
users.patch_one({"username": "darth_later", "email": "[email protected]", "laser_sword_color": "red"})

list(users.revisions({"username": "darth_later"}))

# [{'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
#   'username': 'darth_later',
#   'email': '[email protected]',
#   '_revision_metadata': None},
#  {'_id': ObjectId('5d98c3385d8edadaf0bb845b'),
#   'username': 'darth_later',
#   'email': '[email protected]',
#   '_revision_metadata': None,
#   'laser_sword_color': 'red'}]

Pełne ujawnienie, jestem autorem pakietu. :)

Dash2TheDot
źródło