Dieses Kapitel beschreibt den Abgleich der lokal gehaltenen Daten mit der Datenbank. Dabei werden insbesondere auch Konfliktsituationen berücksichtigt.
27 Datenbanken aktualisieren
Im letzten Kapitel haben wir uns ausgiebig mit den Daten im lokalen Speicher beschäftigt. Sie sind in Objekten von Typ DataTable enthalten und werden in einem DataSet verwaltet. Um die Synchronisation mit der Datenbank zu ermöglichen, werden die Daten sowohl im aktuellen Zustand als auch im Ursprungszustand (so, wie von der Originaldatenbank bezogen) gespeichert (Stichwort DataRowVersion). Ich habe Ihnen gezeigt, welche Möglichkeiten es gibt, die Daten im DataSet beliebig zu ändern. Nun folgt die »Krönung«: Wir schreiben geänderte Daten in die Originaldatenbank zurück.
Das Zurückschreiben in eine Datenbank ist nicht schwierig. Sie müssen nur ein entsprechendes SQL-UPDATE-Kommando an den Datenbankserver schicken, denn nur über SQL-Befehle können Sie mit einer Datenbank kommunizieren. Im einfachsten Fall genügt es, sich dazu an die Methode ExecuteNonQuery des Command-Objekts zu erinnern.
Solange Sie sicher davon ausgehen können, dass nur ein einziger Benutzer einen bestimmten Datensatz in der Datenbank ändern kann, ist das Thema »Aktualisierung« schnell erledigt: Sie schieben den geänderten Datensatz in die Originaldatenbank – fertig. Meistens aber haben wir es mit Mehrbenutzerumgebungen zu tun, in denen mehrere Anwender denselben Datensatz ändern dürfen. Denken Sie beispielsweise an die Online-Buchung einer Reise. Während mehrere potenzielle Reise-Interessenten noch den Familienrat tagen lassen und sich überlegen, ob sie das Superangebot eines Reiseveranstalters buchen sollen, bucht ein anderer Interessent schnell entschlossen die Reise. Versuchen später andere Interessenten, die Reise zu buchen, werden sie nach dem Absenden der Buchungsinformationen darauf aufmerksam gemacht, dass sie zu spät gekommen sind: Der Aktualisierungsversuch wird abgelehnt.
Solange wir mit verbindungsorientierten Datenbankzugriffen arbeiten, ist das Problem recht einfach zu lösen, da wir von den Sperrmechanismen der Datenbank unterstützt werden. Da ADO.NET jedoch eine verbindungslose Datenzugriffstechnologie ist, sind die Verhältnisse häufig nicht ganz so einfach und müssen gründlich analysiert werden.
Wann treten Konflikte auf?
Ehe ich auf die Aktualisierung einer Datenbank mit ADO.NET eingehe, möchte ich die Um-stände klären, unter denen in einer verbindungslosen Umgebung Konflikte auftreten können.
Ein Anwender A bezieht die Daten von einer Datenbank. Diese werden im lokalen Speicher abgelegt. und die Änderungen – Anwender A ändert den ursprünglichen Produktnamen Käse in Cheese – wirken sich zunächst nur im DataSet aus. Zu einem späteren Zeitpunkt entscheidet sich Anwender A, die Originaldatenbank mit der vorgenommenen Änderung zu synchronisieren, und schickt die geänderte Datenzeile zur Datenbank. Der SQL-Befehl könnte wie folgt aussehen (die WHERE-Klausel enthält den Produktnamen zur späteren Erläuterung der Problematik und ist technisch überflüssig, da ProductID eine Zeile bereits eindeutig identifiziert):
UPDATE Products
SET ProductName = 'Cheese'
WHERE ProductID = 55 AND ProductName = 'Käse'
Die Verbindung zur Datenbank wird geöffnet und der UPDATE-Befehl zur Datenbank geschickt, die ihrerseits die betroffene Zeile aktualisiert. Beachten Sie bitte die WHERE-Klausel, die bei einem UPDATE-Befehl dazu dient, in der Datenbank nach der zu aktualisierenden Datenzeile zu suchen. In allen Feldern der WHERE-Klausel werden die von der Datenbank bezogenen Originaldaten angegeben. Nur sie sind der Datenbank bekannt, da die Datenbank nichts von der lokalen Änderung weiß.
Aber es gibt vielleicht noch einen zweiten Anwender B, der dieselbe (ungeänderte) Datenzeile vom Datenbankserver bezogen hat und in seinem lokalen Speicher bearbeitet. Allerdings ändert Benutzer B nicht den Produktnamen, sondern den Einzelpreis. Selbstverständlich möchte auch Benutzer B seine Änderungen in die Datenbank schreiben. Nehmen wir an, er versucht es mit dem folgenden UPDATE-Befehl (mit derselben WHERE-Klausel wie A):
UPDATE Products
SET UnitPrice = 12.98
WHERE ProductID = 55 AND ProductName = 'Käse'
Anwender B hat dieselben Daten empfangen wie Anwender A und verwendet daher dieselbe WHERE-Klausel. Sie enthält den Wert Käse. Von der Änderung des Benutzers A weiß Benutzer B allerdings nichts. Abbildung 27.1 zeigt die Situation.
Abbildung 27.1 Konfliktsituation beim Aktualisieren
Der Aktualisierungsversuch von Benutzer B scheitert, denn die Datenzeile, die von der WHERE-Klausel des Benutzers B beschrieben wird, existiert nicht mehr – wegen der Änderung von Anwender A im Feld ProductName.
Anders sieht das Ergebnis für Anwender B aus, enthält die WHERE-Klausel von UPDATE als einziges Suchkriterium den Primärschlüssel der zu aktualisierenden Datenzeile:
UPDATE Products
SET UnitPrice = 12.98
WHERE ProductID = 55
Der Datensatz wird in der Datenbank gefunden, denn Anwender A hat ein Feld geändert, das in der WHERE-Klausel von Anwender B nicht angegeben ist.
Die Spaltenauswahl in der WHERE-Klausel bestimmt über den Erfolg einer Aktualisierung. |
An dieser Stelle sollten wir uns überlegen, welche Situationen auftreten können, die zu einem Konflikt führen:
- Ein Anwender B editiert eine Datenzeile, die ein Anwender A vorher aktualisiert hat. Je nach Formulierung der WHERE-Klausel wird die Aktualisierung durch Anwender B scheitern, weil die gesuchte Datenzeile in der Originaldatenbank nicht gefunden wird.
- Anwender B versucht eine Datenzeile zu ändern, die Anwender A zuvor gelöscht hat.
- Anwender B löscht eine Datenzeile, die schon gelöscht ist. Meistens wird man einen solchen Konflikt nicht behandeln müssen, er wird aber der Vollständigkeit halber erwähnt.
- Anwender B fügt eine neue Datenzeile mit einem Primärschlüssel hinzu, der bereits existiert. Wenn die betreffende Tabelle den Primärschlüssel automatisch vergibt, kann diese Konfliktsituation natürlich nicht auftreten.
Tritt ein Konflikt auf, muss der Anwender mindestens über die Ursache der Ablehnung einer Aktualisierung informiert werden. Die Anwendung sollte dem Anwender weitergehende Informationen bereitstellen. Im Beispiel oben ist es sinnvoll, Anwender B mitzuteilen, dass Anwender A den Produktnamen verändert hat. Wenn beide Änderungen ihre Berechtigung haben, kann Anwender B dann einen erneuten Aktualisierungsversuch starten, der sowohl den neuen Produktnamen als auch den geänderten Einzelpreis berücksichtigt.
Konflikte können in jeder Umgebung auftreten, in der Änderungen verbindungslos vorgenommen werden, also auch unter ADO.NET. Daher gibt uns ADO.NET alles an die Hand, um aufgetretene Konflikte zu analysieren und zu lösen. Dieses Kapitel beschreibt, wie Sie dabei vorgehen. Dabei unterscheiden wir zwei Szenarien:
- das automatische Aktualisieren unter Zuhilfenahme der Klasse CommandBuilder
- das manuell gesteuerte Aktualisieren
Fangen wir zunächst mit der einfacheren, automatischen Aktualisierung an.
27.1 Aktualisieren mit CommandBuilder 

Sie können eine DataTable mit Daten aus jeder Datenquelle füllen. Handelt es sich um eine Datenbank und können die Benutzer die Daten auch ändern, müssen die Änderungen irgendwann an die Datenbank übermittelt werden. Des Öfteren habe ich bereits die Update-Methode des DataAdapters erwähnt, die eine Verbindung zu der Datenbank aufbaut, um deren Datenbestand zu aktualisieren. Vielleicht haben Sie auch schon die Update-Methode nach Änderung der Daten getestet und dabei einen Laufzeitfehler erhalten. Das folgende Beispiel zeigt eine solche Situation, in der eine neue Datenzeile hinzugefügt und eine vorhandene geändert wird. Nach Abschluss der Änderungen wird die Methode Update des DataAdapters aufgerufen.
'...\ADO\Aktualisierung\CommandBuilder.vb |
Option Strict On
Imports System.Data.Common, System.Data.SqlClient
Namespace ADO
Module CommandBuilder
...
Sub Test()
Dim da As DbDataAdapter, ds As DataSet
Lesen(da, ds)
Ändern(ds, False)
Aktualisieren(da, ds)
End Sub
Sub Lesen(ByRef da As DbDataAdapter, ByRef ds As DataSet)
Dim conn As DbConnection = New SqlConnection()
conn.ConnectionString = "Data Source=(local);" & _
"Initial Catalog=Northwind;Integrated Security=sspi"
Dim cmd As DbCommand = New SqlCommand()
cmd.CommandText = "SELECT ProductID, ProductName, " & _
"UnitsInStock, Discontinued FROM Products"
cmd.Connection = conn
ds = New DataSet() : da = New SqlDataAdapter()
da.SelectCommand = cmd
da.FillSchema(ds, SchemaType.Source)
da.Fill(ds)
End Sub
'neue Datenzeile hinzu und eine andere ändern
Sub Ändern(ByVal ds As DataSet, ByVal name As Boolean)
Dim neu As DataRow = ds.Tables(0).NewRow()
neu("ProductName") = "Camembert" : neu("UnitsInStock") = 100
neu("Discontinued") = False
ds.Tables(0).Rows.Add(neu)
Dim ändern As DataRow = ds.Tables(0).Select("ProductID=14")(0)
ändern("UnitsInStock") = CType(ändern("UnitsInStock"), Integer) + 1000
If name Then ändern("ProductName") = "Eiweißlieferant"
End Sub
'Datenbank aktualisieren
Sub Aktualisieren(ByVal da As DbDataAdapter, ByVal ds As DataSet)
Dim betroffen As Integer = 0
Try
betroffen = da.Update(ds)
Catch ex As Exception
Console.WriteLine("Fehler: {0}", ex.Message)
End Try
Console.WriteLine("{0} Datenzeilen aktualisiert", betroffen)
Console.ReadLine()
End Sub
End Module
End Namespace
Die Ausgabe zeigt einen Laufzeitfehler, der durch Update hervorgerufen wurde.
Fehler: Update requires a valid UpdateCommand when passed DataRow
collection with modified rows.
0 Datenzeilen aktualisiert
Um diese Meldung zu verstehen, sehen wir uns an, wie der DataAdapter eine Auswahlabfrage an die Datenbank schickt. Nach (oder bei) der Erzeugung des Command-Objekts wird ein SELECT-Befehl spezifiziert und in der SelectCommand-Eigenschaft des DataAdapters gespeichert.
Der DataAdapter kennt noch drei weitere Kommandos:
- InsertCommand
- DeleteCommand
- UpdateCommand
Analog zum Kommando SelectCommand, das die abzusetzende Auswahlabfrage spezifiziert, benötigt der DataAdapter auch noch Command-Objekte, die die SQL-Befehle INSERT, DELETE und UPDATE beschreiben.
Zum Glück stellt der DataAdapter nicht automatisch Aktualisierungsbefehle bereit, obwohl er das durchaus könnte. Dieser scheinbare Mangel zwingt uns, die benötigten Befehle selbst anzugeben. Und hier wird der Mangel zum Vorteil: Nur der Datenbanknutzer (der Client) weiß, was bei einer Aktualisierung zu tun ist. Jeder Automatismus muss hier über kurz oder lang scheitern, weil er die Logik des Anwendungsprogramms nicht kennen kann.
Der kürzeste Weg zur Spezifikation der notwendigen Aktualisierungslogik ist die Generierung mit einem DbCommandBuilder-Objekt, in dem Sie einen DataAdapter in der gleichnamigen Eigenschaft speichern. Mit dem DataAdapter-Objekt ist auch die SELECT-Auswahlabfrage bekannt. Auf deren Grundlage erzeugt DbCommandBuilder die SQL-Befehle INSERT, DELETE und UPDATE und speichert sie in den entsprechenden Command-Eigenschaften. Wenn im DataSet eine Zeile gelöscht, hinzugefügt oder editiert worden ist, wird der DataAdapter mit den vom DbCommandBuilder erzeugten Kommandos die Originaldatenbank aktualisieren.
Kommen wir zu dem eingangs gezeigten Beispiel zurück. Wenn Sie vor dem Aufruf von Update ein DbCommandBuilder-Objekt erzeugen und initialisieren, wird die Aktualisierung erfolgreich sein. Die folgende Methode Automatisch ersetzt die Methode Test.
Sub Automatisch()
Dim da As DbDataAdapter, ds As DataSet
Lesen(da, ds)
Dim cmb As DbCommandBuilder = New SqlCommandBuilder()
cmb.DataAdapter = da
Ändern(ds, False)
Aktualisieren(da, ds)
End Sub
27.1.1 Parallelitätskonflikt 

Als Nächstes simulieren wir einen sogenannten Parallelitätskonflikt. Statt eines einzelnen Benutzers in der Methode Test tauchen in Konflikt zwei Benutzer auf.
Sub Konflikt()
Dim da1, da2 As DbDataAdapter, ds1, ds2 As DataSet
Lesen(da1, ds1) : Lesen(da2, ds2)
Dim cmb1 As DbCommandBuilder = New SqlCommandBuilder()
cmb1.DataAdapter = da1
Dim cmb2 As DbCommandBuilder = New SqlCommandBuilder()
cmb2.DataAdapter = da2
Ändern(ds1, True) : Ändern(ds2, False)
Aktualisieren(da1, ds1) : Aktualisieren(da2, ds2)
End Sub
Die Ausgabe zeigt, dass die Aktualisierung des zweiten Benutzers fehlschlägt. Da nur die Methode Aktualisieren eine Ausgabe erzeugt, ist sichergestellt, dass die lokalen Änderungen in der Methode Ändern erfolgreich sind.
2 Datenzeilen aktualisiert
Fehler: Concurrency violation: the UpdateCommand affected 0
of the expected 1 records.
0 Datenzeilen aktualisiert
Der Name des Artikels Tofu wird durch den ersten Benutzer geändert (Parameter True in Ändern). Da das DbCommandBuilder-Objekt des zweiten Benutzers davon nichts weiß, wird dessen UPDATE-Befehl nicht der neuen Situation angepasst. Es wird eine Ausnahme vom Typ DBConcurrencyException ausgelöst, obwohl die Primärschlüsselspalte ProductID nicht angetastet wurde. Das automatisch erstellte UPDATE-Kommando stützt sich also nicht auf die Eindeutigkeit der Primärschlüsselspalte, sondern zieht weitere Spaltenwerte zur Identifikation einer Zeile heran.
27.1.2 Aktualisierungsbefehle des DbCommandBuilders 

Ein DbCommandBuilder erzeugt Aktualisierungscode und nutzt dabei das SELECT-Statement. Doch wie sieht die Aktualisierungslogik exakt aus?
Sie können sich die Aktualisierungsstatements ausgeben lassen, indem Sie die Methoden GetUpdateCommand, GetInsertCommand oder GetDeleteCommand des DbCommandBuilders aufrufen. Alle liefern ein Command-Objekt, über dessen Eigenschaft CommandText Sie den jeweiligen SQL-Befehl abfragen können. Für unser Beispiel:
Sub Kommandos()
Dim da As DbDataAdapter, ds As DataSet
Lesen(da, ds)
Dim cmb As DbCommandBuilder = New SqlCommandBuilder()
cmb.DataAdapter = da
Console.WriteLine(da.SelectCommand.CommandText)
Console.WriteLine(cmb.GetInsertCommand().CommandText)
Console.WriteLine(cmb.GetDeleteCommand().CommandText)
Console.WriteLine(cmb.GetUpdateCommand().CommandText)
Console.ReadLine()
End Sub
Die Ausgabe zeigt alle Kommandos:
SELECT ProductID, ProductName, UnitsInStock, Discontinued FROM Products
INSERT INTO [Products] ([ProductName], [UnitsInStock], [Discontinued])
VALUES (@p1, @p2, @p3)
DELETE FROM [Products]
WHERE (([ProductID] = @p1) AND ([ProductName] = @p2)
AND ((@p3 = 1 AND [UnitsInStock] IS NULL) OR ([UnitsInStock] = @p4))
AND ([Discontinued] = @p5))
UPDATE [Products]
SET [ProductName] = @p1, [UnitsInStock] = @p2, [Discontinued] = @p3
WHERE (([ProductID] = @p4) AND ([ProductName] = @p5)
AND ((@p6 = 1 AND [UnitsInStock] IS NULL) OR ([UnitsInStock] = @p7))
AND ([Discontinued] = @p8))
Sie können erkennen, dass hinter der WHERE-Klausel alle Spalten der SELECT-Abfrage als Suchkriterium nach dem zu editierenden Datensatz aufgeführt sind. Der DbCommandBuilder wertet demnach alle Spalten aus und verwendet sie zur Bildung des Command-Objekts.
Die Parameter im SET-Teil werden mit Werten aus DataRowVersion.Current belegt, die im WHERE-Teil aus DataRowVersion.Original.
27.1.3 Aktualisierungsoptionen des DbCommandBuilders 

ConflictOption
Standardmäßig verwendet der DbCommandBuilder in der WHERE-Klausel für UpdateCommand und DeleteCommand alle zu vergleichenden Spalten. Eine Änderung der Daten in der Datenbank führt zu einer Ausnahme (DBConcurrencyException), wenn ein anderer User eine dieser Spalten in dem Zeitraum verändert hat, in dem die ursprünglichen Daten für die Zeile abgerufen und neue Werte für die Zeile übermittelt werden.
Dieses Verhalten ist nicht immer wünschenswert. Daher stellt Ihnen der DbCommandBuilder mit der Eigenschaft ConflictOption eine Möglichkeit zur Verfügung, das Aktualisierungsverhalten zu beeinflussen. Die Eigenschaft ist vom Typ der gleichnamigen Enumeration, deren Mitglieder Sie Tabelle 27.1 entnehmen können.
Konstante | Spalten in WHERE |
CompareAllSearchableValues |
Alle, nach denen gesucht werden kann (Standard) |
CompareRowVersion |
Timestamp-Spalte(n), wenn vorhanden (UPDATE), sonst wie OverwriteChanges |
OverwriteChanges |
Alle Spalten des Primärschlüssels, wenn vorhanden, sonst wie CompareAllSearchableValues |
Mit dem Standardwert ConflictOption.CompareAllSearchableValues wird die erste Änderung in einer Datenzeile immer zum Erfolg führen, während folgende Änderungen zu einem Konflikt führen. Dieses Szenario wird daher auch als First-in-wins bezeichnet.
Werden mit ConflictOption.OverwriteChanges nur die Primärschlüsselspalte(n) in die WHERE-Klausel einbezogen, werden Änderungen des ersten Benutzers von den nachfolgenden Änderungen überschrieben. Dieses Szenario wird als Last-in-wins bezeichnet.
Ein Timestamp ist ein automatisch generierter, eindeutiger 8-Byte-Wert. Mithilfe der Timestamp-Spalte einer Zeile können Sie ermitteln, ob ein Wert in der Zeile seit dem Auslesen geändert wurde. Bei jeder Änderung der Zeile wird der Timestamp-Wert aktualisiert. Mit ConflictOption.CompareRowVersion weisen Sie den DbCommandBuilder an, in der WHERE-Klausel nur die Primärschlüsselspalte(n) und die Timestamp-Spalte aufzunehmen.
SetAllValues
Betrachten Sie noch einmal das Beispiel mit Benutzer A und B am Anfang des Kapitels. Es ist denkbar, dass weder das Last-in-wins- noch das First-in-wins-Szenario passend sind. Es können zwei oder mehr Änderungen an einer Datenzeile akzeptiert werden, wenn sie nicht dieselbe Spalte betreffen. Das bedeutet, dass in der WHERE-Klausel neben dem Primärschlüssel auch die jeweils geänderte Spalte mit ihrem Ursprungswert angegeben werden kann. Benutzer A könnte in einem solchen Fall das folgende UPDATE absetzen:
UPDATE Products
SET ProductName = 'Cheese'
WHERE ProductID = 55 AND ProductName = 'Käse'
Ändert Benutzer B unter den gleichen Voraussetzungen die Spalte UnitPrice, wird diese seinem UPDATE-Statement hinzugefügt:
UPDATE Products
SET UnitPrice = 12.98
WHERE ProductID = 55 AND UnitPrice = 13
Beide Aktualisierungen werden in die Datenbank übernommen.
Setzen Sie die Eigenschaft SetAllValues des DbCommandBuilders auf False, werden neben der Primärschlüsselspalte nur diejenigen Spalten der WHERE-Klausel als Suchkriterium hinzugefügt, deren Inhalte sich verändert haben. Das entspricht genau dem gezeigten Muster.
27.1.4 Vor- und Nachteile des DbCommandBuilders 

Der DbCommandBuilder ist sehr einfach zu handhaben und benötigt nur wenig Programmcode. Der Effizienz bei der Programmierung steht aber ein Performance-Verlust gegenüber. Sie sollten daher in zeitkritischen Anwendungen das Aktualisierungsverhalten nicht vom DbCommandBuilder abhängig machen und, wie gleich gezeigt wird, die Aktualisierungslogik manuell programmieren. Über das Standardverhalten hinaus bietet der DbCommandBuilder eine ganze Reihe von Möglichkeiten, um eine angepasste Konfliktsteuerung zu betreiben. Manchmal wird diese aber nicht ausreichen. Dann heißt es wieder, alles manuell zu codieren.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.