27.6 Hierarchische Änderungen an die Datenbank übermitteln
Nicht immer haben Sie nur eine DataTable in Ihrem DataSet, die Sie aktualisieren müssen. Oft wird Ihr DataSet eine interne hierarchische Struktur mit Tabellen haben, die miteinander in Beziehung stehen. Die mit der Änderung solcher Strukturen zusammenhängenden Aspekte beschreibe ich in diesem Abschnitt.
Im Folgenden werden wir exemplarisch die beiden Tabellen Order Details und Orders der Northwind-Datenbank betrachten. Orders ist die Mastertabelle und Order Details die Detailtabelle (siehe Abbildung 27.2).
Abbildung 27.2 Beziehung der Tabellen »Orders« und »Order Details«
Nehmen wir an, der Anwender hat eine Reihe von Änderungen an den Daten vorgenommen und möchte diese nun der Datenbank übermitteln. Die referenziellen Integritätseinschränkungen erzwingen eine bestimmte Reihenfolge bei der Datenübermittlung. Wir müssen zuerst die Daten der neuen Datensätze in der Tabelle Orders in die Datenbank schreiben und können erst danach die entsprechenden neuen Datenzeilen in der Tabelle Order Details übermitteln. Zum Löschen einer Bestellung muss man genau den umgekehrten Weg gehen: Zuerst müssen alle Bestelldetails einer konkreten Bestellung in Order Details gelöscht werden, ehe die Bestellung in Orders gelöscht werden kann. Damit können im Allgemeinen anstehende Aktualisierungen nicht in einem Rutsch durchgeführt werden, sondern die Reihenfolge muss explizit kontrolliert werden.
Das folgende Codefragment zeigt die Grobstruktur des Vorgehens, die Details folgen weiter unten. Zuerst werden die beiden Tabellen ausgelesen und mit DataRelation in Beziehung zueinander gesetzt. Danach wird das DataSet geändert und die neuen Bestellungen zur späteren Löschung in Reset() gespeichert. Um die referenzielle Integrität zu wahren, werden erst die Bestellungen und dann die Details aktualisiert. Für den Abgleich der Bestellungen wird ein Ereignishandler registriert, der sich um die Autoinkrementspalte OrderID kümmert. Schließlich werden die Bestellungen protokolliert und wird die Datenbank restauriert.
'...\ADO\Aktualisierung\Beziehungen.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Beziehungen ... Private con As DbConnection = _ New SqlConnection("Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi") Private ds As New DataSet() Private Bestellungen, Details As DataTable Sub Test() ' Datenbank auslesen Dim daDetails As DbDataAdapter = Lesen("[Order Details]") Dim daOrders As DbDataAdapter = Lesen("Orders") Bestellungen = ds.Tables("Orders") Details = ds.Tables("[Order Details]") ' Datenrelation erzeugen Dim rel As New DataRelation("Bez", _ Bestellungen.Columns("OrderID"), Details.Columns("OrderID")) ds.Relations.Add(rel) ' neue Bestellung mit 2 Posten ProductID,UnitPrice,Quantity,Discount LokaleÄnderungen(ds, New Object()() _ {New Object() {1, 12, 3, 0}, New Object() {2, 8.89, 3, 0}}) Dim Neu() As DataRow = _ Bestellungen.Select("", "", DataViewRowState.Added) ' Datenbank aktualisieren daOrders.InsertCommand = InsertCommand() AddHandler CType(daOrders, SqlDataAdapter).RowUpdated, _ AddressOf Änderung daOrders.Update(Neu) ' Zeilen daDetails.InsertCommand = DetailsInsertCommand() daDetails.Update(Details.GetChanges(DataRowState.Added)) ' Tabelle For Each row As DataRow In Neu Console.WriteLine("Bestellung mit Nummer {0}", row("OrderID")) Next Reset(Neu) Console.ReadLine() End Sub End Module End Namespace
Jeder Aufruf des Programms zeigt eine neue Bestellnummer:
Bestellung mit Nummer 11093
Zur Selektion der geänderten Zeilen verwendet das Beispiel der Vollständigkeit halber die Methoden Select und GetChanges von DataTable. Die Methode GetChanges ist auch für DataSet definiert und speichert die Änderungen in einem DataSet (statt in einer DataTable). Um alle Arten von Änderungen zu sammeln, wird sie parameterlos aufgerufen.
27.6.1 Datenbank auslesen
Da die Aktualisierung der Datenbank nach Tabellen getrennt erfolgt, ist jede mit ihrem eigenen DataAdapter verbunden. Das Auslesen erfolgt analog und ist in einer Methode zusammengefasst. Sie liest nur die Metadaten mit FillSchema, da die vorhandenen Daten im Beispiel nicht gebraucht werden. Die Verknüpfung der Tabellen war bereits in Test() weiter oben zu sehen.
'...\ADO\Aktualisierung\Beziehungen.vb |
Function Lesen(ByVal tabelle As String) As DbDataAdapter Dim cmd As DbCommand = New SqlCommand() cmd.Connection = con : cmd.CommandText = "SELECT * FROM " & tabelle Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd da.FillSchema(ds, SchemaType.Source, tabelle) Return da End Function
27.6.2 Änderung
Das Beispiel fügt eine Bestellung mit den zugehörigen Details ein. Zuerst wird eine neue Bestellung mit beliebiger OrderID angelegt, die dann in den Details referenziert wird. Der Wert von OrderID spielt keine Rolle, da die Spalte automatisch von der Datenbank belegt wird (Autoinkrement). Exemplarisch für weitere Daten wird das Datum gesetzt. Um die Funktion flexibel zu halten, werden die Werte der Details als zweiter Parameter übergeben. Der Aufruf erfolgt in der oben gezeigten Methode Test().
'...\ADO\Aktualisierung\Beziehungen.vb |
Sub LokaleÄnderungen(ByVal ds As DataSet, ByVal vals()() As Object) Dim Auftrag As DataRow = Bestellungen.NewRow() Auftrag("OrderID") = –100 'wird in Datenbank neu vergeben Auftrag("OrderDate") = DateTime.Today Bestellungen.Rows.Add(Auftrag) ' neue [Order Details](OrderID,ProductID,UnitPrice,Quantity,Discount) Dim Posten As DataRow For Each row As Object() In vals Posten = Details.NewRow() Posten("OrderID") = Auftrag("OrderID") For i As Integer = 0 To row.Length – 1 Posten(Details.Columns(i + 1)) = row(i) Next Details.Rows.Add(Posten) Next End Sub
27.6.3 Bestellung einfügen
Das Hinzufügen einer Bestellung besteht aus zwei Teilen:
- Aktualisierung der Datenbank mit dem Kommando InsertCommand()
- Bezug der von der Datenbank vergebenen OrderID im Ereignishandler Änderung()
Die Aktualisierung berücksichtigt exemplarisch das Bestelldatum:
'...\ADO\Aktualisierung\Beziehungen.vb |
Function InsertCommand() As DbCommand Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "INSERT INTO Orders (OrderDate) Values(@Date)" cmd.Connection = con Dim col As DbParameterCollection = cmd.Parameters col.Add(New SqlParameter("@Date", SqlDbType.DateTime, 8, "OrderDate")) Return cmd End Function
Nach der Änderung der Datenbank liegt ein neuer Wert für OrderID vor, der in @@Identity gespeichert ist und den der Ereignishandler mit ExecuteScalar() abfragt. Die mit FillSchema() ermittelte Metainformation kennzeichnet die Primärschlüsselspalte OrderID als schreibgeschützt. In unserem Fall müssen wir diesen Schutz umgehen, um die lokalen Daten im DataSet mit den neu vergebenen Werten in der Datenbank abzugleichen. Durch die weiter oben definierte DataRelation() erhalten in der Tabelle Order Details alle korrespondierenden Werte automatisch dieselbe OrderID.
'...\ADO\Aktualisierung\Beziehungen.vb |
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 Bestellungen.Columns("OrderID").ReadOnly = False ev.Row("OrderID") = cmd.ExecuteScalar() 'Verbindung bereits offen Bestellungen.Columns("OrderID").ReadOnly = True '[Order Details](OrderID) automatisch durch Fremdschlüsselbeziehung End If End Sub
27.6.4 Bestelldetails einfügen
Die Synchronisation der Bestelldetails, die nach der korrespondierenden Bestellung erfolgen muss, weist keine Besonderheiten auf.
'...\ADO\Aktualisierung\Beziehungen.vb |
Function DetailsInsertCommand() As DbCommand Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "INSERT INTO [Order Details] " & _ "(OrderID,ProductID,UnitPrice,Quantity,Discount) " & _ "Values(@OID,@PID,@Preis,@Quant,@Red)" cmd.Connection = con ' die Parameter der Parameters-Auflistung hinzufügen Dim col As DbParameterCollection = cmd.Parameters col.Add(New SqlParameter("@OID", SqlDbType.Int, 4, "OrderID")) col.Add(New SqlParameter("@PID", SqlDbType.Int, 4, "ProductID")) col.Add(New SqlParameter("@Preis", SqlDbType.Money, 8, "UnitPrice")) col.Add(New SqlParameter("@Quant", SqlDbType.SmallInt, 2, "Quantity")) col.Add(New SqlParameter("@Red", SqlDbType.Real, 4, "Discount")) Return cmd End Function
27.6.5 Wiederherstellen der Datenbank
Schließlich müssen wir noch die eingefügten Zeilen wieder aus der Datenbank entfernen. Um die referenzielle Integrität nicht zu verletzen, müssen die Details vor den zugehörigen Bestellungen gelöscht werden. Im umgekehrten Fall würden sich sonst einige Details auf eine nicht mehr vorhandene Bestellung beziehen.
'...\ADO\Aktualisierung\Beziehungen.vb |
Sub Reset(ByVal Neu As IEnumerable(Of DataRow)) Dim con As DbConnection = New SqlConnection() con.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim cmd As DbCommand = New SqlCommand() cmd.Connection = con con.Open() For Each row As DataRow In Neu Dim i As Integer = CType(row("OrderID"), Integer) cmd.CommandText = "DELETE [Order Details] WHERE OrderID=" & i cmd.ExecuteNonQuery() cmd.CommandText = "DELETE Orders WHERE OrderID=" & i cmd.ExecuteNonQuery() Next con.Close() End Sub
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.