27.4 Konfliktverursachende Datenzeilen bei der Datenbank abfragen
Damit nicht nur der Konfliktverursacher ermittelt wird, sondern auch ein Lösungsansatz gefunden werden kann, muss auch die Ursache analysiert werden. Warum wurde die Aktualisierung einer geänderten Datenzeile von der Datenbank abgelehnt? Beispielsweise könnte ein anderer Anwender seinerseits den Datensatz geändert haben, oder der Datensatz existiert gar nicht mehr, weil er in der Originaltabelle gelöscht worden ist. Erst mit genauerer Kenntnis der Ursache kann der Anwender unter Berücksichtigung der Konfliktsituation entscheiden, ob er seine Änderungen doch noch in die Datenbank schreiben oder verwerfen will.
Der Schlüssel zur Lösung ist aber nicht im aktuellen DataSet zu finden, sondern in der Datenbank selbst. Was wir brauchen, ist eine neue Originalversion der konfliktverursachenden Datenzeile. Jetzt hilft uns der DataAdapter weiter. Er löst nämlich für jede zu aktualisierende Datenzeile zwei Ereignisse aus, wenn anstehende Änderungen über die Methode Update an die Datenbank übermittelt werden:
- RowUpdating
- RowUpdated
Hinweis |
Nicht die abstrakte Klasse DbDataAdapter, sondern die providerspezifischen Klassen veröffentlichen die Ereignisse. |
RowUpdating wird ausgelöst, bevor eine Zeile übermittelt wird, RowUpdated tritt unmittelbar nach der Übermittlung auf. Zur Konfliktanalyse untersuchen wir im Ereignishandler des RowUpdated-Ereignisses den zweiten Parameter vom Typ RowUpdatedEventArgs (siehe Tabelle 27.2).
Eigenschaft | Beschreibung | |
Command |
Das beim Aufruf von Update ausgeführte Command |
RU |
Errors |
Fehler, die während der Ausführung generiert wurden |
U |
RecordsAffected |
Die Anzahl der durch die Ausführung der SQL-Anweisung geänderten, eingefügten oder gelöschten Zeilen |
R |
Row |
Die durch ein Update gesendete DataRow |
RU |
RowCount |
Die Anzahl betroffener Zeilen |
R |
StatementType |
Der Typ der ausgeführten SQL-Anweisung |
RU |
Status |
Der Zustand vom Typ der Enumeration UpdateStatus |
U |
TableMapping |
Das durch ein Update gesendete DataTableMapping |
RU |
Hinweis |
Die Datenprovider leiten die Klasse RowUpdatedEventArgs ab. Die abgeleitete Klasse hat den Namen des Datenproviders als Präfix (Sql, OleDb, Odbc, Oracle). |
Die Eigenschaft Status kann die Werte aus Tabelle 27.3 annehmen:
Konstante | Beschreibung |
Continue |
DataAdapter soll mit der Verarbeitung von Zeilen fortfahren. |
ErrorsOccured |
Der Ereignishandler meldet, dass die Aktualisierung als Fehler behandelt werden soll. |
SkipAllRemainingRows |
Keine Aktualisierung der aktuellen und aller restlichen Zeilen |
SkipCurrentRow |
Keine Aktualisierung der aktuellen Zeile |
Wie nutzen wir nun das Ereignis?
Beide Ereignisse werden unabhängig vom Erfolg der Synchronisation der Datenbank ausgelöst. Wir müssen also selbst feststellen, ob die Aktualisierung einer Datenzeile zu einem Konflikt geführt hat. In diesem Fall hat die Eigenschaft Status des RowUpdatedEventArgs-Objekts den Enumerationswert UpdateStatus.ErrorsOccured.
Sub RowUpdated(object sender, SqlRowUpdatedEventArgs ev) If ev.Status = UpdateStatus.ErrorsOccurred Then ... End If End Sub
Alle konfliktverursachenden Datenzeilen können in einem DataSet zusammengefasst werden, das sich nach der Aktualisierungsoperation aller geänderten Datenzeilen auswerten lässt. Dazu müssen die aktuellen Daten der konfliktverursachenden Datenzeilen aus der Originaldatenbank gelesen werden. Mit den dann vorliegenden aktuellen Daten haben wir eine Basis für eine Konfliktlösung.
Im Ereignishandler des RowUpdated-Ereignisses wird deshalb eine parametrisierte Abfrage spezifiziert, die gegen die Datenbank abgesetzt wird. Dabei wird im Suchkriterium der WHERE-Klausel nur der Primärschlüssel der konfliktverursachenden Datenzeile als Parameter verwendet. Die Zeile liefert uns die Eigenschaft Row des RowUpdatedEventArgs-Parameters. Abgefragt werden von der Datenbank die Inhalte aller Spalten, die im Zusammenhang mit der Aktualisierung stehen.
Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "SELECT ProductID, ProductName, " & _ "UnitPrice, Discontinued FROM Products WHERE ProductID = @ID" cmd.Connection = con cmd.Parameters.Add(New SqlParameter("@ID", SqlDbType.Int, 4, "ProductID")) daConflict.SelectCommand = cmd daConflict.SelectCommand.Parameters(0).Value = ev.Row("ProductID")
daConflict ist hierbei die Referenz auf ein DbDataAdapter-Objekt. Er füllt ein DataSet, in dem nur die nun aktuellen Inhalte der konfliktverursachenden Datenzeilen enthalten sind. Nennen wir dieses DataSet »Konflikt-Dataset«.
Beim Aktualisieren einer Datenzeile können zwei verschiedene Ausnahmen auftreten:
- DbException
- DBConcurrencyException
DbException tritt auf, wenn die Datenbank einen Fehler zurückgibt. Das ist beispielsweise der Fall, wenn ein Datensatz mit einem Primärschlüssel hinzugefügt wird, der in der Tabelle bereits existiert. DBConcurrencyException hingegen wird ausgelöst, wenn eine Parallelitätsverletzung vorliegt. Dann ist die Anzahl der aktualisierten Datenzeilen 0.
Die Eigenschaft Errors des RowUpdatedEventArgs-Objekts speichert eine aufgetretene Ausnahme und ist Nothing, wenn keine Fehler aufgetreten sind. Damit kann mit Errors der Fehlertyp ermittelt werden, der nicht unbedingt mit der Aktualisierungslogik zusammenhängen muss (zum Beispiel Netzwerkfehler).
Wenn außerdem der oben beschriebene DataAdapter mit Fill eine Zeile aus der Datenbank lesen kann, liegt der Fehler in Errors an der doppelten Verwendung desselben Primärschlüssels, da die Parametrisierung des SelectCommands auf die zu aktualisierende Zeile zugeschnitten ist und der Befehl nur null oder eine Zeile aus der Datenbank beziehen kann.
Console.WriteLine("Fehler vom Typ {}.", ev.Errors.GetType().Name) If ev.Errors.GetType() Is GetType(DbException) AndAlso _ daConflict.Fill(dsConflict) = 1 Then Console.WriteLine("Der Primärschlüssel existiert bereits.") End If
Handelt es sich bei Errors um DBConcurrencyException, wurde der Versuch abgelehnt, die Änderung an einer Datenzeile in die Originaltabelle zu schreiben. Die Parallelitätsverletzung kann zwei Ursachen haben:
- Ein anderer Anwender hat den Datensatz zwischenzeitlich geändert.
- Der Datensatz wurde von einem anderen Anwender gelöscht.
Die Unterscheidung der Fälle geschieht wieder mit dem Ergebnis der Fill-Methode des oben eingeführten DataAdapters. Ist der Rückgabewert 0, konnte keine entsprechende Zeile in der Datenbank gefunden werden: Die Zeile wurde gelöscht. Wir ergänzen die If-Bedingung um einen Else If-Zweig.
Else If ev.Errors.GetType() Is GetType(DBConcurrencyException) Then If daConflict.Fill(dsConflict) = 1 Then Console.WriteLine("Ein anderer Nutzer hat den Datensatz geändert.") Else Console.WriteLine("Datensatz existiert nicht in der Datenbank.") End If
Fassen wir nun den Ereignishandler des RowUpdated-Ereignisses zusammen:
Sub Änderung(sender As Object, ev As RowUpdatedEventArgs) If ev.Status = UpdateStatus.ErrorsOccurred Then Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "SELECT ProductID, ProductName, " & _ "UnitPrice, Discontinued FROM Products WHERE ProductID = @ID" cmd.Connection = con cmd.Parameters.Add(New SqlParameter("@ID",SqlDbType.Int,4,"ProductID")) daConflict.SelectCommand = cmd daConflict.SelectCommand.Parameters(0).Value = ev.Row("ProductID") ' Fehleranalyse Console.WriteLine("Fehler vom Typ {}.", ev.Errors.GetType().Name) If ev.Errors.GetType() Is GetType(DbException) AndAlso _ daConflict.Fill(dsConflict) = 1 Then Console.WriteLine("Der Primärschlüssel existiert bereits.") Else If ev.Errors.GetType() Is GetType(DBConcurrencyException) Then If daConflict.Fill(dsConflict) = 1 Then Console.WriteLine("Ein anderer Nutzer hat den Datensatz geändert.") Else Console.WriteLine("Datensatz existiert nicht in der Datenbank.") End If End If End If End Sub
Wie Sie mit dem Inhalt des DataSets umgehen, das den aktuellen Stand der konfliktverursachenden Zeilen enthält, richtet sich nach den Bedürfnissen des Kunden, der die Anwendung einsetzt. Die Lösung kann sehr unterschiedlich ausfallen und dabei auch noch sehr komplex sein. Im folgenden zusammengefassten Beispielprogramm werden nur das neue Original und die Daten der konfliktverursachenden Datenzeile an der Konsole ausgegeben.
'...\ADO\Aktualisierung\Konflikt |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Ereignisse Dim con As DbConnection = New SqlConnection() Dim daConflict As DbDataAdapter = New SqlDataAdapter() Dim dsConflict As New DataSet() Sub Test() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "SELECT ProductID, ProductName, " & _ "UnitPrice, Discontinued FROM Products" cmd.Connection = con Dim ds As New DataSet() Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd da.Fill(ds) ' Konflikt simulieren cmd.CommandText = _ "UPDATE Products SET ProductName='Tee' WHERE ProductID=1" con.Open() : cmd.ExecuteNonQuery() : con.Close() ' Änderungen durchführen ds.Tables(0).Select("ProductID=1")(0)("ProductName") = "Kräutertee" da.UpdateCommand = CreateUpdateCommand(con) da.ContinueUpdateOnError = True AddHandler CType(da, SqlDataAdapter).RowUpdated, AddressOf Änderung da.Update(ds) ' Konflikte ausgeben If dsConflict.Tables.Count = 0 Then Return Console.WriteLine("{0} konfliktverursachende Datenzeile(n)", _ dsConflict.Tables(0).Rows.Count) For Each cRow As DataRow In dsConflict.Tables(0).Rows Dim rows() As DataRow = ds.Tables(0).Select( _ "ProductID = " & cRow("ProductID").ToString()) Console.WriteLine("{0,-10}{1}", "ProductID", "ProductName") Console.WriteLine("Original : {0,-10}{1}", cRow(0), cRow(1)) Console.WriteLine("Erfolglos: {0,-10}{1}", rows(0)(0), rows(0)(1)) Console.WriteLine(New String("-"c, 50)) Next Console.ReadLine() End Sub Sub Änderung(sender As Object, ev As RowUpdatedEventArgs) ... Function CreateUpdateCommand(ByVal con As DbConnection) As DbCommand ... End Module End Namespace
Wenn Sie das Beispielprogramm ausprobieren, sollten Sie zuerst sicherstellen, dass es den Artikel Chai auch tatsächlich gibt (wir haben bereits sehr viel mit dieser Datenzeile experimentiert). Wegen des in der Tabelle Products definierten autoinkrementellen Primärschlüssels kann das Programm allerdings keinen doppelten Primärschlüssel simulieren, der von der Datenbank vergeben wird.
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.