## 27.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 darüber, welche Datenzeilen nicht aktualisiert werden konnten. Sie implementieren über die reine Information zum fehlgeschlagenen Aktualisierungsversuch 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 lautet. Beide Szenarien wollen wir uns nun ansehen. ### 27.3.1 Benutzer über fehlgeschlagene Aktualisierungen informieren Wenn Sie vor dem Aufruf der Update-Methode die Eigenschaft ContinueUpdateOnError auf true festlegen, 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 Beispiel setzt das Beispielprogramm des vorhergehenden Abschnitts fort und ergänzt nur noch die notwendigen Passagen.
    // ------------------------------------------------------------------
    // Beispiel: ...\Kapitel 27\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();
    }
Nach dem Aufruf von Update auf dem DataAdapter wird zuerst mit
    if (ds.HasErrors)
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. ### 27.3.2 Konfliktverursachende Datenzeilen bei der Datenbank abfragen Meist genügt es nicht, nur zu wissen, wer der 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 RowUpdated 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 zur anschließenden Konfliktlösung benötigen. In Tabelle 27.2 sind die Eigenschaften des EventsArgs-Parameters des RowUpdated-Ereignisses aufgeführt.

Tabelle 27.2 Eigenschaften des »SqlRowUpdatedEventArgs«-Objekts

Eigenschaft | Beschreibung
Command | Ruft das beim Aufruf von Update ausgeführte SqlCommand ab.
Errors | Ruft alle Fehler ab, die während der Ausführung generiert wurden.
RecordsAffected | Ruft die Anzahl der durch die Ausführung der SQL-Anweisung geänderten, eingefügten oder gelöschten Zeilen ab.
Row | Ruft die durch ein Update gesendete DataRow ab.
StatementType | Ruft den Typ der ausgeführten SQL-Anweisung ab.
Status | Ruft einen Wert der Enumeration UpdateStatus ab oder legt diesen fest.
TableMapping | Ruft das durch ein Update gesendete DataTableMapping ab.
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.

Tabelle 27.3 Member der Enumeration »UpdateStatus«

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? Wir müssen zunächst herausfinden, 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) {
        ...
      }
    }
Um zu einer Konfliktlösung zu kommen, werden die konfliktverursachenden Datenzeilen in ihrer neuen, aktuellen Version 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);
      }
    }
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 Ausnahmetyp zu untersuchen, der in der Eigenschaft Errors des SqlRowUpdatedEventArgs-Objekts enthalten ist.
    if (e.Errors.GetType() == typeof(SqlException)) {
      // Anweisung
    }
    else if (e.Errors.GetType() == typeof(DBConcurrencyException)) {
      // Anweisungen
    }
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 eins, 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 Programm zeigt den Code im Zusammenhang. Ausgangspunkt ist wieder das Programm ManuelleAktualisierung, das entsprechend ergänzt wird. Neben der Implementierung des Ereignishandlers des RowUpdated-Ereignisses wird nach der Aktualisierung auch das DataSet mit den konfliktverursachenden Datenzeilen abgefragt und ausgegeben.
    // ------------------------------------------------------------------
    // Beispiel: ...\Kapitel 27\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.");
          }
        }
      }
    }