# 27 ADO.NET – Aktualisieren der Datenbank Im letzten Kapitel haben Sie gelernt, wie das Ergebnis einer Abfrage in einem DataSet organisiert ist. In diesem Kapitel werden wir uns ansehen, wie die lokalen Daten ediert werden können. ## 27.1 Aktualisieren mit dem »CommandBuilder« Eine DataTable können Sie mit Daten aus jeder Datenquelle füllen. Handelt es sich dabei um eine Datenbank und können die Benutzer die Daten auch ändern, müssen die Änderungen zu einem bestimmten Zeitpunkt 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 getestet, nachdem Sie Zeilen Ihres DataSets geändert hatten. Sie werden dabei bestimmt einen Laufzeitfehler erhalten haben. Sehen Sie sich dazu das folgende Beispiel an, in dem eine neue Datenzeile hinzugefügt und eine vorhandene geändert wird. Nach Abschluss der Änderungen wird die Methode Update des SqlDataAdapters aufgerufen.
    // ------------------------------------------------------------------
    // Beispiel: ...\Kapitel 27\CommandBuilderSample
    // ------------------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        SqlConnection con = new SqlConnection();
        con.ConnectionString = "...";
        SqlCommand cmd = new SqlCommand();
        cmd.Connection = con;
        cmd.CommandText = "SELECT ProductID, ProductName, " +
                          "UnitsInStock, Discontinued FROM Products";
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.FillSchema(ds, SchemaType.Source);
        ds.Tables[0].Columns["ProductID"].AutoIncrementSeed = -1;
        ds.Tables[0].Columns["ProductID"].AutoIncrementStep = -1;
        da.Fill(ds);
        // neue Datenzeile hinzufügen
        DataRow newRow = ds.Tables[0].NewRow();
        newRow["ProductName"] = "Camembert";
        newRow["UnitsInStock"] = 100;
        newRow["Discontinued"] = false;
        ds.Tables[0].Rows.Add(newRow);
        // Datenzeile ändern
        DataRow[] editRow = ds.Tables[0].Select("ProductName='Tofu'");
        if (editRow.Length == 1) {
          editRow[0].BeginEdit();
          editRow[0]["UnitsInStock"] = 1000;
          editRow[0].EndEdit();
        }
        else
          Console.WriteLine("Datenzeile 'Tofu' nicht gefunden.");
        // Datenbank aktualisieren
        int count = da.Update(ds);
        Console.WriteLine("{0} Datenzeilen aktualisiert", count);
        Console.ReadLine();
      }
    }
Wo liegt aber nun die Ursache des Laufzeitfehlers, der in der Anweisung auftritt, die die Methode Update aufruft? Denken wir einmal daran, wie die Abfolge ist, bis der SqlDataAdapter eine Auswahlabfrage an die Datenbank schickt. Wir hatten ein SqlCommand-Objekt erzeugt und diesem das SELECT-Statement übergeben. Bei der Instanziierung haben wir dem SqlDataAdapter das SqlCommand-Objekt über den Konstruktoraufruf bekanntgegeben. Der SqlDataAdapter speichert das in seiner Eigenschaft SelectCommand. Der SqlDataAdapter hat aber noch drei weitere Eigenschaften, die nach einem SqlCommand-Objekt verlangen: InsertCommand DeleteCommand UpdateCommand So wie über SelectCommand die vom SqlDataAdapter abzusetzende Auswahlabfrage bekannt ist, benötigt der Adapter auch noch SqlCommand-Objekte, die die SQL-Statements INSERT, DELETE und UPDATE beschreiben. Erfreulicherweise stellt der SqlDataAdapter nicht nach festgeschriebenen Regeln automatisch Aktualisierungsstatements bereit. Dieses Verhalten gibt uns jedoch die Möglichkeit, selbst Einfluss auf die Aktualisierung zu nehmen. Darauf werden wir später noch genauer eingehen. Die Folge ist jedenfalls, dass die Eigenschaften Insert-, Update- und DeleteCommand zunächst den Inhalt null haben. Für das Erzeugen von SQL-Aktualisierungsstatements bietet uns ADO.NET die Klasse SqlCommandBuilder. Übergeben Sie bei der Instanziierung dieser Klasse dem Konstruktor die Referenz auf den SqlDataAdapter.
    SqlCommandBuilder cmb = new SqlCommandBuilder(da);
Da der SqlCommandBuilder nun das SqlDataAdapter-Objekt kennt, weiß er, wie die SELECT-Auswahlabfrage aussieht. Auf dieser Grundlage erzeugt SqlCommandBuilder die SQL-Befehle INSERT, DELETE und UPDATE, verpackt sie in eine Zeichenfolge und weist sie jeweils einem neuen SqlCommand-Objekt zu. Die drei SqlCommand-Objekte werden den Eigenschaften UpdateCommand, InsertCommand und DeleteCommand des SqlDataAdapters übergeben. Unabhängig davon, ob im DataSet eine Zeile gelöscht, hinzugefügt oder editiert worden ist, wird der SqlDataAdapter mit den vom SqlCommandBuilder erzeugten Kommandos die Originaldatenbank aktualisieren. Kommen wir zu dem eingangs gezeigten Beispiel zurück. Wenn Sie vor dem Aufruf von Update ein SqlCommandBuilder-Objekt erzeugen und dessen Konstruktor die Instanz des SqlDataAdapters übergeben, wird die Aktualisierung erfolgreich sein.
    ...
    SqlCommandBuilder cmb = new SqlCommandBuilder(da);
    da.Update(ds);
### 27.1.1 Von »SqlCommandBuilder« generierte Aktualisierungsstatements SqlCommandBuilder erzeugt Aktualisierungscode auf Grundlage des SELECT-Statements. Doch wie sieht die Aktualisierungslogik exakt aus? Wir wollen uns das nun ansehen. Die Grundlage dafür bildet folgende Abfrage:
    SELECT ProductID, ProductName, UnitsInStock FROM Products
Sie können sich die Aktualisierungsstatements ausgeben lassen, indem Sie die Methoden GetUpdateCommand, GetInsertCommand oder GetDeleteCommand des SqlCommandBuilders aufrufen. Alle liefern ein Objekt vom Typ SqlCommand, über dessen Eigenschaft CommandText Sie das jeweilige SQL-Statement abfragen können. Es genügt, wenn wir uns nur eines der drei ansehen.
    UPDATE [Products]
    SET [ProductName] = @p1, [UnitsInStock] = @p2
    WHERE (([ProductID] = @p3) AND ([ProductName] = @p4) AND
    ((@p5 = 1 AND [UnitsInStock] IS NULL) OR ([UnitsInStock] = @p6)))
Sie erkennen, dass hinter der WHERE-Klausel alle Spalten der SELECT-Abfrage als Suchkriterium nach dem zu editierenden Datensatz aufgeführt sind. Die Parameter @p3 bis @p6 werden mit den Daten gefüllt, die unter DataRowVersion.Original aus dem Dataset bezogen werden, und @p1 bis @p3 erhalten die Daten aus DataRowVersion.Current. In gleicher Weise werden auch die INSERT- und DELETE-Anweisungen vom SqlCommandBuilder generiert. ### 27.1.2 Konfliktsteuerung in einer Mehrbenutzerumgebung Meistens werden Datenbanken in einem Netzwerk betrieben. Das ist ein ganz wesentlicher Gesichtspunkt bei der Bereitstellung der SQL-Aktualisierungsstatements, denn in solchen Umgebungen müssen wir nun noch die Möglichkeit betrachten, dass ein zweiter User gleichzeitig mit denselben Daten arbeitet. Dann stellt sich auch sofort die Frage, was passiert, wenn versucht wird, eine Datenzeile zu aktualisieren, die ein anderer Benutzer zwischenzeitlich geändert hat. Möglicherweise tritt dabei ein Konflikt auf. Ob ein Konflikt auftritt, hängt ganz entscheidend davon ab, wie die WHERE-Klausel des Aktualisierungsstatements formuliert ist. Dabei müssen wir mehrere Fälle betrachten, die wir nun theoretisch untersuchen wollen. #### Die WHERE-Klausel enthält alle Spalten Betrachten wir sofort ein Beispiel, bei dem alle Spalten der SELECT-Abfrage in der WHERE-Klausel angegeben sind, und nehmen wir an, Anwender A und Anwender B rufen praktisch gleichzeitig dieselbe Datenzeile in der Produkttabelle auf. Ändert Anwender A die Spalte ProductName, könnte das UPDATE-Statement beispielsweise wie folgt aussehen:
    UPDATE Products
    SET ProductName="Kuchen", UnitsInStock=18
    WHERE ProductID=1 AND ProductName="Chai" AND
    UnitsInStock=18
In der WHERE-Klausel sind den Spaltenangaben genau die Werte zugeordnet, die Anwender A aus der Datenbank bezogen hat. Anwender A aktualisiert erfolgreich, weil die Datenzeile, die von der WHERE-Klausel beschrieben wird, in der Datenbank gefunden wird. Danach versucht Anwender B seine Aktualisierung der Datenbank mitzuteilen. Ob er dabei die Spalte ProductName geändert hat oder UnitsInStock, spielt keine Rolle. Entscheidend ist, dass die WHERE-Klausel die Spalten ProductID, UnitsInStock und ProductName enthält.
    UPDATE Products
    SET ProductName="Senf", UnitsInStock=56
    WHERE ProductID=1 AND ProductName="Chai" AND
    UnitsInStock=18
Der Aktualisierungsversuch wird scheitern. Schade, aber ein Datensatz mit dem Primärschlüssel 1 und den Spalteninhalten ProductName = Chai und UnitsInStock = 18 wird nicht mehr gefunden, weil Anwender A den Produktnamen vorher geändert hat.
Hinweis
Immer dann, wenn beim Absetzen eines UPDATE- oder DELETE-Statements in der WHERE-Klausel eine Datenzeile beschrieben wird, die nicht in der Datenbank gefunden wird, haben wir es mit einem Konflikt zu tun.
Bei diesem Szenario »gewinnt« immer der Anwender, der als Erster seine Änderungen an die Datenbank übermittelt. Der Anwender, der seine Änderungen später zur Datenbank schickt, hat das Nachsehen. Sein Aktualisierungsversuch misslingt. Es kommt zu einem Parallelitätskonflikt. Dieses Szenario wird auch als First-in-wins bezeichnet. #### Die WHERE-Klausel enthält nur die Primärschlüsselspalte Betrachten wir nun einen anderen Fall. Wieder helfen uns die beiden fiktiven Anwender A und B dabei, den Sachverhalt zu verstehen. Beide Anwender rufen praktisch gleichzeitig dieselbe Datenzeile ab und nehmen Änderungen an einer der Spalten vor. Anwender A aktualisiert die Originaldatenbank zuerst, beispielsweise die Spalte ProductName des ersten Datensatzes:
    UPDATE Products
    SET ProductName="Marmorkuchen", UnitsInStock=56
    WHERE ProductID=1
Anwender B übermittelt seine Änderung, nachdem Anwender A den ersten Datensatz geändert hat. Nehmen wir an, Anwender B hat den Inhalt in der Spalte UnitsInStock editiert, so könnte sein vollständiges Aktualisierungsstatement wie folgt lauten:
    UPDATE Products
    SET ProductName="Chai", UnitsInStock=56
    WHERE ProductID=1
Die Aktualisierung wird erfolgreich sein, wenn der Datensatz mit der angegebenen ProductID in der Tabelle gefunden wird. Die Änderungen von Anwender A sieht Anwender B nicht; er wird vielleicht auch niemals erfahren, welche Daten Anwender A geändert hat, denn er überschreibt die Änderung von Anwender A in der Spalte ProductName mit dem alten Wert. Dieses Szenario, bei dem die letzte Änderung grundsätzlich immer erfolgreich an die Datenbank übermittelt werden kann, wird als Last-in-wins bezeichnet. Die Identifizierung der zu ändernden Datenzeile nur anhand der Primärschlüsselspalte ist folglich denkbar ungeeignet, wenn Sie vermeiden müssen, dass Anwender B unwissentlich geänderte Daten überschreibt. Können Sie davon ausgehen, dass die letzte Aktualisierung zweifelsfrei diejenige mit den »besten« Daten ist, sollten Sie sich für diese Variante entscheiden.
Hinweis
Natürlich wird es auch hier zu einem Konfliktfall kommen, sollte Anwender A dieselbe Datenzeile nicht geändert, sondern bereits gelöscht haben.
#### Weitere Szenarien Die beiden zuvor beschriebenen Szenarien stellen Grenzfälle dar. Zwischen diesen beiden gibt es, abhängig von der zugrunde liegenden Tabelle, unzählige weitere Optionen. Gehen wir beispielsweise wieder davon aus, dass zwei Benutzer mit
    SELECT ProductID, ProductName, UnitsInStock FROM Products
Datenzeilen abrufen. Stellen wir uns weiter vor, dass der Artikelbestand UnitsInStock durchaus überschrieben werden darf, aber eine Änderung der Spalte ProductName nicht erlaubt ist. Unabhängig davon, mit welchem UPDATE-Statement Benutzer A die Datenbank aktualisiert hat, muss unser fiktiver Benutzer B das folgende UPDATE-Statement zur Datenbank schicken:
    UPDATE Products
    SET ProductName="Kuchen", UnitsInStock=18
    WHERE ProductID=1 AND ProductName="Chai"
Hat Anwender A nur den Lagerbestand UnitsInStock geändert, wird der Datensatz gefunden und die in der SET-Klausel angegebenen Werte werden eingetragen. Dabei werden die neuen Daten des Anwenders A durch die alten Daten überschrieben, weil die Spalte UnitsInStock in der SET-Klause von Anwender B enthalten ist. Hat Anwender A jedoch den Artikelbezeichner ProductName editiert, kommt es zu einem Konflikt. Selbstverständlich könnte man sich auch eine Abfrage vorstellen, in der die SET-Klausel nur die veränderten Spaltenwerte beschreibt und die WHERE-Klausel neben der Angabe der Primärschlüsselspalte nur die Spalten, die als konfliktverursachend eingestuft werden und darüber hinaus auch verändert worden sind. Bezogen auf unser letztes Beispiel sollte das UPDATE-Statement wie folgt aussehen:
    UPDATE Products
    SET ProductName="Kuchen"
    WHERE ProductID=1 AND ProductName="Chai"
Jetzt wird die Spalte UnitsInStock nicht mehr in der SET-Klausel angeführt. Falls Anwender A diese Spalte geändert hat, ist seine Aktualisierung weiterhin gültig und in der Datenzeile wird nur der Produktbezeichner editiert. Sollten die beiden Extremszenarien Last-in-wins und First-in-wins nicht Ihren Anforderungen an die Aktualisierung entsprechen, eröffnet sich ein weites Feld der Möglichkeiten, das mit der Komplexität einer Tabelle steigt. Hier bedarf es sicherlich einer gründlichen Analyse, was im Einzelfall als Konflikt zu betrachten ist. ### 27.1.3 Die Eigenschaft »ConflictOption« des »SqlCommandBuilder« Grundsätzlich ist das Aktualisieren einer Datenquelle mit dem SqlCommandBuilder-Objekt sehr einfach. Aber diese Einfachheit hat ihren Preis, denn wir müssen uns mit den Charakteristika des Objekts abfinden und haben nur wenig Einfluss darauf, wie die Daten zurückgeschrieben werden. Der SqlCommandBuilder generiert, wie Sie weiter oben gesehen haben, Abfragen, die zur Identifikation einer Datenzeile in der Tabelle einer Datenbank alle Spalten einschließen, die mit SELECT abgefragt worden sind. Er bildet demnach per Vorgabe das First-in-wins-Szenario ab. Eine Änderung der Daten in der Datenbank führt zu der Ausnahme DBConcurrencyException, wenn ein anderer User eine dieser Spalten genau 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 SqlCommandBuilder 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.

Tabelle 27.1 Die Enumeration »ConflictOption«

Konstante | Beschreibung
CompareAllSearchableValues | UPDATE- und DELETE-Anweisungen schließen alle Spalten aus der Tabelle, nach denen gesucht werden kann, in die WHERE-Klausel ein. Das ist der Standard.
CompareRowVersion | Wenn in der Tabelle eine Timestamps-Spalte vorhanden ist, wird sie in der WHERE-Klausel für alle generierten UPDATE-Anweisungen verwendet.
OverwriteChanges | Alle UPDATE- und DELETE-Anweisungen enthalten nur die Spalten des Primärschlüssels in der WHERE-Klausel.
Der Wert ConflictOption.CompareAllSearchableValues ist der Standardwert. In diesem Szenario wird immer die erste Änderung in einer Datenzeile zum Erfolg und die dann folgende Änderung zu einem Konflikt führen. Dieses Szenario entspricht dem First-in-Wins-Szenario. Das ist die Vorgabe. Mit ConflictOption.OverwriteChanges teilen Sie dem SqlCommandBuilder mit, nur die Primärschlüsselspalte(n) in die WHERE-Klausel einzubeziehen. Das hat zur Folge, dass die Änderungen des ersten Benutzers von den nachfolgenden Änderungen überschrieben werden. Diese Einstellung bildet das Last-in-Wins-Szenario ab. Ein Timestamp ist ein automatisch generierter, eindeutiger 8-Byte-Wert. Mithilfe der Timestamp-Spalte einer Zeile können Sie sehr einfach ermitteln, ob sich ein Wert in der Datenzeile geändert hat, seit er eingelesen wurde. Der Timestamp-Wert wird bei jeder Aktualisierung geändert. Ist er beim Absetzen des UPDATE-Statements identisch, liegt keine andere zwischenzeitliche Aktualisierung vor. Mit ConflictOption.CompareRowVersion weisen Sie den SqlCommandBuilder an, in der WHERE-Klausel nur die Primärschlüsselspalte(n) und die Timestamp-Spalte aufzunehmen. ### 27.1.4 Die Eigenschaft »SetAllValues« Betrachten Sie noch einmal das Beispiel mit Benutzer A und Benutzer B. Es wäre denkbar, dass weder das Last-in-Wins- noch das First-in-Wins-Szenario die Forderung passend erfüllt. Vielleicht soll auch jede Änderung an einer Datenzeile akzeptiert werden, solange dieselbe Spalte nicht von einem anderen User verändert worden ist. Das bedeutet, dass in der WHERE-Klausel neben dem Primärschlüssel auch die jeweils geänderte Spalte mit ihrem Ursprungswert angegeben werden muss. Benutzer A müsste 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 UnitsInStock, wird diese Spalte seinem UPDATE-Statement hinzugefügt:
    UPDATE Products
    SET UnitsInStock = 2
    WHERE ProductID = 55 AND UnitsInStock = 13
Die Aktualisierung wird erfolgreich sein. Mehr noch, die betroffene Datenzeile in der Datenbank wird beide Änderungen aufweisen.