Version, die den Inhalt einer Datenbank steuert

16

Ich arbeite an einem Webprojekt, das vom Benutzer bearbeitbare Inhalte umfasst, und ich möchte in der Lage sein, eine Versionsverfolgung der tatsächlichen Inhalte durchzuführen, die in einer Datenbank gespeichert sind. Grundsätzlich möchte ich Änderungsverläufe im Wiki-Stil implementieren.

Ich mache einige Hintergrundrecherchen und sehe eine Menge Dokumentation darüber, wie man das Datenbankschema versioniert (meines ist bereits kontrolliert), aber alle vorhandenen Strategien, wie man die Änderungen des Datenbankinhalts verfolgt , gehen zumindest in der Lawine der Schemaversionierung verloren in meinen Suchen.

Ich kann mir ein paar Möglichkeiten vorstellen, mein eigenes Change Tracking zu implementieren, aber sie scheinen alle ziemlich grob zu sein:

  • Speichern Sie die gesamte Zeile bei jeder Änderung, und verknüpfen Sie die Zeile mit einem Primärschlüssel mit der Quell-ID (was ich derzeit anstrebe, ist die einfachste). Viele kleine Änderungen können jedoch zu viel Blähungen führen.
  • Speichern Sie vor / nach / Benutzer / Zeitstempel für jede Änderung mit einem Spaltennamen, um die Änderung wieder auf die entsprechende Spalte zu beziehen.
  • Speichern Sie vor / nach / Benutzer / Zeitstempel mit einer Tabelle für jede Spalte (dies würde zu vielen Tabellen führen).
  • Speichern Sie Diffs / Benutzer / Zeitstempel für jede Änderung mit einer Spalte (dies würde bedeuten, dass Sie den gesamten Änderungsverlauf durchgehen müssten, um zu einem bestimmten Datum zurückzukehren).

Was ist hier der beste Ansatz? Meinen eigenen zu rollen scheint, als würde ich wahrscheinlich die (bessere) Codebasis eines anderen neu erfinden.


Bonuspunkte für PostgreSQL.

Falscher Name
quelle
Diese Frage wurde bereits auf SO diskutiert: stackoverflow.com/questions/3874199/… . Google für "Datenbank-Datensatz-Verlauf", und Sie werden einige weitere Artikel finden.
Doc Brown
1
Klingt nach einem idealen Kandidaten für Event Sourcing
James
Warum nicht das Transaktionslog des SQL-Servers benutzen, um den Trick zu machen?
Thomas Junk

Antworten:

11

Normalerweise speichere ich den gesamten Datensatz mit einem end_timestamp-Feld. Es gibt eine Geschäftsregel, dass nur eine Zeile einen Null-End_Timestamp haben kann, und dies ist natürlich der aktuell aktive Inhalt.

Wenn Sie dieses System übernehmen, empfehle ich dringend, einen Index oder eine Einschränkung hinzuzufügen, um die Regel durchzusetzen. Dies ist mit Oracle ganz einfach, da ein eindeutiger Index nur eine Null enthalten kann. Andere Datenbanken können problematischer sein. Wenn die Datenbank die Regel erzwingt, bleibt Ihr Code ehrlich.

Sie sind sich ziemlich sicher, dass viele kleine Änderungen aufblähen, aber Sie müssen dies gegen Code und die Einfachheit der Berichterstellung austauschen.

Kiwiron
quelle
Beachten Sie, dass sich andere Datenbank-Engines möglicherweise anders verhalten, z. B. erlaubt MySQL mehrere NULL-Werte in einer Spalte mit einem eindeutigen Index. Dies macht es sehr viel schwieriger, diese Einschränkung durchzusetzen.
qbd
Die Verwendung eines tatsächlichen Zeitstempels ist unsicher, aber einige MVCC-Datenbanken arbeiten intern, indem sie minimale und maximale Transaktionsseriennummern zusammen mit Tupeln speichern.
user2313838
"Dies ist mit Oracle einfach, da ein eindeutiger Index nur eine Null enthalten kann". Falsch. Oracle enthält überhaupt keine Nullwerte in Indizes. Die Anzahl der Nullen in einer Spalte mit einem eindeutigen Index ist unbegrenzt.
Gerrat
@Gerrat Es ist einige Jahre her, dass ich eine Datenbank mit dieser Anforderung entworfen habe und keinen Zugriff mehr auf diese Datenbank habe. Sie haben Recht, dass ein eindeutiger Standardindex mehrere Nullen unterstützen kann, aber ich denke, wir haben entweder eine eindeutige Einschränkung oder möglicherweise einen funktionalen Index verwendet.
Kiwiron
8

Beachten Sie, dass es bei Verwendung von Microsoft SQL Server bereits eine Funktion mit dem Namen " Datenerfassung ändern" gibt . Sie müssen immer noch Code schreiben, um später auf die vorherigen Revisionen zugreifen zu können (CDC erstellt dafür spezielle Ansichten), aber zumindest müssen Sie das Schema Ihrer Tabellen nicht ändern oder die Änderungsnachverfolgung selbst implementieren.

Unter der Haube passiert Folgendes:

  • CDC erstellt eine zusätzliche Tabelle mit den Revisionen,

  • Ihre Originaltabelle wird wie bisher verwendet, dh alle Aktualisierungen werden direkt in diese Tabelle übernommen.

  • In der CDC-Tabelle werden nur die geänderten Werte gespeichert, sodass die Datenverdoppelung auf ein Minimum beschränkt bleibt.

Die Tatsache, dass Änderungen in einer anderen Tabelle gespeichert werden, hat zwei Hauptfolgen:

  • Auswahlmöglichkeiten aus der Originaltabelle sind so schnell wie ohne CDC. Wenn ich mich gut erinnere, geschieht CDC nach dem Update, so dass Updates gleich schnell sind (obwohl ich mich nicht gut erinnere, wie CDC die Datenkonsistenz verwaltet).

  • Einige Änderungen am Schema der Originaltabelle führten zum Entfernen von CDC. Wenn Sie beispielsweise eine Spalte hinzufügen, weiß CDC nicht, wie Sie damit umgehen sollen. Andererseits sollte das Hinzufügen eines Index oder einer Einschränkung in Ordnung sein. Dies wird schnell zu einem Problem, wenn Sie CDC für eine Tabelle aktivieren, die häufig geändert wird. Möglicherweise gibt es eine Lösung, mit der das Schema geändert werden kann, ohne dass CDC verloren geht, aber ich habe nicht danach gesucht.

Arseni Mourzenko
quelle
6

Lösen Sie das Problem zuerst "philosophisch" und im Code. Und dann mit Code und Datenbank "verhandeln", um dies zu ermöglichen.

Wie als Beispiel , wenn Sie mit Sammelartikel zu tun hat , ein erstes Konzept für einen Artikel, könnte wie folgt aussehen:

class Article {
  public Int32 Id;
  public String Body;
}

Und auf der nächst grundlegenderen Ebene möchte ich eine Liste der Revisionen führen:

class Article {
  public Int32 Id;
  public String Body;
  public List<String> Revisions;
}

Und es könnte mir einfallen, dass der aktuelle Körper nur die neueste Revision ist. Und das bedeutet zwei Dinge: Ich muss jede Revision datieren oder nummerieren:

class Revision {
  public Int32 Id;
  public Article ParentArticle;
  public DateTime Created;
  public String Body;
}

Und ... und der aktuelle Text des Artikels muss sich nicht von der neuesten Version unterscheiden:

class Article {
  public Int32 Id;
  public String Body {
    get {
      return (Revisions.OrderByDesc(r => r.Created))[0];
    }
    set {
      Revisions.Add(new Revision(value));
    }
  }
  public List<Revision> Revisions;
}

Ein paar Details fehlen; aber es zeigt, dass Sie wahrscheinlich zwei Entitäten wollen . Eine steht für den Artikel (oder einen anderen Header-Typ), die andere für eine Liste von Revisionen (Gruppierung der Felder, die für eine Gruppierung "philosophisch" sinnvoll sind). Sie benötigen anfangs keine speziellen Datenbankeinschränkungen, da sich Ihr Code nicht um die Revisionen an und für sich interessiert - sie sind Eigenschaften eines Artikels, der sich mit Revisionen auskennt.

Sie müssen sich also nicht darum kümmern, Revisionen auf besondere Weise zu kennzeichnen oder sich auf eine Datenbankeinschränkung zu stützen, um den "aktuellen" Artikel zu markieren. Sie müssen sie nur mit einem Zeitstempel versehen (auch eine automatisch eingegebene ID wäre in Ordnung), sie mit ihrem übergeordneten Artikel in Beziehung setzen und dem Artikel mitteilen, dass der "neueste" Artikel der relevanteste ist.

Und Sie überlassen einem ORM die weniger philosophischen Details - oder Sie verbergen sie in einer benutzerdefinierten Utility-Klasse, wenn Sie kein sofort einsatzbereites ORM verwenden.

Viel später, nachdem Sie einige Stresstests durchgeführt haben, können Sie darüber nachdenken, diese Revisionseigenschaft faul zu laden oder Ihr Body-Attribut faul zu laden, nur die oberste Revision. In diesem Fall sollte sich Ihre Datenstruktur jedoch nicht ändern müssen, um diese Optimierungen zu berücksichtigen.

Svidgen
quelle
2

Es gibt eine PostgreSQL-Wiki-Seite für einen Audit-Tracking-Trigger, die Sie durch die Einrichtung eines Audit-Protokolls führt, das genau das tut, was Sie benötigen.

Es verfolgt die vollständigen Originaldaten einer Änderung sowie die Liste der neuen Werte für Aktualisierungen (für Einfügungen und Löschungen gibt es nur einen Wert). Wenn Sie eine alte Version wiederherstellen möchten, können Sie die Kopie der Originaldaten aus dem Überwachungsdatensatz abrufen. Wenn Ihre Daten Fremdschlüssel enthalten, müssen diese Datensätze möglicherweise auch zurückgesetzt werden, um die Konsistenz zu gewährleisten.

Wenn Ihre Datenbankanwendung die meiste Zeit nur mit den aktuellen Daten verbringt, ist es meiner Meinung nach besser, alternative Versionen in einer von den aktuellen Daten getrennten Tabelle zu verfolgen. Dadurch bleiben Ihre aktiven Tabellenindizes übersichtlicher.

Wenn die Zeilen, die Sie verfolgen, sehr groß sind und der Platz ein ernstes Problem darstellt, können Sie versuchen, die Änderungen aufzuschlüsseln und minimale Unterschiede / Patches zu speichern, aber das ist definitiv mehr Arbeit, um alle Arten von Datentypen abzudecken. Ich habe das schon einmal gemacht, und es war mühsam, alte Versionen von Daten wiederherzustellen, indem ich alle Änderungen nacheinander rückwärts durchging.

Ben Turner
quelle
1

Nun, ich bin mit der einfachsten Option fertig geworden, einem Auslöser, der die alte Version einer Zeile in ein Verlaufsprotokoll pro Tabelle kopiert.

Wenn ich mit zu viel aufgeblähter Datenbank fertig werde, kann ich prüfen, ob möglicherweise einige der geringfügigen Änderungen in der Historie reduziert wurden, falls erforderlich.

Die Lösung war ziemlich chaotisch, da ich die Triggerfunktionen automatisch generieren wollte. Ich bin SQLAlchemy, also konnte ich die Verlaufstabelle durch einige Vererbungs-Hijinks erzeugen, was sehr gut war, aber die eigentlichen Trigger-Funktionen erforderten ein paar Zeichenfolgen, um die PostgreSQL-Funktionen ordnungsgemäß zu generieren und die Spalten einer Tabelle zuzuordnen ein anderer richtig.

Jedenfalls ist hier alles auf Github .

Falscher Name
quelle