29 Eine Datenbank aktualisieren
Im letzten Kapitel haben wir uns ausgiebig mit den Daten im lokalen Speicher beschäftigt. Sie wissen nun, dass alle Daten in Objekten von Typ DataTable enthalten sind und dass die Tabellen von einem DataSet verwaltet werden. Wir haben hinter die Kulissen der DataTable geschaut und festgestellt, dass die Daten immer im Ursprungszustand (wie von der Originaldatenbank bezogen) und im aktuellen Zustand vorliegen (Stichwort DataRowVersion). Ich habe Ihnen auch gezeigt, welche Möglichkeiten es gibt, die Daten im DataSet in jeglicher Art und Weise zu ändern. Nur die »Krönung« habe ich Ihnen vorenthalten: Sie wissen noch nicht, wie man die geänderten Daten in die Originaldatenbank schreibt. Das soll in diesem Kapitel nachgeholt werden.
Das Zurückschreiben in eine Datenbank ist zunächst einmal grundsätzlich nicht schwierig. Sie brauchen nur ein passend formuliertes SQL-UPDATE-Kommando an den Datenbankserver zu schicken, denn nur über SQL-Statements können Sie mit einer Datenbank kommunizieren. Im einfachsten Fall genügt es, sich dazu an die Methode ExecuteNonQuery des Command-Objekts zu erinnern.
Solange Sie sicher davon ausgehen können, dass nur ein Benutzer einen bestimmten Datensatz in der Datenbank ändern kann, ist das Thema der Aktualisierung schnell erledigt. Sie schieben den geänderten Datensatz in die Originaldatenbank – fertig. Aber ein solches Szenario ist eher selten anzutreffen. Vielmehr haben wir es in der Regel mit Mehrbenutzerumgebungen zu tun, in denen wir davon ausgehen müssen, dass zwei oder gar noch mehr Anwender denselben Datensatz zu ändern versuchen. Denken Sie beispielsweise nur an die Online-Buchung einer Reise. Während mehrere potenzielle Reiseinteressenten noch im Familienrat tagen und sich noch überlegen, ob sie das Superangebot eines Reiseveranstalters buchen sollen, bucht ein anderer Interessent schnell entschlossen die Reise. Versucht später ein anderer Interessent, die Reise zu buchen, wird er nach dem Absenden der Buchungsinformationen darauf aufmerksam gemacht, dass er zu spät sei. Sicher ist das noch ein recht einfach konstruierter Fall, aber Sie können sicher schon erkennen, dass es auch durchaus noch viel komplexere gibt, die nicht nur in der Ablehnung eines Aktualisierungsversuchs enden.
Solange wir uns im Umfeld von verbindungsorientierten Datenbankzugriffen bewegen, sind solche Problematiken recht einfach zu lösen, da wir von den Sperrmechanismen der Datenbank unterstützt werden. Da ADO.NET jedoch eine verbindungslose Datenzugriffstechnologie ist, sind die Verhältnisse häufig nicht ganz so einfach und müssen gründlich analysiert werden.
Wann treten Konflikte auf?
Ehe ich Ihnen zeige, wie Sie mit ADO.NET eine Datenbank aktualisieren, sollten wir uns klarmachen, unter welchen Umständen in einer verbindungslosen Umgebung Konflikte auftreten können.
Ein Anwender, nennen wir ihn A, bezieht die Daten von einer Datenbank. Diese werden im lokalen Speicher vorgehalten, und die Änderungen – Anwender A ändert den ursprünglichen Produktnamen Käse in Cheese – wirken sich zunächst nur im DataSet aus. Zu einem späteren Zeitpunkt entscheidet sich Anwender A, die Originaldatenbank mit der vorgenommenen Änderung zu aktualisieren, und schickt die geänderte Datenzeile zur Datenbank. Das SQL-Statement könnte dabei vielleicht wie folgt aussehen:
UPDATE Products SET ProductName = ’Cheese’ WHERE ProductID = 55 AND ProductName = ’Käse’
Die Verbindung zur Datenbank wird geöffnet und das UPDATE-Statement bei der Datenbank abgesetzt, die ihrerseits die betroffene Zeile aktualisiert. Beachten Sie bitte die WHERE-Klausel, die bei einem UPDATE-Statement dazu dient, in der Datenbank nach der zu aktualisierenden Datenzeile zu suchen. In allen Feldern der WHERE-Klausel werden die von der Datenbank bezogenen Originaldaten angegeben. Sie bilden das Kriterium, nach dem in der Datenbank nach dem zu aktualisierenden Datensatz gesucht wird.
Aber es gibt vielleicht noch einen zweiten Anwender, Benutzer B, der quasi gleichzeitig dieselbe Datenzeile vom Datenbankserver bezogen hat und in seinem lokalen Speicher bearbeitet. Allerdings ändert Benutzer B nicht den Produktnamen, sondern den Einzelpreis. Selbstverständlich möchte auch Benutzer B seine Änderungen der Originaldatenbank mitteilen. Nehmen wir an, er versucht es mit dem folgenden UPDATE-Statement:
UPDATE Products SET UnitPrice = 12.98 WHERE ProductID = 55 AND ProductName = ’Käse’
Anwender B hat dieselben Daten empfangen wie Anwender A. In der WHERE-Klausel wird daher neben dem Primärschlüsselwert auch der Originalwert der Spalte Productname aufgeführt: Käse. Von der Änderung des Benutzers A weiß Benutzer B nichts.
Abbildung 29.1 Konfliktsituation beim Aktualisieren
Der Aktualisierungsversuch von Benutzer B wird scheitern, denn die Datenzeile, die von der WHERE-Klausel des Benutzers B beschrieben wird, existiert nicht mehr – wegen der Änderung von Anwender A im Feld ProductName. Es kommt zu einem Konflikt.
Anders würde das Ergebnis für Anwender B ausgesehen, enthielte die WHERE-Klausel von UPDATE als einziges Suchkriterium nur den Primärschlüssel der zu aktualisierenden Datenzeile, also:
UPDATE Products SET UnitPrice = 12.98 WHERE ProductID = 55
Der Datensatz wird in der Datenbank gefunden, denn Anwender A hat ein Feld geändert, das in der WHERE-Klausel von Anwender B nicht angegeben ist.
Sehr gut zu erkennen ist, dass die WHERE-Klausel ganz entscheidend dazu beiträgt, ob ein Aktualisierungsversuch gelingt oder scheitert.
An dieser Stelle sollten wir uns auch überlegen, welche denkbaren Situationen auftreten können, die zu einem Konflikt führen können:
- Ein Anwender B editiert eine Datenzeile, die ein Anwender A vorher aktualisiert hat. Je nach Formulierung der WHERE-Klausel wird der Aktualisierungsversuch von Anwender B scheitern, weil die gesuchte Datenzeile in der Originaldatenbank nicht gefunden wird.
- Anwender B versucht eine Datenzeile zu ändern, die Anwender A zuvor gelöscht hat.
- Anwender B löscht eine Datenzeile, die schon gelöscht ist. Meistens wird man einen solchen Konflikt nicht weiter behandeln müssen, aber er muss der Vollständigkeit halber erwähnt werden.
- Anwender B fügt eine neue Datenzeile mit einem Primärschlüssel hinzu, der bereits existiert. Wenn die betreffende Tabelle den Primärschlüssel automatisch vergibt, kann diese Konfliktsituation natürlich nicht auftreten.
Tritt ein Konflikt auf, gilt es, zuerst den Anwender darüber zu informieren, welche Ursache zu einer Ablehnung der Aktualisierung geführt hat. Damit nicht genug, die Anwendung sollte dem betroffenen Anwender auch weitergehende Informationen bereitstellen. Wäre es nicht sinnvoll, im Beispiel oben Anwender B mitzuteilen, dass Anwender A den Produktnamen verändert hat? Vielleicht haben beide Änderungen ihre Berechtigung. Anwender B könnte dann einen erneuten Aktualisierungsversuch starten, der sowohl den neuen Produktnamen als auch den geänderten Einzelpreis berücksichtigt. Eine solche Lösung wäre optimal.
Konflikte können in einer Umgebung auftreten, in der Änderungen verbindungslos vorgenommen werden, also auch unter ADO.NET. Wichtig ist nur, dass uns adäquate Möglichkeiten in die Hand gegeben werden, mit Konflikten umzugehen. Mit anderen Worten: Die Datenzugriffstechnologie muss uns gestatten, das grundsätzliche Auftreten von Konflikten beeinflussen zu können, aufgetretene Konflikte zu analysieren und natürlich auch zu lösen. ADO.NET kann das. Daher wird ein großer Teil dieses Kapitels sich mit Konflikten beschäftigen müssen.
Wenn wir über die Aktualisierung mit ADO.NET reden, gilt es zwei elementare Szenarien zu unterscheiden:
- das automatische Aktualisieren unter Zuhilfenahme der Klasse CommandBuilder
- das manuelle gesteuerte Aktualisieren
Fangen wir zunächst mit der einfacheren, automatischen Aktualisierung an.
29.1 Aktualisieren mit dem »CommandBuilder«-Objekt 

Sie können eine DataTable mit Daten aus jeder Datenquelle füllen. Handelt es sich 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 das folgende Beispiel dazu 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 29\CommandBuilderDemo
// -------------------------------------------------------
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 die Ursache des Laufzeitfehlers, der nun 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 gaben wir dem SqlDataAdapter das SqlCommand-Objekt über den Konstruktoraufruf bekannt. Der SqlDataAdapter speichert das in seiner Eigenschaft SelectCommand.
Der SqlDataAdapter hat aber noch drei weitere Eigenschaften, die ein SqlCommand-Objekt erfordern:
- 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 automatisch nach festgeschriebenen Regeln Aktualisierungsstatements bereit, obwohl er das durchaus könnte. Dieses im ersten Moment als Mangel erscheinende Verhalten gibt uns jedoch die Möglichkeit, selbst Einfluss auf die Aktualisierung auszuüben. Darauf werde ich später noch genau eingehen. Die Folge ist jedenfalls, dass die Eigenschaften InsertCommand, UpdateCommand und DeleteCommand den Inhalt null haben. Ergo benötigen wir noch passende Aktualisierungsstatements.
Um die notwendige Aktualisierungslogik automatisch zu erzeugen bietet uns ADO.NET die Klasse SqlCommandBuilder an. Ü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 deren Grundlage erzeugt SqlCommandBuilder die SQL-Befehle INSERT, DELETE und UPDATE, verpackt sie in eine Zeichenfolge und weist sie einem jeweils eigenen SqlCommand-Objekt zu. Die drei SqlCommand-Objekte werden den Eigenschaften UpdateCommand, InsertCommand und DeleteCommand des SqlDataAdapters zugewiesen. 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 zurück zu dem eingangs gezeigten Beispiel. 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);
29.1.1 Simulation eines Parallelitätskonflikts 

Das eben gezeigte Beispielprogramm wollen wir auch sofort dazu benutzen, einen sogenannten Parallelitätskonflikt zu simulieren. Ergänzen Sie dazu den Programmcode vor der Ausführung der Update-Methode wie folgt:
Console.WriteLine("Aktualisierung eines zweiten Users simulieren.");
Console.ReadLine();
int count = da.Update(ds);
...Starten Sie anschließend die Anwendung aus dem Visual Studio heraus. Wenn an der Konsole Aktualisierung eines zweiten Users simulieren angezeigt wird, können Sie einen fiktiven zweiten Benutzer spielen. Öffnen Sie die Tabelle Products im SQL Server Management Studio, und ändern Sie beispielsweise den Bezeichner des Artikels Tofu. Das ist die Datenzeile, deren Lagerbestand die Anwendung ändern soll. Nach dem Speichern der Änderung im Management Studio setzen Sie die Ausführung der Konsolenanwendung fort. Es wird eine Exception vom Typ DBConcurrencyException ausgelöst. Der Konfliktfall ist eingetreten.
29.1.2 Die von »SqlCommandBuilder« generierten Aktualisierungsstatements 

Ein SqlCommandBuilder erzeugt Aktualisierungscode und bedient sich dabei des SELECT-Statements. Doch wie sieht die Aktualisierungslogik exakt aus?
Wir wollen uns das nun noch kurz ansehen. Stellen wir uns vor, die Auswahlabfrage sei wie folgt definiert:
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 eines der drei Statements 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 können erkennen, dass hinter der WHERE-Klausel alle Spalten der SELECT-Abfrage als Suchkriterium nach dem zu editierenden Datensatz aufgeführt sind. Der SqlCommandBuilder wertet demnach alle Spalten der Auswahlabfrage aus und verwendet sie zur Bildung des SqlCommand-Objekts.
Die Parameter @p3 bis @p6 werden von der Update-Methode des DataAdapters mit den Daten gefüllt, die unter DataRowVersion.Original aus dem DataSet bezogen werden, @p1 bis @p3 erhalten die Daten aus DataRowVersion.Current. In gleicher Weise werden auch die INSERT- und DELETE-Anweisungen vom SqlCommandBuilder generiert.
29.1.3 Weitere Aktualisierungsoptionen des SqlCommandBuilders 

Die Eigenschaft »ConflictOption«
Standardmäßig verwendet der SqlCommandBuilder in der WHERE-Klausel für UpdateCommand und DeleteCommand alle zu vergleichenden Spalten. Eine Änderung der Daten in der Datenbank führt zu einer 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 SqlCommanBuilder 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 29.1 entnehmen können.
| 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, wie Sie gesehen haben. In diesem Szenario wird immer die erste Änderung in einer Datenzeile zum Erfolg, die dann folgende Änderung zu einem Konflikt führen. Dieses Szenario wird daher auch als First-in-wins bezeichnet.
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. Dieses Szenario wird als Last-in-wins bezeichnet.
Ein Timestamp ist ein automatisch generierter, eindeutiger 8-Byte-Wert. Mithilfe der Timestamp-Spalte einer Zeile können Sie auf einfache Weise ermitteln, ob ein Wert in der Zeile geändert wurde, seit er zuletzt gelesen wurde. Falls die Zeile geändert wurde, wird der Timestamp-Wert aktualisiert. Falls die Zeile nicht geändert wurde, ist der Timestamp-Wert unverändert, seitdem die Zeile zuletzt gelesen wurde. Mit ConflictOption.CompareRowVersion weisen Sie den SqlCommandBuilder an, in der WHERE-Klausel nur die Primärschlüsselspalte(n) und die Timestamp-Spalte aufzunehmen.
Die Eigenschaft »SetAllValues«
Betrachten Sie noch einmal das Beispiel mit Benutzer A und Benutzer B am Anfang des Kapitels. Es wäre doch denkbar, dass weder das Last-in-wins- noch das First-in-wins-Szenario die Forderung passend erfüllt. Vielleicht können zwei oder mehr Änderungen an einer Datenzeile akzeptiert werden, vorausgesetzt, sie betreffen nicht die gleiche Spalte. 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 gleichen Voraussetzungen die Spalte UnitPrice, wird diese seinem UPDATE-Statement hinzugefügt:
UPDATE Products SET UnitPrice = 12.98 WHERE ProductID = 55 AND UnitPrice = 13
Die Aktualisierung wird erfolgreich sein. Mehr noch, die betroffene Datenzeile in der Datenbank wird beide Änderungen aufweisen.
Setzen Sie die Eigenschaft SetAllValues des SqlCommandBuilders auf false, werden neben der Primärschlüsselspalte nur die Spalten der WHERE-Klausel als Suchkriterium hinzugefügt, deren Inhalte sich verändert haben. Das entspricht genau dem gezeigten Muster.
29.1.4 Die Vor- und Nachteile des SqlCommandBuilders 

Der SqlCommandBuilder ist sehr einfach zu handhaben und benötigt nur wenig Programmcode. Der Effizienz bei der Programmierung steht aber ein Performanceverlust gegenüber. Wollen Sie leistungsmäßig das Letzte aus Ihrer Anwendung herauskitzeln, sollten Sie das Aktualisierungsverhalten nicht vom SqlCommandBuilder abhängig machen und, wie gleich gezeigt wird, die Aktualisierungslogik manuell programmieren. Über das Standardverhalten hinaus bietet der CommandBuilder eine ganze Reihe von Möglichkeiten, eine angepasste Konfliktsteuerung zu betrieben. Manchmal wird diese aber nicht ausreichen. Dann heißt es wieder, alles manuell zu codieren.





Jetzt bestellen





