35.3 Konfliktanalyse
Stehen mehrere Datenzeilen zur Aktualisierung an, wird der DataAdapter versuchen, eine nach der anderen an die Datenbank zu senden. Wie im letzten Beispielprogramm zu sehen war, wird der DataAdapter eine DBConcurrencyException auslösen und die verbleibenden Änderungen nicht mehr an die Datenbank schicken. Das ist das Standardverhalten.
Sie können den DataAdapter anweisen, nach einem etwaigen Konflikt seine Aufgabe fortzusetzen und die verbleibenden Änderungen zu übermitteln. Dazu setzen Sie seine Eigenschaft ContinueUpdateOnError=true. Eine Exception wird in diesem Fall nicht ausgelöst. Stattdessen stehen Ihnen zwei andere Optionen zur Verfügung, mit den aufgetretenen Konflikten umzugehen:
- Sie informieren den Benutzer lediglich, welche Datenzeilen nicht aktualisiert werden konnten.
- Sie implementieren über die reine Information des fehlgeschlagenen Aktualisierungsversuchs hinaus auch eine Konfliktlösung. Dazu benötigt der Benutzer alle zur Verfügung stehenden Informationen, unter anderem auch diejenige, wie der neue aktuelle Inhalt der konfliktverursachenden Datenzeile in der Datenbank ist.
Beide Szenarien wollen wir uns nun ansehen.
35.3.1 Den Benutzer über fehlgeschlagene Aktualisierungen informieren
Legen Sie vor dem Aufruf der Update-Methode die Eigenschaft ContinueUpdateOnError auf true fest, verursacht ein fehlgeschlagener Aktualisierungsversuch keine Ausnahme mehr. Stattdessen wird die Eigenschaft HasErrors des entsprechenden DataRow-Objekts auf true gesetzt, ebenso die gleichnamige Eigenschaft des DataSets und der DataTable. Eine DataRow hat eine Eigenschaft RowError. Diese enthält nach dem misslungenen Versuch eine Fehlermeldung.
Im folgenden Beispielprogramm wird der Einsatz der Eigenschaften ContinueUpdateOnError, HasErrors und RowError gezeigt. Das Beispielprogramm setzt dasjenige des vorhergehenden Abschnitts fort und ergänzt nur noch die notwendigen Passagen.
// Beispiel: ..\Kapitel 35\HasErrorsSample
static void Main(string[] args) {
[...]
// Simulation eines Konflikts
Console.Write("Konflikt simulieren ...");
Console.ReadLine();
// Datenbank aktualisieren
da.ContinueUpdateOnError = true;
da.Update(ds);
if (ds.HasErrors) {
string text = "Folgende Zeilen konnten nicht aktualisiert werden:";
foreach (DataRow row in ds.Tables[0].Rows)
if (row.HasErrors) {
Console.WriteLine(text);
Console.WriteLine("ID: {0}, Fehler: {1}", row["ProductID"], row.RowError);
}
}
else
Console.WriteLine("Die Aktualisierung war erfolgreich.");
Console.ReadLine();
}
Listing 35.5 Code des Beispielprogramms »HasErrorsSample«
Nach dem Aufruf von Update auf den DataAdapter wird zuerst mit
das DataSet dahingehend untersucht, ob tatsächlich ein Konflikt vorliegt. HasErrors ist false, wenn die Datenbank die Änderungen angenommen hat. true signalisiert hingegen, dass wir alle Datenzeilen in der Tabelle des DataSets durchlaufen müssen, um die konfliktverursachenden Zeilen zu finden. Der Code wird fündig, wenn er auf eine Datenzeile mit HasErrors=true trifft.
foreach (DataRow row in ds.Tables[0].Rows)
if (row.HasErrors) {
[...]
}
Jetzt können wir reagieren. Im einfachsten Fall lassen wir uns zumindest die ID des betreffenden »Übeltäters« ausgeben – so wie in diesem Beispiel. Sie können die Information natürlich auch dazu benutzen, dem Benutzer die Möglichkeit zu geben, die Konfliktursache zu beseitigen, denn der Verursacher ist ermittelt.
35.3.2 Konfliktverursachende Datenzeilen bei der Datenbank abfragen
Meist genügt es nicht, nur zu wissen, wer Konfliktverursacher ist. Es wird darüber hinaus auch eine Lösung angestrebt. Dies bedarf aber einer genaueren Analyse der Umstände, die zu einem Konflikt führen können. Dabei sind vier Situationen zu beachten:
- Ein Anwender versucht, eine Datenzeile mit einem neuen Primärschlüssel hinzuzufügen, der bereits in der Tabelle existiert.
- Ein Anwender versucht, einen Datensatz zu ändern, den ein zweiter Benutzer zuvor geändert hat.
- Es wird versucht, eine Datenzeile zu ändern, die zwischenzeitlich gelöscht worden ist.
- Es wird versucht, eine Datenzeile zu löschen, die bereits gelöscht ist. In der Regel wird man aber diesem Konflikt keine Beachtung schenken müssen.
Wird versucht, einen bereits vorhandenen Primärschlüssel für einen neuen Datensatz ein zweites Mal zu vergeben, scheint die Lösung des Problems noch recht einfach zu sein: Es muss nur ein anderer Primärschlüssel vergeben werden. Aber das könnte eine falsche Entscheidung sein. Können Sie denn sicherstellen, dass nicht zwei Anwender versuchen, den gleichen Datensatz zur Tabelle hinzuzufügen? Falls Sie diese Situation nicht berücksichtigen, liegen im schlimmsten Fall zwei identische Datensätze vor.
Um eine präzise Konfliktlösung der beiden anderen relevanten Konflikte zu ermöglichen, fehlen uns Informationen, die nur in der Datenbank zu finden sind. Was wir brauchen, ist eine neue Originalversion der konfliktverursachenden Datenzeile.
Der DataAdapter hilft uns an dieser Stelle 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 wird ausgelöst, bevor eine Zeile übermittelt wird, RowUpdated tritt unmittelbar nach der Übermittlung auf.
Für unsere Lösung interessiert uns natürlich nur das Ereignis RowUpdated, dessen zweiter Parameter vom Typ SqlRowUpdatedEventArgs uns mit allen Informationen versorgt, die wir zur Konfliktanalyse und der anschließenden Konfliktlösung benötigen. In Tabelle 35.2 sind die Eigenschaften des EventsArgs-Parameters des RowUpdated-Ereignisses aufgeführt.
Der Vollständigkeit halber folgt jetzt auch noch die Tabelle mit den Membern der Enumeration UpdateStatus, die von der Eigenschaft Status des SqlRowUpdatedEventArgs-Objekts offengelegt wird.
Member | Beschreibung |
Continue |
Der DataAdapter soll mit der Verarbeitung von Zeilen fortfahren. |
ErrorsOccured |
Der Ereignishandler meldet, dass die Aktualisierung als Fehler behandelt werden soll. |
SkipAllRemainingRows |
Die aktuelle Zeile und alle restlichen Zeilen sollen nicht aktualisiert werden. |
SkipCurrentRow |
Die aktuelle Zeile soll nicht aktualisiert werden. |
Wie können wir nun das Ereignis zu unserem Nutzen einsetzen?
Es gilt zunächst herauszufinden, ob das Update einer Datenzeile zu einem Konflikt geführt hat. Hierzu prüfen wir, ob die Eigenschaft Status des SqlRowUpdatedEventArgs-Objekts den Enumerationswert UpdateStatus.ErrorsOccured aufweist.
private void da_RowUpdated(object sender, SqlRowUpdatedEventArgs e) {
if (e.Status == UpdateStatus.ErrorsOccurred) {
[...]
}
}
Listing 35.6 Prüfen, ob ein Fehler beim Aktualisieren aufgetreten ist
Um zu einer Konfliktlösung zu kommen, werden die konfliktverursachenden Datenzeilen in ihrer neuen, aktuellen Originalversion aus der Datenbank benötigt. Deshalb wird innerhalb des Ereignishandlers eine erneute Abfrage an die Datenbank geschickt und der Primärschlüssel der konfliktverursachenden Datenzeile als Filter benutzt. Da uns das EventArgs-Objekt in seiner Eigenschaft Row die Referenz auf die entsprechende Datenzeile mitteilt, stellt das kein Problem dar.
Sinnvollerweise stellt man ein eigenes DataSet-Objekt für alle abgefragten Datenzeilen zur Verfügung (hier: dsConflict), ebenso einen separaten SqlDataAdapter (hier: daConflict). Der Aufruf der Fill-Methode bewirkt, dass entweder genau eine Datenzeile das Ergebnis des Aufrufs bildet oder keine. Die Fill-Methode teilt uns über ihren Rückgabewert die Anzahl der Datensätze mit, die die Ergebnismenge bilden. Der Rückgabewert ist ganz entscheidend, um festzustellen, welche Ursache der Konflikt hat. Sehen wir uns nun zuerst das entsprechende Codefragment zu dem Gesagten an:
DataSet dsConflict = new DataSet();
SqlDataAdapter daConflict = new SqlDataAdapter();
[...]
private void da_RowUpdated(object sender, SqlRowUpdatedEventArgs e) {
if (e.Status == UpdateStatus.ErrorsOccurred) {
SqlCommand cmdConflict = new SqlCommand();
string sql = "SELECT ProductID, ProductName, UnitPrice, " +
"UnitsInStock FROM Products WHERE ProductID = " +
e.Row["ProductID"];
cmdConflict = new SqlCommand();
cmdConflict.Connection = con;
cmdConflict.CommandText = sql;
daConflict.SelectCommand = cmdConflict;
int result = daConflict.Fill(dsConflict);
}
}
Listing 35.7 Abrufen der konfliktverursachenden Datenzeile aus der Datenbank
Grundsätzlich können beim Aktualisieren einer Datenzeile zwei verschiedene Exceptions auftreten:
- SqlException
- DBConcurrencyException
SqlException beschreibt Ausnahmen, die der SQL Server zurückgibt. Das wäre 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. Das ist der Fall, wenn die Anzahl der aktualisierten Datenzeilen 0 ist. Um festzustellen, welche Ausnahme ausgelöst worden ist, brauchen Sie nur den in der Eigenschaft Errors des SqlRowUpdatedEventArgs-Objekts enthaltenen Ausnahmetyp zu untersuchen.
if (e.Errors.GetType() == typeof(SqlException)) {
[...]
}
else if (e.Errors.GetType() == typeof(DBConcurrencyException)) {
[...]
}
Anschließend kommt es zur Auswertung der Anzahl der Datensätze, die in der Variablen result stehen. Es kann sich nur um die Zahl 0 oder 1 handeln, woraus weitere Rückschlüsse gezogen werden können.
Betrachten wir zuerst den Fall, dass Errors ein SqlException-Objekt enthält.
if (e.Errors.GetType() == typeof(SqlException)) {
if (result == 1)
Console.WriteLine("Der PS existiert bereits.");
}
Ist in diesem Fall der Inhalt von result 1, handelt es sich um den Versuch, einen neuen Datensatz mit einem Primärschlüssel hinzuzufügen, wobei der Primärschlüssel in der Originaltabelle bereits vergeben ist. Beschreibt result die Zahl 0, liegt ein anderer Datenbankfehler vor.
Handelt es sich um den Ausnahmetyp DBConcurrencyException, wurde der Versuch, die Änderung an einer Datenzeile in die Originaltabelle zu schreiben, abgelehnt. Die Parallelitätsverletzung kann zwei Ursachen haben:
- Ein anderer Anwender hat den Datensatz zwischenzeitlich geändert. Der Inhalt der Variablen result muss in diesem Fall die Zahl 1 sein. Mit anderen Worten: Der Datensatz existiert noch.
- Wird der Inhalt von result mit der Zahl 0 beschrieben, wurde der Datensatz von einem anderen Anwender gelöscht. Der entsprechende Primärschlüssel existiert nicht mehr.
Der else if-Zweig muss demnach wie folgt codiert werden:
[...]
else if (e.Errors.GetType() == typeof(DBConcurrencyException)) {
// ist Anzahl=1 -> anderer Benutzer hat DS geändert
if (result == 1)
Console.WriteLine("Ein anderer User hat den Datensatz geändert.");
else
Console.WriteLine("Datensatz existiert nicht in der Datenbank.");
}
Das folgende Beispielprogramm zeigt den Code im Zusammenhang. Ausgangspunkt sei wieder das Beispielprogramm ManuelleAktualisierung, das entsprechend ergänzt wird. Neben der Implementierung des Ereignishandlers des RowUpdated-Ereignisses wird nach der Aktualisierung auch das DataSet mit allen konfliktverursachenden Datenzeilen abgefragt und ausgegeben.
// Beispiel: .. \Kapitel 35\KonfliktAnalyse
class Program {
static DataSet dsConflict = new DataSet();
static SqlDataAdapter daConflict = new SqlDataAdapter();
static SqlConnection con = new SqlConnection();
static void Main(string[] args) {
[...]
// Datenbank aktualisieren
da.ContinueUpdateOnError = true;
da.RowUpdated += new SqlRowUpdatedEventHandler(da_RowUpdated);
da.Update(ds);
// Konflikt-DataSet abrufen
if (dsConflict.Tables.Count > 0) {
Console.WriteLine("\n{0,-5}{1,-35}{2,-12}{3}",
"ID", "ProductName", "UnitPrice", "UnitsInStock");
Console.WriteLine(new string('-', 65));
foreach (DataRow item in dsConflict.Tables[0].Rows)
Console.WriteLine("{0,-5}{1,-35}{2,-12}{3}",
item["ProductID"], item["ProductName"],
item["UnitPrice"], item["UnitsInStock"]);
}
Console.ReadLine();
}
static void da_RowUpdated(object sender, SqlRowUpdatedEventArgs e) {
if (e.Status == UpdateStatus.ErrorsOccurred) {
SqlCommand cmdConflict = new SqlCommand();
string sql = "SELECT ProductID, ProductName, UnitPrice, UnitsInStock FROM
Products WHERE ProductID = " + e.Row["ProductID"];
cmdConflict = new SqlCommand();
cmdConflict.Connection = con;
cmdConflict.CommandText = sql;
daConflict.SelectCommand = cmdConflict;
int result = daConflict.Fill(dsConflict);
if (e.Errors.GetType() == typeof(SqlException)) {
// Prüfen, ob es einen DS mit einem bestimmten PS gibt
if (result == 1)
Console.WriteLine("Der PS existiert bereits.");
}
else if (e.Errors.GetType() == typeof(DBConcurrencyException)) {
// ist Anzahl=1 -> anderer Benutzer hat DS geändert
if (result == 1)
Console.WriteLine("Ein anderer User hat den Datensatz geändert.");
else
Console.WriteLine("Datensatz existiert nicht in der Datenbank.");
}
}
}
}
Listing 35.8 Analyse bei einem Konflikt
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.