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.