26.5 Mit mehreren Tabellen arbeiten
26.5.1 JOIN-Abfragen
Bisher haben wir immer nur eine Tabelle im DataSet betrachtet. In der Praxis sind oft mehrere Tabellen gleichzeitig im Spiel. Um beispielsweise die Frage zu beantworten, welche Artikel von den einzelnen Lieferanten stammen, sind zwei Tabellen notwendig: Products und Suppliers. Die meisten Tabellen einer Datenbank stehen mit anderen Tabellen in Beziehung. Meistens handelt es sich dabei um 1:n-Beziehungen. Beispielsweise stammen von einem Lieferanten mehrere Artikel. Allerdings berücksichtigt die Northwind-Datenbank nicht, dass ein bestimmtes Produkt durchaus auch von mehreren Lieferanten angeboten werden könnte. Dann müsste die Beziehung zwischen den beiden Tabellen durch eine m:n-Beziehung beschrieben werden, die normalerweise in drei Tabellen aufgelöst wird, die miteinander jeweils in einer 1:n-Beziehung stehen. Diese Aufteilung ergibt sich aus der angestrebten Normalisierung der Datentabellen, die eine »robuste« Datenbank ergeben. Das Prinzip der Normalisierung wurde von Boyce und Codd in den 1970er-Jahren eingeführt.
Wenden wir uns für die weiteren Ausführungen nun zwei Tabellen zu, deren Beziehung Sie in Abbildung 26.2 sehen.
Um Daten aus mehreren Tabellen auszuwerten, werden JOIN-Abfragen benutzt, zum Beispiel um festzustellen, welche Produkte von den einzelnen Lieferanten angeboten werden:
SELECT Suppliers.CompanyName, Suppliers.ContactName, Products.ProductName FROM Suppliers INNER JOIN Products ON Suppliers.SupplierID = Products.SupplierID
Das Ergebnis der Abfrage kombiniert Daten aus beiden Tabellen:
Exotic Liquids Charlotte Cooper Chai Exotic Liquids Charlotte Cooper Chang Exotic Liquids Charlotte Cooper Aniseed Syrup New Orleans Cajun Delights Shelley Burke Chef Anton's Cajun Seasoning New Orleans Cajun Delights Shelley Burke Chef Anton's Gumbo Mix ...
Abbildung 26.2 Beziehung zwischen »Products« und »Suppliers«
JOIN-Abfragen haben einige Vorteile:
- Das Ergebnis lässt sich filtern.
- Das Resultat steht in einer überschaubaren Ergebnismenge.
- JOIN-Abfragen sind anerkannter Standard.
Bei kritischer Betrachtung stehen den Vorteilen aber auch gewichtige Nachteile gegenüber:
- Die Daten einer JOIN-Abfrage sind schwierig zu aktualisieren. Insbesondere beim Löschen oder Hinzufügen einer Datenzeile in einer Join-Abfrage wird die Problematik deutlich. Angenommen, Sie löschen eine Datenzeile, steht sofort die Frage im Raum, ob nur die Datenzeile in der Detailtabelle, also auf der n-Seite einer Beziehung gelöscht werden soll oder gleichzeitig auch die Datenzeile in der übergeordneten Mastertabelle, also der 1-Seite.
- JOIN-Abfragen geben redundante Daten zurück. Lassen Sie sich beispielsweise die Artikelliste ausgeben und zu jedem Artikel auch noch die notwendigen Informationen des entsprechenden Lieferanten, werden die Lieferanteninformationen mehrfach zurückgeliefert (siehe das letzte Abfrageergebnis).
- Änderungen in einer JOIN-Abfrage sind schwer zu synchronisieren. Firmiert sich einer der Lieferanten um und tragen Sie das im Abfrageergebnis ein, muss die Änderung sofort zur Datenbank übermittelt und die gesamte Abfrage erneut ausgeführt werden.
26.5.2 Mehrere Tabellen in einem DataSet
ADO.NET löst die Nachteile, die eine JOIN-Abfrage hat, auf eigene Art und Weise. Dazu wird die JOIN-Abfrage in Einzeltabellen aufgeteilt, die miteinander in Beziehung gesetzt werden. Mit anderen Worten: Es wird ein Teil der Originaldatenbank abgebildet. Die Beziehung zwischen zwei Tabellen wird durch ein Objekt vom Typ DataRelation beschrieben.
Solche strukturierten DataSets sind schwer zu filtern, aber sie haben entscheidende Vorteile. So werden weniger Daten zurückgegeben als bei einer JOIN-Abfrage. Damit wird einerseits sowohl die Netzbelastung als auch die Auslastung des lokalen Speichers so gering wie möglich gehalten. Zudem ist es viel einfacher, Daten zu aktualisieren. Löschen Sie zum Beispiel einen Datensatz aus der Detailtabelle (n-Seite), möchten Sie vermutlich nicht auch gleichzeitig den entsprechenden Datensatz der Mastertabelle löschen (1-Seite). Beide Informationen sind in einer JOIN-Abfrage jedoch in einer Datenzeile zusammengefasst. Operieren Sie mit einer DataRelation zwischen zwei DataTable-Objekten, lässt sich der Datensatz aus der Detailtabelle löschen, ohne dass zwangsläufig auch die entsprechende Datenzeile der Mastertabelle gelöscht wird.
26.5.3 Tabellenbeziehungen erzeugen
Die Verknüpfung von zwei DataTable-Objekten in einer DataRelation erfolgt über DataColumn-Objekte. In der Products/Suppliers-Beziehung ist die Tabelle Suppliers das übergeordnete Element und die Tabelle Products das untergeordnete Element der Beziehung. Dies ist vergleichbar mit einer Primärschlüssel/Fremdschlüssel-Beziehung. Beziehungen werden zwischen einander entsprechenden Spalten in der übergeordneten und der untergeordneten Tabelle erstellt. Das heißt, dass der Datentyp für beide Spalten identisch sein muss.
Ich möchte Ihnen im Folgenden den ersten der DataRelation-Konstruktoren vorstellen (optionale Teile sind in eckige Klammern gesetzt, [()] bedeutet einfaches Objekt oder Feld).
Sub New(relationName As String, _ parentColumns As DataColumn[()], childColumns As DataColumn[()] [,createConstraints As Boolean]) Sub New(relationName As String, _ parentTableName As String [, parentTableNamespace As String], _ childTableName As String [, childTableNamespace As String], _ parentColumnNames As String(), childColumnNames As String(),nested As Boolean) |
Der erste Parameter ist der Name, unter dem die DataRelation angesprochen wird, der zweite erwartet die Referenz auf die übergeordnete Spalte der Mastertabelle (1-Seite) und der dritte die Referenz auf die untergeordnete Spalte der Detailtabelle (n-Seite).
Nachdem eine DataRelation erzeugt worden ist, muss sie der Relations-Auflistung eines DataSets hinzugefügt werden. Die Auflistung hat den Typ DataRelationCollection.
Das folgende Beispiel zeigt, wie die durch ein Programm erzeugte Beziehung zwischen den beiden Tabellen Suppliers und Products festgelegt wird (der vollständige Code folgt später).
Dim conn As DbConnection = New SqlConnection() conn.ConnectionString = "..." Dim cmd As DbCommand = New SqlCommand() cmd.Connection = conn cmd.CommandText = "SELECT * FROM Suppliers;SELECT * FROM Products" Dim ds As DataSet = New DataSet() Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd da.TableMappings.Add("Table", "Händler") da.TableMappings.Add("Table1", "Produkte") da.Fill(ds) ' Beziehung zwischen den beiden Tabellen Dim master As DataColumn = ds.Tables("Händler").Columns("SupplierID") Dim detail As DataColumn = ds.Tables("Produkte").Columns("SupplierID") Dim rel As DataRelation = New DataRelation("HändlerProdukte",master,detail) ds.Relations.Add(rel)
26.5.4 DataRelations und Einschränkungen
Wenn Sie eine DataRelation zwischen zwei Tabellen erzeugen, wie zuvor gezeigt, wird ein UniqueConstraint auf der Mastertabelle und ein ForeignKeyConstraint auf der Detailtabelle erstellt.
Sind vor dem Erstellen der DataRelation Einschränkungen definiert, die einer Unique- und einer Fremdschlüsseleinschränkung entsprechen, übernimmt die neue DataRelation die vorhandenen Einschränkungen und erzeugt implizit keine neuen.
Sie können das implizite Erzeugen der Einschränkungen mit dem vierten Konstruktorparameter von DataRelation durch Angabe von False unterdrücken, zum Beispiel:
Dim rel As DataRelation = _ New DataRelation("HändlerProdukte", master, detail, False)
ForeignKeyConstraint im Detail
Das ForeignKeyConstraint-Objekt gehört zur ConstraintCollection der Detailtabelle, also der Tabelle auf der n-Seite einer 1:n-Beziehung. Es weist nicht nur eine Reihe von Eigenschaften auf, um die Beziehung zwischen den beiden Tabellen zu untersuchen, sondern legt auch fest, wie sich die beiden Tabellen verhalten, wenn in der übergeordneten Mastertabelle Daten geändert oder Datenzeilen gelöscht werden.
Das folgende Codefragment zeigt, wie Sie die Eigenschaften Table, RelatedTable, Columns und RelatedColumns auswerten können. Die Auswertung basiert auf der Beziehung, die weiter oben zwischen den beiden Tabellen Suppliers und Products codiert worden ist (der vollständige Code folgt später).
Dim cons As ConstraintCollection = ds.Tables("Produkte").Constraints For Each con As Constraint in cons If TypeOf con Is ForeignKeyConstraint Then Dim col As DataColumn Console.Write("Untergeordnete Tabelle: ") Console.WriteLine(CType(con, ForeignKeyConstraint).Table) Console.Write("Untergeordnete Spalte(n): ") For Each col In CType(con, ForeignKeyConstraint).Columns Console.Write(col.ColumnName & " ") Next Console.WriteLine() Console.Write("Übergeordnete Tabelle: ") Console.WriteLine(CType(con, ForeignKeyConstraint).RelatedTable) Console.Write("Übergeordnete Spalte(n): ") For Each col In CType(con, ForeignKeyConstraint).RelatedColumns Console.WriteLine(col.ColumnName & " ") Next Console.WriteLine() End If Next
Die in Beziehung stehenden Spalten und die Tabelle auf der Masterseite liefern die Eigenschaften RelatedColumns und RelatedTable der Einschränkung, auf der untergeordneten Seite sind es Columns und Table. Die Spalten werden als DataColumn-Array gespeichert, weil mehrere Spalten ein gemeinsames Merkmal für die Beziehung zwischen zwei Tabellen darstellen können.
Wichtiger als die Auswertung der Eigenschaften einer Beziehung sind die Eigenschaften, über die das Verhalten der Relation festgelegt wird. Wie sollen sich zum Beispiel die verknüpften Datenzeilen in der untergeordneten Detailtabelle verhalten, wenn Sie eine Datenzeile in der Mastertabelle löschen? Sollen sie ebenfalls gelöscht werden, oder sollen sie in der Detailtabelle erhalten bleiben? Wird der Wert in der untergeordneten Tabelle ebenfalls aktualisiert, wenn in der Mastertabelle ein Wert geändert wird?
Die Steuerung dieses Verhaltens wird von den Eigenschaften UpdateRule und DeleteRule bestimmt. Beide Eigenschaften sind vom Typ der Enumeration Rule im Namensraum System.Data, deren Werte in Tabelle 26.7 angegeben sind.
Konstante | Beschreibung |
Cascade |
Jede Löschung (Änderung) in der Mastertabelle wird auch in der Detailtabelle analog durchgeführt. Dies ist die Standardeinstellung. |
None |
Statt einer Aktion wird eine Ausnahme ausgelöst. |
SetDefault |
Die Werte in den verknüpften Datenzeilen der Detailtabelle werden auf die Standardwerte eingestellt, die in der Eigenschaft DefaultValue des DataColumn-Objekts festgelegt sind. |
SetNull |
Die Werte in den verknüpften Datenzeilen werden auf DBNull gesetzt. |
Wichtig ist auch die Eigenschaft AcceptRejectRule, die steuert, wie mit den verknüpften Datenzeilen umgegangen wird, wenn in der Mastertabelle AcceptChanges oder RejectChanges aufgerufen wird.
Sie kann nur die Werte AcceptRejectRule.None oder AcceptRejectRule.Cascade annehmen. Der Standardwert None bewirkt, dass sich der Aufruf von AcceptChanges oder RejectChanges für eine Masterzeile nicht auf die Datenzeilen der untergeordneten Datenzeilen auswirkt. Wenn Sie die Eigenschaft AcceptRejectRule auf Cascade setzen, wird die Aktion an die untergeordnete Datenzeile weitergegeben, die im ForeignKeyConstraint-Objekt definiert ist.
Das folgende Beispielprogramm Relationen enthält zusammenfassend einige der Features, die im Zusammenhang mit der DataRelation stehen.
'...\ADO\DataSet\Relationen.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Relationen Sub Test() Dim conn As DbConnection = New SqlConnection() conn.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "SELECT * FROM Suppliers;SELECT * FROM Products" cmd.Connection = conn Dim ds As New DataSet() Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd da.TableMappings.Add("Table", "Händler") da.TableMappings.Add("Table1", "Produkte") da.Fill(ds) ' Beziehung zwischen den beiden Tabellen Dim master As DataColumn = ds.Tables("Händler").Columns("SupplierID") Dim detail As DataColumn = ds.Tables("Produkte").Columns("SupplierID") Dim rel As DataRelation = _ New DataRelation("HändlerProdukte", master, detail) ds.Relations.Add(rel) For Each con As Constraint In ds.Tables("Produkte").Constraints If TypeOf con Is ForeignKeyConstraint Then Dim col As DataColumn Console.Write("Untergeordnete Tabelle: ") Console.WriteLine(CType(con, ForeignKeyConstraint).Table) Console.Write("Untergeordnete Spalte(n): ") For Each col In CType(con, ForeignKeyConstraint).Columns Console.Write(col.ColumnName & " ") Next Console.WriteLine() Console.Write("Übergeordnete Tabelle: ") Console.WriteLine(CType(con, ForeignKeyConstraint).RelatedTable) Console.Write("Übergeordnete Spalte(n): ") For Each col In CType(con, ForeignKeyConstraint).RelatedColumns Console.WriteLine(col.ColumnName & " ") Next Console.WriteLine() End If Next ' Löschen einiger Händler Print("Vor dem Löschen", ds) Dim fkey As ForeignKeyConstraint = _ CType(ds.Tables("Produkte").Constraints(0), ForeignKeyConstraint) fkey.DeleteRule = Rule.SetNull For i As Integer = 0 To 2 ds.Tables("Händler").Rows(i).Delete() Next Print("Nach Änderung", ds) Console.ReadLine() End Sub Sub Print(ByVal header As String, ByVal ds As DataSet) Console.WriteLine(header & ": ") For Each row As DataRow In ds.Tables(0).Rows If row.RowState <> DataRowState.Deleted Then _ Console.WriteLine("{0,3}{1,35}", row(0), row(1)) Next End Sub End Module End Namespace
26.5.5 In Beziehung stehende Daten suchen
DataRelation-Objekte werden oft dazu benutzt, um Daten zu suchen, die in verschiedenen DataTable-Objekten enthalten sind. Zu diesem Zweck stellt eine DataRow drei Methoden zur Verfügung, die auf einer DataRelation basieren:
- GetChildRows
- GetParentRow
- GetParentRows
GetChildRows sucht, ausgehend von einer Datenzeile in der Mastertabelle, alle zugehörigen untergeordneten Datenzeilen in der Detailtabelle. Dazu übergeben Sie der Methode die DataRelation, die die Tabellen verknüpft, und erhalten als Ergebnis ein DataRow-Array. In den folgenden Deklarationen sind optionale Teile in eckige Klammern gesetzt:
GetChildRows(relName As String [, ver As DataRowVersion]) As DataRow() GetChildRows(rel As DataRelation [, ver As DataRowVersion]) As DataRow() |
Ausgehend von der untergeordneten Zeile einer Detailtabelle liefert GetParentRow die zugehörige übergeordnete Datenzeile aus der Mastertabelle. Auch hier müssen Sie die DataRelation zwischen den beiden Tabellen angeben; der Rückgabewert ist eine einzige Datenzeile.
GetParentRow(relName As String [, ver As DataRowVersion]) As DataRow() GetParentRow(rel As DataRelation [, ver As DataRowVersion]) As DataRow() |
Sollte zwischen zwei Tabellen eine n:m-Beziehung bestehen, können Sie die GetParentRows-Methode einsetzen:
GetParentRows(relName As String [, ver As DataRowVersion]) As DataRow() GetParentRows(rel As DataRelation [, ver As DataRowVersion]) As DataRow() |
Diese Methoden verwenden wir nun, um zu den einzelnen Aufträgen (Tabelle Orders) die bestellten Produkte (Tabelle Products) aufzulisten. Zwischen den beiden Tabellen besteht eine m:n-Beziehung, die durch die Tabelle Order Details in zwei 1:n-Beziehungen aufgelöst wird (siehe Abbildung 26.3).
Abbildung 26.3 Beziehungen der Tabellen »Orders«, »Order Details« und »Products«
In dem Beispiel werden die drei Tabellen mittels TableMappings umbenannt, um den Code lesbarer zu gestalten. Nachdem der DataAdapter die Daten abgerufen hat, werden in einer äußeren For Each-Schleife alle Datenzeilen der Tabelle Aufträge (Orders) nacheinander durchlaufen. Für jede Bestellnummer (OrderID) eines Auftrags (Datenzeile) werden mit GetChildRows über die Beziehung Auftragsdaten alle Bestellungen (OrderDetails) durchlaufen. Die Bestellung ermittelt mit GetParentRow über die Beziehung Produktdaten den korrespondierenden Auftrag in der Tabelle Aufträge (Orders) und daraus den Produktnamen.
'...\ADO\DataSet\Navigation.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Navigation Sub Test() Dim conn As DbConnection = New SqlConnection() conn.ConnectionString = "Data Source=(local);" & _ "Initial Catalog=Northwind;Integrated Security=sspi" Dim cmd As DbCommand = New SqlCommand() cmd.CommandText = "SELECT * FROM Orders;" & _ "SELECT * FROM [Order Details];SELECT * FROM Products" cmd.Connection = conn Dim ds As New DataSet() Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd da.TableMappings.Add("Table", "Aufträge") da.TableMappings.Add("Table1", "Bestellung") da.TableMappings.Add("Table2", "Produkte") da.Fill(ds) ' Beziehung zwischen 'Orders' und 'Order Details' Dim master As DataColumn = ds.Tables("Aufträge").Columns("OrderID") Dim details As DataColumn = ds.Tables("Bestellung").Columns("OrderID") Dim rel As DataRelation = _ New DataRelation("Auftragsdaten", master, details) ds.Relations.Add(rel) ' Beziehung zwischen 'Order Details' und 'Products' master = ds.Tables("Produkte").Columns("ProductID") details = ds.Tables("Bestellung").Columns("ProductID") rel = New DataRelation("Produktdaten", master, details) ds.Relations.Add(rel) ' Zu jeder Bestellung die bestellten Artikel anzeigen For Each order As DataRow In ds.Tables("Aufträge").Rows Console.WriteLine("BestellNummer: {0}", order("OrderID")) For Each detail As DataRow _ In order.GetChildRows(ds.Relations("Auftragsdaten")) Dim product As DataRow = _ detail.GetParentRow(ds.Relations("Produktdaten")) Console.WriteLine("Artikel: {0}", product("ProductName")) Next detail Console.WriteLine(New String("-"c, 40)) Next order Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass es mehrere Anbieter für denselben Artikel gibt:
BestellNummer: 10248 Artikel: Queso Cabrales Artikel: Singaporean Hokkien Fried Mee Artikel: Mozzarella di Giovanni ---------------------------------------- BestellNummer: 10249 Artikel: Tofu Artikel: Manjimup Dried Apples ---------------------------------------- ...
26.5.6 Ergänzung zum Speichern von Schemainformationen in einer XML–Schemadatei
In Abschnitt 26.2.6, »Tabellenschema aus einer XML-Datei«, habe ich Ihnen gezeigt, wie Sie die Schemainformationen mit der Methode WriteXmlSchema des DataSets in einer XML-Schemadatei speichern können. Sie können selbstverständlich auf die gleiche Weise auch die Metadaten mehrerer Tabellen eines DataSets in einer Datei ablegen. Bitte beachten Sie aber, dass die Metadaten, die durch die Methode FillSchema in einem DataSet gespeichert wurden, nicht die Beziehungen zwischen den Tabellen berücksichtigen, auch wenn diese in der Originaldatenbank definiert sind.
Um auch die Beziehungen zwischen den Tabellen in der Schemadatei zu speichern, müssen Sie die Beziehungen zwischen den Tabellen zuerst mit Programmcode definieren, bevor Sie die Methode WriteXmlSchema aufrufen. So werden auch die von Ihnen definierten Tabellenbeziehungen in die XSD-Datei geschrieben.
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.