Verlorene Änderungen mit SQL Server rowversion verhindern
Schützen Sie Bearbeitungen mit rowversion, erkennen Sie konkurrierende Änderungen und behandeln Sie Konflikte ohne stilles Überschreiben fremder Arbeit.
Zwei Personen öffnen dieselbe Produktbeschreibung. Eine korrigiert die Preisangabe und speichert. Die andere bearbeitet die Zeichensetzung in einer älteren Kopie und sendet das vollständige Formular. Beide Anfragen sind erfolgreich, doch die zweite entfernt die erste Änderung. Eine Transaktion um jeden einzelnen Schreibvorgang verhindert dieses Problem nicht: Das veraltete Lesen fand bereits vorher statt.
Die gelesene Version beim Schreiben prüfen
Eine Änderungsanfrage muss angeben, welche Version der Benutzer gesehen hat. Eine rowversion-Spalte liefert dafür ein von der Datenbank erzeugtes Token aus acht Bytes. Geben Sie es zusammen mit den editierbaren Feldern zurück und verlangen Sie es beim Speichern. Primärschlüssel und erwartetes Token gehören gemeinsam in die WHERE-Bedingung des UPDATE.
Ein vorheriges SELECT mit anschließendem Vergleich im Anwendungscode reicht nicht. Zwischen diesem Vergleich und einem unbedingten UPDATE kann eine andere Sitzung schreiben. Im folgenden Beispiel lesen zwei simulierte Bearbeiter dasselbe Token. Der erste speichert erfolgreich, der zweite findet anschließend keine passende Version mehr.
CREATE TABLE #Draft
(
DraftId int NOT NULL PRIMARY KEY,
Title nvarchar(100) NOT NULL,
Revision rowversion NOT NULL
);
INSERT #Draft (DraftId, Title) VALUES (1, N'Initial title');
DECLARE @SeenByA binary(8), @SeenByB binary(8);
SELECT @SeenByA = Revision, @SeenByB = Revision
FROM #Draft WHERE DraftId = 1;
UPDATE #Draft SET Title = N'Editor A'
OUTPUT inserted.DraftId, inserted.Title, inserted.Revision
WHERE DraftId = 1 AND Revision = @SeenByA;
UPDATE #Draft SET Title = N'Editor B'
WHERE DraftId = 1 AND Revision = @SeenByB;
DECLARE @Changed int = @@ROWCOUNT;
SELECT @Changed AS RowsChanged;
SELECT DraftId, Title, Revision FROM #Draft;
DROP TABLE #Draft;
RowsChanged ist beim zweiten UPDATE null im Sinne von null geänderten Zeilen, konkret der Zahlenwert 0. Der Titel bleibt Editor A. Das erste OUTPUT liefert bereits das neue Token. Wenn Sie @@ROWCOUNT verwenden, speichern Sie dessen Wert sofort; nachfolgende Anweisungen können ihn verändern. Der Primärschlüssel begrenzt die Änderung auf höchstens eine Zeile.
Behandeln Sie das Token als undurchsichtige Binärdaten. Für JSON eignet sich eine einheitliche Base64-Darstellung oder ein Hexadezimalformat fester Länge. Dekodieren Sie genau acht Bytes und binden Sie einen binären Parameter. Eine JavaScript-Zahl ist dafür ungeeignet. Das Token ist keine Uhrzeit, und der Client darf den nächsten Wert nicht berechnen. Eine lesbare Änderungszeit gehört in eine separate datetime2-Spalte.
Einen brauchbaren Konfliktablauf anbieten
Null betroffene Zeilen bedeutet, dass die Kombination aus Schlüssel und Version nicht geschrieben werden konnte. Ursache kann eine fremde Änderung, eine Löschung oder eine fehlende Berechtigung für die angefragte Zeile sein. Das UPDATE-Ergebnis unterscheidet diese Fälle nicht. Prüfen Sie Berechtigungen in der Schreibbedingung oder mit einer gleichwertig zuverlässigen transaktionalen Prüfung.
Für berechtigte Benutzer sollte die Oberfläche den eingegebenen Text behalten und die aktuelle Fassung laden. Wenn ein Vergleich sinnvoll ist, zeigen Sie Ausgangszustand, eigenen Vorschlag und aktuellen Datenstand. Ein Neuladen, das die ungespeicherte Arbeit verwirft, schützt zwar die Datenbank, löst aber das eigentliche Bearbeitungsproblem nicht.
Wiederholen Sie eine veraltete vollständige Formularänderung nicht automatisch mit dem neuesten Token. Dadurch würde die Anwendung den Konflikt lediglich verdecken. Manche Befehle haben engere Semantik: Ein Zähler lässt sich atomar erhöhen, ohne einen zuvor gelesenen Gesamtwert zurückzuschreiben. Entscheiden Sie bewusst zwischen einer solchen Operation und dem Ersetzen eines Dokuments.
Die Geschäftsoperation vollständig schützen
Das Token schützt eine einzelne Zeile. Bearbeitet eine Maske einen Auftragskopf und mehrere Positionen, erkennt die Prüfung des Kopfs keine unabhängige Positionsänderung. Dafür müsste jede relevante Änderung auch eine gemeinsame Version fortschreiben. Alternativ können Sie die Token aller geänderten Positionen innerhalb einer Transaktion prüfen. Wenn die Geschäftsoperation nur vollständig gültig ist, muss ein einzelner Konflikt alle zugehörigen Änderungen zurückrollen.
Testen Sie außerdem vorhandene Trigger. OUTPUT liefert die Werte vor der Ausführung von AFTER-Triggern. Ändert ein Trigger dieselbe Zeile erneut, kann das ausgegebene Token bereits veraltet sein. Lesen Sie dann das endgültige Token nach dem Trigger innerhalb der Transaktion. Eine Erfolgsantwort darf erst nach dem Commit folgen.
Prüfen Sie paralleles Speichern, Löschen vor dem Speichern und einen erneuten Versuch nach verlorener Netzwerkantwort. Erfassen Sie Konflikte getrennt von Serverfehlern. Viele Konflikte können darauf hinweisen, dass eine Maske unnötig viele Felder ersetzt oder ein Hintergrundprozess auch unveränderte Zeilen aktualisiert. So wird aus dem Versionsvergleich ein überprüfbarer Vertrag zwischen Oberfläche und Datenbank.
Technische Referenzen: Microsoft Learn: rowversion · Microsoft Learn: OUTPUT clause.