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.