MongoDB / NoSQL: Dokumentverlaufsverlauf beibehalten

134

Eine häufig verwendete Anforderung in Datenbankanwendungen besteht darin, Änderungen an einer oder mehreren bestimmten Entitäten in einer Datenbank zu verfolgen. Ich habe gehört, dass dies als Zeilenversionierung, Protokolltabelle oder Verlaufstabelle bezeichnet wird (ich bin sicher, dass es andere Namen dafür gibt). Es gibt verschiedene Möglichkeiten, dies in einem RDBMS zu tun: Sie können alle Änderungen aus allen Quelltabellen in eine einzelne Tabelle (eher ein Protokoll) schreiben oder für jede Quelltabelle eine separate Verlaufstabelle haben. Sie haben auch die Möglichkeit, entweder die Anmeldung im Anwendungscode oder über Datenbank-Trigger zu verwalten.

Ich versuche zu überlegen, wie eine Lösung für dasselbe Problem in einer NoSQL / Dokument-Datenbank (insbesondere MongoDB) aussehen würde und wie sie auf einheitliche Weise gelöst werden würde. Wäre es so einfach, Versionsnummern für Dokumente zu erstellen und diese niemals zu überschreiben? Separate Sammlungen für "echte" oder "protokollierte" Dokumente erstellen? Wie würde sich dies auf die Abfrage und die Leistung auswirken?

Wie auch immer, ist dies ein häufiges Szenario mit NoSQL-Datenbanken, und wenn ja, gibt es eine gemeinsame Lösung?

Phil Sandler
quelle
Welchen Sprachtreiber verwenden Sie?
Joshua Partogi
Noch nicht entschieden - noch gebastelt und noch nicht einmal die Auswahl der Backends abgeschlossen (obwohl MongoDB äußerst wahrscheinlich erscheint). Ich habe an NoRM (C #) herumgebastelt und ich mag einige der Namen, die mit diesem Projekt verbunden sind, daher scheint es sehr wahrscheinlich die Wahl zu sein.
Phil Sandler
2
Ich weiß, dass dies eine alte Frage ist, aber für alle, die nach einer Versionierung mit MongoDB suchen, ist diese SO-Frage verwandt und meiner Meinung nach mit besseren Antworten.
AWolf

Antworten:

107

Gute Frage, ich habe mich auch selbst darum gekümmert.

Erstellen Sie bei jeder Änderung eine neue Version

Ich bin auf das Versionierungsmodul des Mongoid-Treibers für Ruby gestoßen. Ich habe es selbst nicht verwendet, aber nach allem, was ich finden konnte , fügt es jedem Dokument eine Versionsnummer hinzu. Ältere Versionen sind in das Dokument selbst eingebettet. Der Hauptnachteil besteht darin, dass bei jeder Änderung das gesamte Dokument dupliziert wird , was dazu führt, dass beim Umgang mit großen Dokumenten viel doppelter Inhalt gespeichert wird. Dieser Ansatz ist jedoch in Ordnung, wenn Sie mit kleinen Dokumenten arbeiten und / oder Dokumente nicht sehr oft aktualisieren.

Speichern Sie Änderungen nur in einer neuen Version

Ein anderer Ansatz wäre, nur die geänderten Felder in einer neuen Version zu speichern . Anschließend können Sie Ihren Verlauf reduzieren, um eine beliebige Version des Dokuments zu rekonstruieren. Dies ist jedoch recht komplex, da Sie Änderungen in Ihrem Modell verfolgen und Aktualisierungen und Löschvorgänge so speichern müssen, dass Ihre Anwendung das aktuelle Dokument rekonstruieren kann. Dies kann schwierig sein, da Sie sich eher mit strukturierten Dokumenten als mit flachen SQL-Tabellen befassen.

Speichern Sie Änderungen im Dokument

Jedes Feld kann auch einen eigenen Verlauf haben. Das Rekonstruieren von Dokumenten zu einer bestimmten Version ist auf diese Weise viel einfacher. In Ihrer Anwendung müssen Sie Änderungen nicht explizit verfolgen, sondern nur eine neue Version der Eigenschaft erstellen, wenn Sie ihren Wert ändern. Ein Dokument könnte ungefähr so ​​aussehen:

{
  _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" }
      ]
    }
  ]
}

Das Markieren eines Teils des Dokuments als in einer Version gelöscht ist jedoch immer noch etwas umständlich. Sie können ein stateFeld für Teile einfügen, die aus Ihrer Anwendung gelöscht / wiederhergestellt werden können:

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

Mit jedem dieser Ansätze können Sie eine aktuelle und reduzierte Version in einer Sammlung und die Verlaufsdaten in einer separaten Sammlung speichern. Dies sollte die Abfragezeiten verbessern, wenn Sie nur an der neuesten Version eines Dokuments interessiert sind. Wenn Sie jedoch sowohl die neueste Version als auch historische Daten benötigen, müssen Sie statt einer zwei Abfragen durchführen. Die Wahl zwischen einer einzelnen Sammlung und zwei getrennten Sammlungen sollte daher davon abhängen, wie oft Ihre Anwendung die historischen Versionen benötigt .

Der größte Teil dieser Antwort ist nur ein Brain Dump meiner Gedanken, ich habe noch nichts davon ausprobiert. Rückblickend ist die erste Option wahrscheinlich die einfachste und beste Lösung, es sei denn, der Overhead doppelter Daten ist für Ihre Anwendung sehr bedeutend. Die zweite Option ist ziemlich komplex und wahrscheinlich nicht die Mühe wert. Die dritte Option ist im Grunde eine Optimierung von Option zwei und sollte einfacher zu implementieren sein, ist aber wahrscheinlich den Implementierungsaufwand nicht wert, es sei denn, Sie können wirklich nicht mit Option eins gehen.

Ich freue mich auf Feedback zu diesem und den Lösungen anderer Leute für das Problem :)

Niels van der Rest
quelle
Was ist mit dem Speichern von Deltas irgendwo, so dass Sie abflachen müssen, um ein historisches Dokument zu erhalten und immer den aktuellen verfügbar zu haben?
jpmc26
@ jpmc26 Das ähnelt dem zweiten Ansatz, aber anstatt die Deltas zu speichern, um zu den neuesten Versionen zu gelangen, speichern Sie Deltas, um zu den historischen Versionen zu gelangen. Welcher Ansatz verwendet werden soll, hängt davon ab, wie oft Sie die historischen Versionen benötigen.
Niels van der Rest
Sie können einen Absatz über die Verwendung des Dokuments als Ansicht zum aktuellen Stand der Dinge hinzufügen und ein zweites Dokument als Änderungsprotokoll verwenden, in dem jede Änderung einschließlich eines Zeitstempels nachverfolgt wird (Anfangswerte müssen in diesem Protokoll angezeigt werden). Anschließend können Sie sie wiedergeben 'zu einem bestimmten Zeitpunkt und z. B. korrelieren, was vor sich ging, als Ihr Algorithmus es berührte, oder sehen, wie ein Element angezeigt wurde, als der Benutzer darauf klickte.
Manuel Arwed Schmidt
Beeinträchtigt dies die Leistung, wenn indizierte Felder als Arrays dargestellt werden?
DmitriD
@All - Könnten Sie bitte einen Code teilen, um dies zu erreichen?
Pra_A
8

Wir haben dies teilweise auf unserer Website implementiert und verwenden die 'Revisionen in einem separaten Dokument speichern' (und eine separate Datenbank). Wir haben eine benutzerdefinierte Funktion geschrieben, um die Unterschiede zurückzugeben, und wir speichern diese. Nicht so schwer und können eine automatisierte Wiederherstellung ermöglichen.

Amala
quelle
2
Könnten Sie bitte einen Code ungefähr gleich teilen? Dieser Ansatz sieht vielversprechend aus
Pra_A
1
@smilyface - Spring Boot Javers Integration ist am besten, um dies zu erreichen
Pra_A
@PAA - Ich habe eine Frage gestellt (fast das gleiche Konzept). stackoverflow.com/questions/56683389/… Haben Sie dafür eine Eingabe?
Smilyface
6

Warum nicht eine Variation von Store-Änderungen innerhalb des Dokuments ?

Anstatt Versionen für jedes Schlüsselpaar zu speichern, stellen die aktuellen Schlüsselpaare im Dokument immer den neuesten Status dar und ein 'Protokoll' von Änderungen wird in einem Verlaufsarray gespeichert. Nur die Schlüssel, die sich seit der Erstellung geändert haben, haben einen Eintrag im Protokoll.

{
  _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
quelle
2

Man kann eine aktuelle NoSQL-Datenbank und eine historische NoSQL-Datenbank haben. Es wird jeden Tag eine nächtliche ETL geben. Diese ETL zeichnet jeden Wert mit einem Zeitstempel auf, sodass es sich anstelle von Werten immer um Tupel (versionierte Felder) handelt. Es wird nur dann ein neuer Wert aufgezeichnet, wenn eine Änderung am aktuellen Wert vorgenommen wurde, wodurch Platz gespart wird. Diese historische json-Datei der NoSQL-Datenbank kann beispielsweise folgendermaßen aussehen:

{
  _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.
quelle
0

Für Benutzer von Python (Python 3+ und höher ) gibt es HistoricalCollection , eine Erweiterung des Collection-Objekts von pymongo.

Beispiel aus den Dokumenten:

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'}]

Vollständige Offenlegung, ich bin der Paketautor. :) :)

Dash2TheDot
quelle