Konkurrierende Veränderungen von Datensätzen

Änderungen zusammenführen

Bei langen Transaktionen wird der gleiche Datensatz möglicherweise von verschiedenen Benutzern verändert. Der Benutzer, der seine Änderungen zuerst abspeichert, merkt nichts von dieser Situation. Allen weiteren Benutzern muss beim Versuch, ihre Änderungen abzuspeichern,

  • angezeigt werden, dass die Daten geändert wurden;
  • die Änderungen anderer Benutzer sichtbar gemacht werden;
  • die eigene Änderung möglichst erhalten bleiben, soweit sie konfliktfrei ist.

Als Grundlage für diese Anzeigen müssen die Daten in der Datenbank mit den Benutzereingaben verglichen und in möglichst sinnvoller Weise zusammengeführt werden.

3 Weg Merge

In der Beispiel-Applikation ist ein 3 Weg Merge implementiert, um Konflikte bei langen Transaktionen aufzulösen. Bei einem 3 Weg Merge werden die parallel entstandenen unterschiedlichen Versionen mit dem gemeinsamen Vorgänger verglichen. Änderungen, die jeweils nur in einer Version des Datensatzes entstanden sind, werden automatisch zusammengeführt. Unterschiedliche Änderungen, die in beiden Versionen entstanden sind, werden als Konflikt markiert. Dieser muss vom Benutzer aufgelöst werden. Die Skizze soll dieses verdeutlichen.

Konkurrierende Änderungen am gleichen Datensatz
Konkurrierende Änderungen am gleichen Datensatz

Der Benutzer greift zum Bearbeiten über die Web Applikation auf Version V.0 eines Objekts zu. Die Applikation speichert diese Version in der Session und lädt sie in den Browser. Hier kann der Benutzer seine Änderungen durchführen (grüne Felder). In der Zwischenzeit wurde das Objekt ein- oder mehrmals in der Datenbank geändert, so dass es dort in Version V.x vorliegt. Der Handler in der Web Applikation kann den POST Request nicht erfolgreich abschließen, da das Objekt nicht in die Datenbank gespeichert werden kann. Der UpdatedAt Zeitstempel der Version in der DB stimmt nicht mit dem Wert des aktuellen Objekts (Version V.1 in der Skizze) überein. Es wird ein Stale Object Error ausgelöst und die Transaktion abgebrochen, siehe Gorm Code für lange Transaktionen. Die Änderungen in Datenbank und POST Request werden jetzt zusammengeführt, markiert und in den Browser geladen. Der Benutzer kann jetzt die Änderungen überprüfen, mögliche Konflikte auflösen und den aktualisierten Datensatz (Version V.x+1) abspeichern.

Werte für den 3 Weg Merge bereitstellen

Drei Versionen eines Objekts werden verglichen, wenn das Update des editierten Objekts einen Fehler ergibt. Im Beispiel sind dies die wichtigen Schritte in der Funktion saveApplicantSubmission in Datei utils.go:

180
Das in der Session gespeicherte Applicant Objekt (V.0) holen
187 – 190
Das Objekt aus der Session kopieren und alle Formular-Werte aus dem POST Request in die Kopie übernehmen (func setApplicantData)
200
Das editierte Objekt in die Datenbank speichern:
Wenn err != nil ist, beginnt der Merge Code. Dabei wird nur das ApplicantData Objekt betrachtet, das die eigentlichen Daten enthält.
202 – 205, 213
dataOld: V.0 aus der Session,
dataSubmitted: V.1 aus der aktuellen Form Submission,
dataModified: V.x aus der Datenbank
207-209
Spezialfall, dass Objekt gelöscht wurde, z.B. als Folge einer Exmatrikulation
214
Mit diesen drei Werten wird die MergeDiff Funktion aufgerufen, die eine Datenstruktur zurückgibt in der alle voneinander abweichenden Werte und die Art der Differenz enthalten ist.
237 – 238
Wenn alles geklappt hat, wird das Merge-Resultat im JSON Format an den Browser zurückgesendet.

5, 20
Änderungen in der Datenbank, jemand anderes hat diesen Wert verändert – Conflict = 2
10, 15
Lokale Änderungen, aktueller Benutzer hat diesen Wert verändert – Conflict = 1
25
Änderungen in der Datenbank und lokal, Benutzer muss Änderungen manuell zusammenführen – Conflict = 3
Implementierung des 3 Weg Merge

Die Implementierung nutzt das go reflect package. Sie ist an die Anforderungen des Beispiels angepasst und kann noch verallgemeinert werden. Es werden nur einfache Datentypen und Arrays mit einfachen Datentypen unterstützt. Die Felder, die verglichen werden sollen, können ein Tag besitzen, mit dem sie zu Feldern des UI zugeordnet werden können. Wenn keine Tags verwendet werden, wird der Name des Feldes in der Go struct genommen.

Die Ergebnisse des Vergleichs werden für jedes Feld in der Datenstruktur MergeInfo abgelegt. Die Felder Mine und Other enthalten die unterschiedlichen Werte. Das Feld Conflict beschreibt die registrierte Veränderung.

Die Funktion MergeDiff erstellt eine map von MergeInfo Objekten. Die Methode verwendet Go Reflection, also die Möglichkeit, zur Laufzeit des Programms auf den dynamischen Typ der Parameter zuzugreifen, ihre Metaeigenschaften zu bestimmen und ihre Werte zu verändern. Um diesen Code zu verstehen sollte man sich das Package reflect und den dort empfohlenen Artikel „The Laws of Reflection“ als Einführung zu Reflection in Go näher ansehen.

38 – 40
Die Parameter der Funktion sind vom Typ interface{}, also untypisiert. Da Werte verändert werden sollen, müssen sie als Pointer übergeben werden. Mit  reflect.ValueOf(...).Elem()  wird das reflect.Value Objekt erhalten, auf das der Pointer zeigt. Das sind also die Value Objekte der drei structs, die verglichen werden sollen.
43
Es muss sichergestellt werden, dass Objekte des gleichen Typs verglichen werden, sonst kommt Unsinn heraus.
48
Jetzt wird über die Felder iteriert und in den nächsten 3 Zeilen etwas Metainformation gesammelt.
52 – 54
Um mit den Werten der Reflection Objekte zu arbeiten, braucht man ihr Interface(), nicht ihren Value(). Das Value Objekt beschreibt den Wert, mit Interface() greift man auf den Speicherplatz des Wertes zu und kann ihn ändern. Das erscheint zwar nicht besonders intuitiv, aber funktioniert.
71
Felder vom Array- oder Slice-Typ werden separat in mergeArray(...)  behandelt. Dort wird über das gesamte Attay oder Slice iteriert und jede Zelle analog wie hier behandelt.
74 – 76
Damit können jetzt die Werte verglichen werden für die große Fallunterscheidung.
98, 103
Die Ergebnisse des Vergleichs werden in ein neues MergeInfo Objekt geschrieben und in die diffs map eingefügt. Eigene Änderungen, die nicht in Konflikt zu Änderungen in der Datenbank stehen, werden nur in die map aufgenommen, wenn automerge auf true gesetzt ist.

Als Schlüssel für die diffs map sollten Tags verwendet werden, die den Feldnamen in der HTML Form entsprechen. Als Beispiel  die struct ApplicantData:

Hier werden Tags der Art ´form:“feldname“´ verwendet. Eine Besonderheit ist der Tag von Results. Da dies ein Array ist, muss der Index der unterschiedlichen Werte für das Merging übergeben werden. Das ‚#‘ Zeichen im Tag wird daher in der Funktion mergeArray(…) durch den entsprechenden Index ersetzt. Dadurch kann das passende Eingabefeld in der HTML Form identifiziert werden.

Die Darstellung der Merge-Ergebnisse im Browser wird (noch nicht) auf einer anderen Seite beschrieben.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.