27.5 DataSet mit der Datenbank synchronisieren
Bisher habe ich Ihnen gezeigt, wie Sie eine im DataSet geänderte Datenzeile in der Originaldatenbank aktualisieren können. Es gibt aber auch Situationen, in denen nach einer Aktualisierung von der Datenbank für die betroffene Datenzeile neue Werte abgefragt werden müssen. Wenn wir beispielsweise der Tabelle Products einen weiteren Artikel hinzufügen, interessiert uns, welchen Primärschlüssel die Datenbank für den neuen Artikel vergeben hat, da es sich um einen Autoinkrementwert handelt. Vielleicht hat Ihre Tabelle auch eine Timestamp-Spalte. Nach einer Änderung generiert die Datenbank einen neuen Wert, den Sie in Ihrer Datenzeile eintragen müssen.
Eine typische weitere Situation ergibt sich daraus, dass das UPDATE-Kommando neue Werte für einzelne Spalten – und nur für diese – spezifiziert. Der Rest der Zeile bleibt unberührt. Zum Beispiel ändert der Befehl
UPDATE Products SET ProductName = @Artikel WHERE ProductID = @ID
nur den Produktnamen. Auch wenn diese Aktualisierung erfolgreich ist, kann ein anderer Anwender die Spalte UnitPrice zwischenzeitlich editiert haben. Daher gibt die betreffende Datenzeile in Ihrem DataSet dann nicht den tatsächlichen Stand wieder.
Sie sehen, man kann sich eine Reihe von Situationen vorstellen, die es erforderlich machen, den Inhalt einer Datenzeile zu aktualisieren, nachdem eine Änderung übermittelt worden ist.
27.5.1 UpdatedRowSource in DbCommand
Update aktualisiert nicht nur die Datenbank, sondern prüft anschließend auch die Eigenschaft UpdatedRowSource des Command-Objekts, um festzustellen, wie nach einer erfolgreichen Aktualisierung eventuell von Command zurückgelieferte Werte auf die DataRow angewendet werden sollen.
Doch welche Werte werden zurückgeliefert? Wie Sie wissen, gehört der SQL Server zu der Gruppe von Datenbankservern, die mit Batch-Abfragen umgehen können. Als ich Ihnen die Klasse DataSet vorgestellt habe, setzten wir Batch-Abfragen ein, um das DataSet-Objekt in einem Schritt mit mehreren Tabellen zu füllen.
SELECT * FROM Products; SELECT * FROM Categories
Mit Batch-Abfragen können Sie nach der Aktualisierung zum Beispiel neue Daten abrufen:
UPDATE Products SET ProductName=@Name WHERE ProductID=@ID AND ProductName=@OrgName; SELECT ProductID, ProductName, Unitprice, Discontinued FROM Products WHERE ProductID = @ID;
Die Eigenschaft UpdatedRowSource des Command-Objekts legt fest, wie oder ob die vom Kommando zurückgelieferten Werte auf die DataRow angewendet werden sollen. Die Eigenschaft ist vom Typ der gleichnamigen Enumeration, die in Tabelle 27.4 beschrieben wird.
Konstante | Beschreibung |
Both |
DbCommand ruft neue Daten für die Zeile ab und verwendet dafür den ersten zurückgelieferten Datensatz und Ausgabeparameter. Dieses ist auch die Standardeinstellung. |
FirstReturnedRecord |
DbCommand liest neue Daten für die Zeile und verwendet dafür den ersten zurückgegebenen Datensatz. |
None |
DbCommand ruft keine neuen Daten für die Zeile ab. |
OutputParameters |
DbCommand liest neue Daten für die Zeile mit Ausgabeparametern. |
Manuelle Synchronisation mit ExecuteReader
Sie können eine Batch-Abfrage selbst ausführen, indem Sie die Methode ExecuteReader des Command-Objekts aufrufen. Danach können Sie mit RecordsAffected des DataReaders ermitteln, wie viele Datenzeilen von der UPDATE-Abfrage betroffen sind. Handelt es sich um eine Datenzeile, rufen Sie die Read-Methode des Command-Objekts auf und können die neu ermittelten Werte in die betroffene DataRow eintragen. Zum Schluss rufen Sie AcceptChanges auf der DataRow auf, um die Version auf DataRowVersion.Original zu setzen.
'...\ADO\Aktualisierung\SynchronisationMitReader.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module SynchronisationMitReader Sub Test() Dim con As DbConnection = New SqlConnection() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "SELECT ProductID, ProductName, " & _ "UnitPrice FROM Products" cmd.Connection = con Dim ds As New DataSet() Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd da.Fill(ds) ' Datenzeilen editieren Dim row As DataRow = ds.Tables(0).Rows(0) row("ProductName") = "Früchtetee" ' externe Änderung simulieren cmd.CommandText = _ "UPDATE Products SET UnitPrice=16.00 WHERE ProductID=1" con.Open() : cmd.ExecuteNonQuery() : con.Close() ' Änderungen durchführen Dim orig As DataRowVersion = DataRowVersion.Original ds.Tables(0).Select("ProductID=1")(0)("ProductName") = "Kräutertee" Dim update As DbCommand = CreateUpdateCommand(con) update.Parameters("@Name").Value = row("ProductName") update.Parameters("@ID").Value = row("ProductID") con.Open() Dim rd As DbDataReader = update.ExecuteReader() If rd.RecordsAffected = 1 Then While rd.Read() row("ProductName") = rd("ProductName") row("UnitPrice") = rd("UnitPrice") End While End If con.Close() Console.WriteLine("""Orig"" : {0,-20}{1}", row(1, orig), row(2, orig)) Console.WriteLine("Aktuell: {0,-20}{1}", row(1), row(2)) row.AcceptChanges() Console.WriteLine("""Orig"" : {0,-20}{1}", row(1, orig), row(2, orig)) Console.ReadLine() End Sub Function CreateUpdateCommand(ByVal con As DbConnection) As DbCommand Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "UPDATE Products SET ProductName=@Name " & _ "WHERE ProductID=@ID" & ";" & _ "SELECT ProductID, ProductName, Unitprice " & _ "FROM Products WHERE ProductID = @ID" cmd.Connection = con ' die Parameter der Parameters-Auflistung hinzufügen Dim col As DbParameterCollection = cmd.Parameters col.Add(New SqlParameter("@Name", SqlDbType.VarChar, 40, "ProductName")) col.Add(New SqlParameter("@ID", SqlDbType.Int, 4, "ProductID")) Return cmd End Function End Module End Namespace
In der ersten Datenzeile der Products-Tabelle wird der Produktname lokal geändert. Danach wird die Spalte UnitPrice des ersten Datensatzes ohne Mitwirkung des DataAdapters geändert. Diese Spalte ist zwar vom DataAdapter bezogen worden, hat sich aber im Code der Anwendung nicht verändert. In der Konsolenausgabe sehen Sie, dass sowohl der neue Produktname als auch der neue Einzelpreis der Datenzeile angezeigt wird.
"Orig" : Chai 18.0000 Aktuell: Kräutertee 16.0000 "Orig" : Kräutertee 16.0000
Automatische Synchronisation mit DataAdapter
Das Beispiel lässt sich deutlich vereinfachen, wenn Sie es dem DataAdapter überlassen, die Batch-Abfrage auszuführen. Da die Einstellung des Commands UpdatedRowSource.Both lautet, rufen Sie wie gehabt die Methode Update auf. Den Rest erledigt der DataAdapter.
'...\ADO\Aktualisierung\SynchronisationMitAdapter.vb |
... Dim update As DbCommand = CreateUpdateCommand(con) da.UpdateCommand = update da.Update(ds) Console.WriteLine("""Orig"" : {0,-20}{1}", row(1, orig), row(2, orig)) ...
Neue Autoinkrementwerte abrufen
Wenn Sie Ihrer DataTable, die auf einem autoinkrementellen Primärschlüsselwert basiert, eine neue Datenzeile hinzufügen, bekommt diese neue lokale Datenzeile einen temporären Primärschlüssel. Dieser dient nur dazu, die neue Datenzeile in der DataTable eindeutig identifizieren zu können. Aktualisieren Sie später die Datenbank mit einem INSERT INTO-Befehl, vergibt die Datenbank automatisch einen gänzlich neuen Primärschlüssel.
Nach der Aktualisierung sollten Sie diesen neuen endgültigen Primärschlüssel von der Datenbank beziehen. Tun Sie dies nicht, haben Sie Schwierigkeiten, die Datenzeile in der Datenbank eindeutig zu identifizieren, da Ihnen der neu vergebene Primärschlüssel nicht bekannt ist. Spätestens beim nächsten UPDATE-Kommando ist dies sehr ärgerlich.
Die Funktion SCOPE_IDENTITY() des SQL Servers gibt den letzten in der Sitzung automatisch generierten Identitätswert zurück, der in eine Spalte einer beliebigen Tabelle eingefügt wurde. Alternativ können Sie @@IDENTITY verwenden. Wir erhalten den Identitätswert, indem wir nach der Aktualisierungszeichenfolge des Commands die Funktion aufrufen.
INSERT INTO Products (ProductName, UnitPrice, Discontinued) VALUES(@Name, @Preis, @Conti); SELECT @ID = SCOPE_IDENTITY()
Im folgenden Beispielprogramm wird der Products-Tabelle eine Datenzeile hinzugefügt und anschließend an der Konsole angezeigt. Beachten Sie bitte, dass in der Methode CreateInsertCommand der Parameter @ID als Ausgabeparameter spezifiziert ist.
'...\ADO\Aktualisierung\AutoInkrement.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module AutoInkrement Sub UpdatedRowSource() Dim con As DbConnection = New SqlConnection() 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) ' Datenzeile hinzufügen Dim neu As DataRow = ds.Tables(0).NewRow() neu("ProductName") = "Camembert" neu("UnitPrice") = 11.3 neu("Discontinued") = 0 ds.Tables(0).Rows.Add(neu) ' Datenbank aktualisieren Dim insert As DbCommand = CreateInsertCommand(con) insert.UpdatedRowSource = UpdateRowSource.OutputParameters da.InsertCommand = insert da.Update(ds) Console.WriteLine("Neuer Primärschlüssel: {0}", neu("ProductID")) Console.ReadLine() End Sub Function CreateInsertCommand(ByVal con As DbConnection) As DbCommand Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "INSERT INTO Products " & _ "(ProductName,UnitPrice,Discontinued) " & _ "Values(@Name,@Preis,@Conti)" & ";" & _ "SELECT @ID = SCOPE_IDENTITY()" cmd.Connection = con ' die Parameter der Parameters-Auflistung hinzufügen Dim col As DbParameterCollection = cmd.Parameters col.Add(New SqlParameter("@Name", SqlDbType.VarChar, 40, "ProductName")) col.Add(New SqlParameter("@Preis", SqlDbType.Money, 8, "UnitPrice")) col.Add(New SqlParameter("@Conti", SqlDbType.Bit, 1, "Discontinued")) Dim param As DbParameter = _ New SqlParameter("@ID", SqlDbType.Int, 0, "ProductID") cmd.Parameters.Add(param) param.Direction = ParameterDirection.Output Return cmd End Function ... End Module End Namespace
Bei jedem Programmlauf bekommen Sie einen neuen Wert:
Neuer Primärschlüssel: 86
Alternativ zu diesem Beispiel können Sie auch das Ereignis RowUpdated des DataAdapters dazu benutzen, den neuen Primärschlüssel abzurufen. Überprüfen Sie im Ereignishandler zuerst, in welchem Zustand sich die Aktualisierung befindet und ob zuvor ein INSERT abgesetzt worden ist. Dann erzeugen Sie ein passendes Command-Objekt und rufen dessen Methode ExecuteScalar auf, um den neuen Primärschlüssel abzufragen.
Im vorhergehenden Beispiel wurde der betreffenden Datenzeile im DataSet der neue Primärschlüssel zugewiesen, weil wir das Command-Objekt als Batch-Abfrage konstruiert hatten. Rufen wir im Ereignishandler von RowUpdated den Schlüsselwert ab, liegt keine Batch-Abfrage vor, und wir müssen den Schlüsselwert, der von der Spalte ProductID bezogen wurde, unserer Datenzeile im DataSet zuweisen.
'...\ADO\Aktualisierung\AutoInkrement.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module AutoInkrement ... Private con As DbConnection = New SqlConnection() Sub Ereignis() ... Sub Änderung(ByVal sender As Object, ByVal ev As RowUpdatedEventArgs) If ev.Status = UpdateStatus.Continue AndAlso _ ev.StatementType = StatementType.Insert Then Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "SELECT @@Identity" : cmd.Connection = con ev.Row("ProductID") = cmd.ExecuteScalar() End If End Sub End Module End Namespace
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.