26.2 DataSet mit Schemainformationen
Sie können zur Verarbeitung von Daten eine DataTable editieren, neue Datenzeilen hinzufügen oder vorhandene löschen. Wie das gemacht wird, werden Sie später in diesem Kapitel noch sehen. Unabhängig davon, welche Änderungen Sie vorgenommen haben, betreffen diese zunächst nur das DataSet. Die Originaldatenbank weiß davon nichts. Erst zu einem späteren Zeitpunkt werden die Aktualisierungen mit der Update-Methode des DataAdapters zur Originaldatenbank übermittelt und dort gespeichert.
Oft müssen Spalten einer Tabelle der Datenbank Gültigkeitsregeln beachten: Beispielsweise lassen einige nur eine maximale Zeichenanzahl zu, andere schreiben einen eindeutigen Eintrag innerhalb der Datensätze der Tabelle vor oder lassen keinen NULL-Wert zu. Eine DataTable, die wir mit Fill füllen, ist hingegen sehr dumm. Sie enthält zwar alle angeforderten Daten, weiß aber nichts von den Gültigkeitsregeln, die in der Datenbank festgelegt sind. Die Folge ist, dass die Anwendung die Daten beliebig verändern kann, ohne dass eine Überprüfung erfolgt. Der anschließende Versuch, die Änderungen in die Datenbank zu schreiben, wird jedoch möglicherweise scheitern, weil die Datenbank vor der endgültigen Aktualisierung zuerst die Änderungen mit den Gültigkeitsregeln vergleicht und eine Verletzung feststellen wird. Es kommt zu einer Ausnahme.
Im folgenden Beispielprogramm können Sie das ausprobieren. Hierzu dient wieder die bekannte Tabelle Products der Northwind-Datenbank. Das Programm ermöglicht es uns, die Lagermenge des ersten Artikels zu ändern – es handelt sich dabei um Chai. Dazu werden Sie an der Konsole aufgefordert. Die Änderung wird zuerst in das DataSet geschrieben, anschließend wird die Originaldatenbank aktualisiert. Die Aktualisierungslogik mit der Methode Update des DataAdapters sowie das zuvor erzeugte Objekt vom Typ CommandBuilder soll uns an dieser Stelle nicht interessieren.
'...\ADO\DataSet\Inkonsistenz.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module Inkonsistenz 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 Top 2 ProductID, ProductName, UnitsInStock FROM Products" cmd.Connection = con Dim ds As New DataSet() Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd da.Fill(ds) Print(ds) Console.Write("UnitsInStock-Änderung des ersten Produkts: ") ds.Tables(0).Rows(0)("UnitsInStock") = Console.ReadLine() Dim cmb As DbCommandBuilder = New SqlCommandBuilder() cmb.DataAdapter = da Try da.Update(ds) Catch ex As SqlException Console.WriteLine("Fehler: {0}", ex.Message) End Try Print(ds) ds.Clear() : da.Fill(ds) Print(ds) Console.ReadLine() End Sub Sub Print(ByVal ds As DataSet) Console.WriteLine("Produkte: ") For Each row As DataRow In ds.Tables(0).Rows Console.WriteLine("{0,2}{1,35}{2,3}", row(0), row(1), row(2)) Next End Sub End Module End Namespace
Das Feld UnitsInStock in der Datenbank darf nicht negativ sein. Sollten Sie gegen diese Beschränkung verstoßen, wird eine Ausnahme vom Typ SqlException ausgelöst, die von der Datenbank initiiert wird. Die geänderte Spalte im DataSet hatte keinen Einwand gegen die vorgenommene Änderung, denn bekanntlich sind die Daten im DataSet dumm. Der Versuch, sie endgültig zu aktualisieren, scheitert jedoch an der Feldlängenbegrenzung in der Datenbank. Die folgende Ausgabe zeigt, dass die Originaldaten unverändert bleiben:
Produkte: 1 Chai 39 2 Chang 17 UnitsInStock-Änderung des ersten Produkts: –2 Fehler: Die UPDATE-Anweisung steht in Konflikt mit der CHECK-Einschränkung "CK_UnitsInStock". Der Konflikt trat in der "Northwind"-Datenbank, Tabelle "dbo.Products", column 'UnitsInStock' auf. Die Anweisung wurde beendet. Produkte: 1 Chai –2 2 Chang 17 Produkte: 1 Chai 39 2 Chang 17
Obwohl aufgrund der Einschränkungen in der Datenbank sichergestellt ist, dass keine unzulässigen Daten geschrieben werden, stellt der gezeigte Ansatz keine gute Lösung dar. Denken Sie nur an eine stark frequentierte Datenbank im Internet. Jeder Anwender, der unzulässige Daten übermittelt, würde von der Datenbank in Form einer Ausnahme über das Scheitern der Aktualisierung informiert. Der Datenfluss von der Datenbank zum Anwender würde nicht nur das Netz belasten, sondern auch die Performance der Anwendung verschlechtern.
26.2.1 Schemainformationen bereitstellen
Besser ist es, wenn bereits das DataSet die Gültigkeitsregeln kennt. So können Änderungen überprüft werden, bevor sie der Datenbank übermittelt werden. In unserem Beispiel hätte dann das DataSet eine negative Menge abgelehnt, ohne die Datenbank zu kontaktieren.
Damit DataSet die Gültigkeit prüfen kann, werden Schemainformationen benötigt, die einer Anwendung auf drei verschiedene Arten bereitgestellt werden können:
- Die Schemainformationen werden mittels Programmcode für alle betreffenden Tabellen und Spalten explizit festgelegt.
- Die Schemainformationen werden von der Datenbank mit der Methode FillSchema sowie der Eigenschaft MissingSchemaAction des DataAdapters bezogen.
- Die Schemainformationen werden aus einer XML-Schemadatei bezogen.
Schemainformationen beschreiben Datenüberprüfungsmechanismen, die sogenannten Einschränkungen (Constraints). Sie wirken auf Spalten- und Tabellenebene, die von einer DataTable und einer DataColumn unterstützt werden. Ich erkläre erst, wie die Einschränkungen in ADO.NET realisiert sind, und dann die Zusammenarbeit mit DataSet.
26.2.2 Gültigkeitsprüfung in einer DataColumn
Um die in der Anwendung eingegebenen Daten mittels Programmcode zu überprüfen, stellt das DataColumn-Objekt, mit dem eine Spalte der Abfrage beschrieben wird, einige Eigenschaften zur Verfügung (siehe Tabelle 26.1). Weitere Einschränkungen speichert die Constraints-Auflistung einer DataTable.
Eigenschaft | Beschreibung |
AllowDBNull |
Legt fest, ob eine Spalte den Wert NULL akzeptiert oder nicht. |
AutoIncrement |
Automatische Nummerierung von Zeilen (keine eigenen Werte) |
DataType |
Datentyp der Werte (Meist erfolgt die Gültigkeitsprüfung bereits durch den korrespondierenden .NET-Typ.) |
Expression |
Ausdruck zur Berechnung des Wertes in einer Spalte |
MaxLength |
Maximale Anzahl der Zeichen in einer Zeichenfolge in einer Spalte |
ReadOnly |
Mit dem Wert True sind die Daten einer Spalte vor dem Überschreiben geschützt. |
Unique |
Für True ist sichergestellt, dass alle Werte einer Spalte verschieden sind. |
26.2.3 Constraints-Klassen einer DataTable
Zwei von der Basisklasse Constraint abgeleitetete Klassen beschreiben die Einschränkungen einer DataTable:
- UniqueConstraint
- ForeignKeyConstraint
Da eine DataTable mehrere Einschränkungen haben kann, werden alle Constraint-Objekte in einer Auflistung des Typs ConstraintCollection verwaltet, die über die Eigenschaft Constraints der DataTable erreichbar ist.
Hinweis |
Eine Verletzung einer Einschränkung erzeugt eine Ausnahme, die Sie in Ihrem Code abfangen sollten. |
UniqueConstraint
Ein UniqueConstraint-Objekt wird automatisch angelegt, wenn die Eigenschaft Unique einer Spalte auf True gesetzt wird. Gleichzeitig wird das Objekt der ConstraintCollection hinzugefügt. Sie können ein UniqueConstraint-Objekt natürlich auch im Code erzeugen und dessen Eigenschaft Columns die Spalte übergeben, auf der die Einschränkung gesetzt wird. Die Eigenschaft Unique einer Spalte zu setzen ist aber einfacher. Trotzdem kann das explizite Erzeugen sinnvoll sein. Das ist der Fall, wenn Sie sicherstellen müssen, dass die Kombination von Werten aus mehreren Spalten eindeutig ist.
ForeignKeyConstraint
Mit einem ForeignKeyConstraint-Objekt können Sie festlegen, wie sich eine Beziehung zwischen Tabellen bezüglich Datenänderungen auswirken soll. In der Tabelle Products der North-wind-Datenbank muss die Spalte CategoryID einen Wert enthalten, der in der Tabelle Categories enthalten ist. Der Spalte CategoryID wird dazu ein ForeignKeyConstraint-Objekt zugeordnet. Allerdings müssen Sie dieses nicht explizit erzeugen. Wenn Sie im DataSet eine Beziehung zwischen zwei Tabellen einrichten, wird automatisch ein ForeignKeyConstraint-Objekt erzeugt. Wir werden auf das Thema der Einrichtung einer Beziehung zwischen zwei Tabellen in Abschnitt 26.5, »Mit mehreren Tabellen arbeiten«, zurückkommen.
Primärschlüsselfelder
Primärschlüssel werden in der DataTable definiert. Die entsprechende Eigenschaft heißt PrimaryKey. Dass ein Primärschlüssel nicht die Eigenschaft einer DataColumn ist, liegt daran, dass viele Tabellen mehrere Spalten zu einem gemeinsamen Primärschlüssel kombinieren. Die PrimaryKey-Eigenschaft der DataTable beschreibt deshalb auch ein Array von DataColumn-Objekten. Beim Festlegen der PrimaryKey-Eigenschaft wird ein UniqueConstraint-Objekt erzeugt, um die Primärschlüsseleinschränkung durchzusetzen.
CHECK-Einschränkungen
Zu CHECK-Einschränkungen auf Datenbankebene gibt es keine korrespondierende Klasse in ADO.NET. Wollen Sie eine der Constraint-Klassen beerben, müssen Sie mit Reflection arbeiten, da die entscheidende Methode, Friend Overrides Sub CheckConstraint(row As DataRow, action As DataRowAction), keinen öffentlichen Zugriff erlaubt.
Einfacher ist die Registrierung einer Methode für das ColumnChanging-Ereignis. Bei Verletzung der Einschränkung lösen Sie dann eine Ausnahme aus. Dies ist auch der Mechanismus, den die eingebauten Einschränkungen nutzen.
26.2.4 Tabellenschema durch Programmcode
Verhältnismäßig aufwändig ist die Bereitstellung eines Schemas durch die Eigenschaften AllowDBNull, MaxLength und Unique einer DataColumn sowie der Eigenschaft PrimaryKey einer DataTable. Mit ReadOnly=True haben Sie zudem die Möglichkeit, gültige Daten vor einer Veränderung durch den Benutzer zu schützen.
Im folgenden Beispiel soll die Bestellmenge eines Produkts der Tabelle Products geändert werden. Ein ähnliches Beispiel habe ich ein paar Seiten zuvor schon einmal gezeigt. Diesmal wird die DataTable im DataSet jedoch mit den Schemainformationen für die abgefragten Felder gefüllt. Der erste Codeabschnitt zeigt den Rahmen: Kommando und DataAdapter erzeugen, Schema im Code erzeugen (Schema, siehe unten), Ausdruck der Originaltabelle (Print, siehe unten), Änderung von Werten und Ausdruck der neuen Tabelle. Die Synchronisation mit dem Datenbankserver ist weggelassen.
'...\ADO\DataSet\SchemaDurchCode.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module SchemaDurchCode 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 Top 2 ProductID, ProductName, UnitsInStock FROM Products" cmd.Connection = con Dim ds As New DataSet() Dim da As DbDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd da.Fill(ds) Schema(ds.Tables(0)) Print(ds) Try Console.Write("UnitsInStock-Änderung des ersten Produkts: ") ds.Tables(0).Rows(0)("UnitsInStock") = Console.ReadLine() Console.Write("ProductName-Änderung des ersten Produkts: ") ds.Tables(0).Rows(0)("ProductName") = Console.ReadLine() Catch ex As Exception Console.WriteLine("Falscher Wert: {0}.", ex.Message) End Try Print(ds) Console.ReadLine() End Sub ... End Module End Namespace
Die Metainformation wird in Schema erstellt. Die maximale Länge der Produktbezeichnung und das Verbieten von NULL-Werten wird auf Spaltenebene festgelegt, der Primärschlüssel auf Tabellenebene. Zur Überprüfung der Mengenangaben wird der Ereignishandler Positiv definiert und beim Ereignis registriert. Im Fall einer Verletzung wird eine Ausnahme ausgelöst.
'...\ADO\DataSet\SchemaDurchCode.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module SchemaDurchCode ... Sub Schema(ByVal tbl As DataTable) tbl.PrimaryKey = New DataColumn() {tbl.Columns("ProductID")} tbl.Columns("ProductName").MaxLength = 10 tbl.Columns("ProductName").AllowDBNull = False AddHandler tbl.ColumnChanging, AddressOf Positiv End Sub Sub Positiv(ByVal sender As Object, ByVal ev As DataColumnChangeEventArgs) If ev.Column.ColumnName = "UnitsInStock" AndAlso _ CType(ev.ProposedValue, Int16) < 0 Then _ Throw New ApplicationException("negativ") End Sub Sub Print(ByVal ds As DataSet) Console.WriteLine("Produkte: ") For Each row As DataRow In ds.Tables(0).Rows Console.WriteLine("{0,2}{1,35}{2,3}", row(0), row(1), row(2)) Next End Sub End Module End Namespace
Die folgende Ausgabe zeigt die durch die negative Mengenangabe erzeugte Ausnahme im Ereignishandler:
Produkte: 1 Chai 39 2 Chang 17 UnitsInStock-Änderung des ersten Produkts: –2 Falscher Wert: negativ. Produkte: 1 Chai 39 2 Chang 17
Ein zu langer Produktname wird von der Spalteneinschränkung zurückgewiesen. Die vorher gesetzte Menge von 20 bleibt davon unberührt:
Produkte: 1 Chai 39 2 Chang 17 UnitsInStock-Änderung des ersten Produkts: 20 ProductName-Änderung des ersten Produkts: Kamillentee Falscher Wert: Cannot set column 'ProductName'. The value violates the MaxLength limit of this column.. Produkte: 1 Chai 20 2 Chang 17
Da nach dem Füllen des DataSets kein Kontakt zum Datenbankserver aufgebaut wird, ist sichergestellt, dass die Prüfung komplett auf Clientseite stattfindet. Der Datenbankserver ist also entlastet.
26.2.5 Tabellenschema durch DataAdapter
FillSchema
Enthält ein DataSet mehrere Tabellen mit jeweils verhältnismäßig vielen Spalten, kann die Codierung der Schemainformationen eine ziemlich aufwändige, aber auch langweilige Aufgabe sein. Mit der Methode FillSchema des DataAdapters können Sie alle Schemainformationen für das DataSet oder die DataTable bei der Datenbank abrufen.
Grundlage ist dabei das in SelectCommand beschriebene SELECT-Kommando. Als Ergebnis des FillSchema-Aufrufs werden die Eigenschaften ReadOnly, AllowDBNull, AutoIncrement, Unique und MaxLength der in der Abfrage enthaltenen Spalten gesetzt und die Eigenschaften PrimaryKey und Constraints der entsprechenden Tabelle festgelegt.
Alle Überladungen von FillSchema erwarten ein Argument vom Typ der in Tabelle 26.2 gezeigten Enumeration SchemaType. Der Parameter steuert, ob die Spaltenzuordnungen in der DataTableMappingCollection und der DataColumnMappingCollection benutzt werden.
Konstante | Beschreibung |
Mapped |
Der DataAdapter verwendet die Spaltenzuordnungen. |
Source |
Der DataAdapter ignoriert die Spaltenzuordnungen. |
Wenn Sie mittels Programmcode die Gültigkeitsregeln beschreiben, können diese zu jedem beliebigen Zeitpunkt gesetzt werden, also auch nach dem Füllen des DataSets. Es muss aber vor der Aktualisierung der Daten sein. Ziehen Sie die Methode FillSchema vor, muss diese vor dem Füllen des DataSets aufgerufen werden:
... Dim ds As DataSet = New DataSet() da.FillSchema(ds, SchemaType.Source) da.Fill(ds) ...
Der Aufruf der Methode ist natürlich sehr bequem. Sie dürfen dabei aber nicht vergessen, dass dabei sowohl das Netzwerk als auch die Datenbank belastet werden.
MissingSchemaAction
Der DataAdapter ist so eingestellt, dass Spalten zu einer DataTable hinzugefügt werden, wenn diese in der DataTable noch nicht existieren. Damit stellt der DataAdapter sicher, die Ergebnisse einer Abfrage speichern zu können. Geändert wird dieses Verhalten durch die Eigenschaft MissingSchemaAction, die Werte der gleichnamigen Aufzählung beschreibt (siehe Tabelle 26.3).
Konstante | Beschreibung |
Add |
Fügt die erforderlichen Spalten zum Vervollständigen des Schemas hinzu. |
AddWithKey |
Eine nicht in der DataTable enthaltene Spalte wird hinzugefügt, und es werden MaxLength sowie AlloDBNull gesetzt. Wenn die DataTable noch nicht existiert, wird aus der Datenbank der Primärschlüssel ermittelt. |
Error |
Werden Daten gelesen, die nicht in das Schema passen, wird die Ausnahme InvalidOperation ausgelöst. |
Ignore |
Ignoriert die zusätzlichen Spalten. |
Hat die Eigenschaft MissingSchemaAction den Wert AddWithKey, werden ähnlich wie mit der Methode FillSchema die Schemainformationen abgerufen. Diese sind jedoch auf den Primärschlüssel der Tabelle sowie die Einschränkungen AllowDBNull und MaxLength der Spalten beschränkt. Unique, AutoIncrement und ReadOnly werden nicht berücksichtigt.
26.2.6 Tabellenschema aus einer XML-Datei
Nun kennen Sie zwei Varianten, um Metadaten einer Tabelle im DataSet bereitzustellen. Sie wissen, dass es sehr einfach ist, mit FillSchema oder MissingSchemaAction=AddWithKey zu arbeiten. Der Nachteil dabei ist die erhöhte Belastung des Netzes und der Datenbank. Die Methode ist daher eher für Ad-hoc-Abfragen geeignet. Die Schemainformationen mittels Programmcode zu beschreiben ist bezüglich der Laufzeit effektiv, weil das Netz und die Datenbank nur die Daten selbst liefern müssen, während die Metadaten im Code beschrieben werden. Allerdings bedeutet das einen nicht unerheblichen Programmieraufwand.
Als dritte, praxisgerechte Möglichkeit stellt DataSet mit WriteXmlSchema und ReadXmlSchema zwei Methoden zur Verfügung, die das Schema in XML beschreiben. WriteXmlSchema schreibt die Schemainformationen eines DataSets in ein XML-Dokument, ReadXmlSchema liest ein solches Schema ein. Das Schema enthält Definitionen von Tabellen und Einschränkungen. XML-Schemadateien haben üblicherweise die Dateiendung .xsd.
Bevor Sie das Schema eines DataSets in einer Schemadatei speichern, muss das Schema im DataSet bekannt sein. Sie können sich dieses daher zur Entwicklungszeit mit FillSchema besorgen und anschließend mit WriteXmlSchema in einer Datei speichern:
ds.WriteXmlSchema("C:MyDataSetSchema.xsd")
Die erzeugte Schemadatei muss zusammen mit der Anwendung ausgeliefert werden. Im folgenden Listing sehen Sie die Schemadatei einer Abfrage der Spalten ProductID und ProductName der Tabelle Products:
<?xml version="1.0" standalone="yes"?> <xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/ XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xs:element name="NewDataSet" msdata:IsDataSet="true" msdata:UseCurrentLocale="true"> <xs:complexType><xs:choice minOccurs="0" maxOccurs="unbounded"> <xs:element name="Table"> <xs:complexType><xs:sequence> <xs:element name="ProductID" msdata:ReadOnly="true" msdata:AutoIncrement="true" type="xs:int" /> <xs:element name="ProductName"><xs:simpleType> <xs:restriction base="xs:string <xs:maxLength value="40" /> </xs:restriction> </xs:simpleType></xs:element> </xs:sequence></xs:complexType> </xs:element> </xs:choice></xs:complexType> <xs:unique name="Constraint1" msdata:PrimaryKey="true"> <xs:selector xpath=".//Table" /> <xs:field xpath="ProductID" /> </xs:unique> </xs:element> </xs:schema>
Sie können erkennen, dass die Spalte ProductID die Primärschlüsselspalte der Tabelle beschreibt. AutoIncrement=True signalisiert, dass der Spaltenwert bei einer neu hinzugefügten Spalte automatisch erhöht wird. Infolgedessen gilt für die Spalte auch ReadOnly=True. Die Spalte ProductName ist auf maximal 40 Zeichen begrenzt.
Die Auswertung einer Schemadatei ist sehr einfach. Zur Laufzeit erzeugen Sie zuerst das DataSet-Objekt, lesen anschließend die Schemadatei ein und füllen danach das DataSet mit den Daten:
...
Dim ds As DataSet = New DataSet()
ds.ReadXmlSchema("C:MyDataSetSchema.xsd")
da.Fill(ds)
Daten und Schema in eine Datei schreiben
Mit WriteXmlSchema erzeugen Sie eine Schemadatei, die die Metadaten des DataSets beinhaltet. Analog sichert WriteXml von DataSet die Daten in einer XML-Datei.
Metadaten und Dateninformationen müssen Sie nicht in getrennten Dateien speichern. Mit einer Überladung von WriteXml lässt sich der aktuelle Inhalt des DataSets als XML-Daten mit den Metadaten als XSD-Inlineschema beschreiben, d. h., sowohl Daten als auch Schema sind in einer Datei gespeichert.
ds.WriteXml("C:ContentsOfdataSet.xml", XmlWriteMode.WriteSchema)
Das Auslassen des zweiten Parameters ist äquivalent zu XmlWriteMode.IgnoreSchema.
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.