26.2 Gültigkeitsprüfung im »DataSet« 

26.2.1 Dem »DataSet« Schema-Informationen übergeben 

Die Daten in einer DataTable können editiert werden. Sie können auch neue Datenzeilen hinzufügen oder vorhandene löschen. Wie das gemacht wird, werden Sie noch später in diesem Kapitel sehen. Unabhängig davon, welche Änderungen Sie vorgenommen haben, betreffen diese zunächst nur das DataSet. Die Produktionsdatenbank weiß davon nichts. Erst zu einem späteren Zeitpunkt werden die Aktualisierungen mit der Update-Methode des SqlDataAdapters zur Originaldatenbank übermittelt und dort gespeichert.
Viele Spalten der Tabelle in der Datenbank unterliegen Gültigkeitsregeln: 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 in der Anwendung die Daten beliebig verändert werden können, ohne dass eine Überprüfung erfolgt. Der anschließende Versuch, die Änderungen in die Datenbank zu schreiben, wird jedoch scheitern, weil die Datenbank vor der endgültigen Aktualisierung zuerst die Änderungen mit den Gültigkeitsregeln vergleicht und eine Verletzung feststellt. Es kommt zu einer Ausnahme.
Im folgenden Beispielprogramm können Sie dies ausprobieren. Hierzu dient uns wieder die schon reichlich bekannte Tabelle Products der Datenbank Northwind. Das Programm ermöglicht es, den Bezeichner des ersten Artikels (es handelt sich dabei um Chai) zu ändern. 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 SqlDataAdapters sowie das zuvor erzeugte Objekt vom Typ SqlCommandBuilder sollen uns an dieser Stelle nicht interessieren.
// ------------------------------------------------------------------ // Beispiel: ...\Kapitel 26\FehlgeschlageneAktualisierung // ------------------------------------------------------------------ class Program { static void Main(string[] args) { SqlConnection con = new SqlConnection(); con.ConnectionString = "..."; SqlCommand cmd = new SqlCommand(); cmd.Connection = con; cmd.CommandText = "SELECT ProductID, ProductName, UnitsInStock " + "FROM Products"; DataSet ds = new DataSet(); SqlDataAdapter da = new SqlDataAdapter(cmd); da.Fill(ds); // Ausgabe der Ergebnisliste Console.WriteLine("Abfrageergebnis:\n"); foreach (DataRow row in ds.Tables[0].Rows) Console.WriteLine("{0,-35}{1}", row[0], row[1]); Console.WriteLine(new string('-',60)); // Datenänderung Console.Write("Namensänderung von 'Chai': "); ds.Tables[0].Rows[0]["ProductName"] = Console.ReadLine(); SqlCommandBuilder cmb = new SqlCommandBuilder(da); da.Update(ds); Console.ReadLine(); } }
Beachten Sie bitte, dass das Feld ProductName in der Datenbank auf eine Maximallänge von 40 Zeichen begrenzt ist. Ändern Sie den Artikelbezeichner innerhalb dieser Grenzen, wird die Datenbank die Änderung annehmen. Sollten Sie jedoch gegen die 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 der endgültigen Aktualisierung scheitert jedoch an der Feldlängenbegrenzung in der Datenbank.
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 darüber hinaus auch die Performance der Anwendung verschlechtern.
Besser ist es, wenn bereits das DataSet die Gültigkeitsregeln kennt. Das hat zur Folge, dass Änderungen überprüft werden, bevor sie der Datenbank übermittelt werden. In unserem Beispiel hätte dann das DataSet eine Änderung des Artikelbezeichners abgelehnt, ohne dabei die Datenbank zu kontaktieren.
Um eine Gültigkeitsüberprüfung vom DataSet vornehmen zu lassen, werden Schema-Informationen benötigt, die auf drei verschiedene Weisen einer Anwendung bereitgestellt werden können:
- Die Schema-Informationen werden mittels Programmcode für alle betreffenden Tabellen und Spalten explizit festgelegt.
- Die Schema-Informationen werden von der Datenbank mit dem SqlDataAdapter bezogen. Dazu bieten sich die Methode FillSchema sowie die Eigenschaft MissingSchemaAction an.
- Die Schema-Informationen werden aus einer XML-Schema-Datei bezogen.
Schema-Informationen beschreiben Datenüberprüfungsmechanismen, die sogenannten Einschränkungen (Constraints). Dabei handelt es sich um Einschränkungen auf Spalten- und Tabellenebene, die auch von einer DataTable und einer DataColumn unterstützt werden. Ehe ich Ihnen zeige, wie Sie ein DataSet davon überzeugen, eine Gültigkeitsüberprüfung vorzunehmen, sollten wir uns ansehen, wie die Beschränkungen von ADO.NET realisiert werden.
26.2.2 Eigenschaften einer »DataColumn«, die zur Gültigkeitsprüfung dienen 

Um die in der Anwendung eingegebenen Daten mittels Programmcode zu überprüfen, veröffentlicht das DataColumn-Objekt, mit dem eine Spalte der Abfrage beschrieben wird, einige Eigenschaften.
Eigenschaft | Beschreibung |
AllowDBNull |
Mit dieser Eigenschaft legen Sie fest, ob eine Spalte den Wert NULL akzeptiert oder nicht. |
MaxLength |
Mit dieser Eigenschaft legen Sie die Länge einer Zeichenfolge in einer Spalte fest. |
ReadOnly |
Sollen die Daten einer Spalte schreibgeschützt sein, setzen Sie diese Eigenschaft für die betreffende Spalte auf true. |
Unique |
Mit dieser Eigenschaft geben Sie an, ob die Werte in einer Spalte eindeutig sein müssen. Ist diese Eigenschaft einer Spalte auf true gesetzt, prüft ADO.NET die Werte in jeder Zeile dieser Tabelle, wenn Sie in einer Datenzeile den Wert einer UNIQUE-Spalte ändern oder einen neuen Datensatz hinzufügen. Wird gegen die Regel verstoßen, wird die Ausnahme ConstraintException ausgelöst. |
26.2.3 Die »Constraints«-Klassen einer »DataTable« 

Die folgenden beiden Klassen beschreiben Einschränkungen einer DataTable:
- UniqueConstraint
- ForeignKeyConstraint
Beide Klassen sind von der gleichen Basisklasse Constraint abgeleitet. Da eine DataTable mehrere Einschränkungen beschreiben kann, werden alle Constraint-Objekte in einer Auflistung (Typ: ConstraintCollection) verwaltet. Die Eigenschaft Constraint der DataTable liefert die Referenz auf diese Collection.
Die Klasse »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 per Code erzeugen und dessen Eigenschaft Columns der Spalte übergeben, auf der die Einschränkung gesetzt wird. Das Setzen der Eigenschaft Unique einer Spalte 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.
Die Klasse »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 Northwind-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 später noch einmal zurückkommen.
Primärschlüsselfelder
Primärschlüssel werden in der DataTable definiert. Die entsprechende Eigenschaft lautet PrimaryKey. Dass ein Primärschlüssel nicht die Eigenschaft einer DataColumn besitzt, 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.
26.2.4 Schema mit Programmcode erzeugen 

Die Bereitstellung eines Schemas ist verhältnismäßig aufwendig. Mit den Eigenschaften AllowDBNull, MaxLength und Unique einer DataColumn sowie PrimaryKey einer DataTable können Sie Datenmechanismen implementieren. Mit ReadOnly=true haben Sie zudem die Möglichkeit, gültige Daten vor einer Veränderung durch den Benutzer zu schützen.
Auch im folgenden Beispiel soll der Bezeichner eines Artikels der Tabelle Products geändert werden. Etwas Ähnliches habe ich ein paar Seiten zuvor schon einmal gezeigt. Diesmal wird die DataTable im DataSet jedoch mit den Schema-Informationen für die abgefragten Felder gefüllt. Aus Gründen der Übersichtlichkeit wird der entsprechende Code in eine spezielle Methode ausgelagert, die nach dem Füllen des DataSets aufgerufen wird.
// ------------------------------------------------------------------ // Beispiel: ...\Kapitel 26\SchemaPerCode // ------------------------------------------------------------------ class Program { static void Main(string[] args) { SqlConnection con = new SqlConnection(); con.ConnectionString = "..."; SqlCommand cmd = new SqlCommand(); cmd.Connection = con; cmd.CommandText = "SELECT ProductID, ProductName, " + "QuantityPerUnit, Discontinued FROM Products"; DataSet ds = new DataSet(); SqlDataAdapter da = new SqlDataAdapter(cmd); da.Fill(ds); FillSchemaInfos(ds.Tables[0]); // Der Artikelbezeichner des Artikels in der ersten Datenzeile // soll geändert werden. DataRow row = ds.Tables[0].Rows[0]; Console.Write("Produktname ändern: "); row["ProductName"] = Console.ReadLine(); // Änderung in die Originaldatenbank schreiben SqlCommandBuilder cmb = new SqlCommandBuilder(da); da.Update(ds); Console.WriteLine("Aktualisierung erfolgreich."); Console.ReadLine(); } // Diese Methode füllt die Tabelle mit Schema-Informationen. static void FillSchemaInfos(DataTable tbl) { // ProductID tbl.PrimaryKey = new DataColumn[]{ tbl.Columns["ProductID"]}; // ProductName tbl.Columns["ProductName"].MaxLength = 40; tbl.Columns["ProductName"].AllowDBNull = false; // QuantityPerUnit tbl.Columns["QuantityPerUnit"].MaxLength = 20; // Discontinued tbl.Columns["Discontinued"].AllowDBNull = false; } }
Sie können selbst bestimmen, welchen Bezeichner der Artikel bekommen soll. Entscheiden Sie sich für einen Bezeichner mit maximal 40 Zeichen, wird die Änderung in die Datenbank geschrieben. Geben Sie aber mehr als 40 Zeichen ein, wird eine Ausnahme vom Typ ArgumentException ausgelöst. Vielleicht erinnern Sie sich an das ähnliche Aktualisierungsbeispiel weiter oben. Dort wurde das Überschreiten der zulässigen Maximallänge des Feldes ProductName mit einer Ausnahme vom Typ SqlException beantwortet. Diese kam vom SQL Server. Die Ausnahme ArgumentException hingegen wird von ADO.NET in der Clientanwendung ausgelöst.
Vergleichen Sie auch, in welcher Codezeile die Ausnahme auslöst wird: Aktualisieren Sie ohne Schema-Informationen, ist die Update-Methode des SqlDataAdapters der Urheber. Liegen Schema-Informationen im DataSet vor und wird gegen die Einschränkungen verstoßen, handelt es sich um die Anweisung mit der fehlschlagenden Zuweisung, in unserem Beispiel also:
row["ProductName"] = Console.ReadLine();
Das DataSet nimmt die Änderung überhaupt nicht entgegen. Damit ist gezeigt, dass ein DataSet mit Schema-Informationen zur Entlastung des Datenbankservers beiträgt.
26.2.5 Schema-Informationen mit »SqlDataAdapter« abrufen 

Schema-Informationen mit »FillSchema« abrufen
Enthält ein DataSet mehrere Tabellen mit jeweils vielen Spalten, kann die Codierung der Schema-Informationen ziemlich aufwendig sein. Ebenso können Sie mit der Methode FillSchema des SqlDataAdapters alle Schema-Informationen für das DataSet oder die DataTable abrufen. Die Methode ruft das Schema bei der Datenbank ab. Grundlage ist dabei das in SelectCommand beschriebene SELECT-Kommando. Als Ergebnis des Methodenaufrufs werden die Eigenschaften ReadOnly, AllowDBNull, AutoIncrement, Unique und MaxLength der in der Abfrage enthaltenen Spalten gesetzt. Außerdem werden die Eigenschaften PrimaryKey und Constraints der entsprechenden Tabelle festgelegt.
FillSchema ist mehrfach überladen. Alle Überladungen erwarten ein Argument vom Typ der Enumeration SchemaType. Die Aufzählung hat zwei Mitglieder: Source und Mapped. Über diese Parameter wird gesteuert, ob der SqlDataAdapter die Zuordnungen verwenden soll, die in der DataTableMappingCollection und der DataColumnMappingCollection angegeben sind.
Beschreiben Sie mittels Programmcode die Gültigkeitsregeln, können diese zu jedem beliebigen Zeitpunkt gesetzt werden. Es muss nur vor der Aktualisierung der Daten im DataSet sein. Benutzen Sie dagegen die Methode FillSchema, muss dies vor dem Füllen des DataSets erfolgen.
... DataSet ds = new DataSet(); da.FillSchema(ds, SchemaType.Source); da.Fill(ds); ...
Der Aufruf der Methode ist einerseits natürlich sehr bequem, aber andererseits dürfen Sie nicht vergessen, dass dabei sowohl das Netzwerk als auch die Datenbank selbst belastet werden.
Die Eigenschaft »MissingSchemaAcion« des »SqlDataAdapter«
Per Vorgabe ist der SqlDataAdapter so eingestellt, dass Spalten zu einer DataTable hinzugefügt werden, wenn diese in der DataTable noch nicht existieren. Damit stellt der SqlDataAdapter sicher, die Ergebnisse einer Abfrage speichern zu können. Gesteuert wird dieses Verhalten von der Eigenschaft MissingSchemaAction, die Werte der gleichnamigen Aufzählung beschreibt.
Wert | Beschreibung |
Add |
Fügt die erforderlichen Spalten zum Vervollständigen des Schemas hinzu. |
AddWithKey |
Findet der SqlDataAdapter eine Spalte, die noch nicht in der DataTable existiert, fügt er die Spalte hinzu und setzt die Eigenschaften MaxLength und AllowDBNull. Falls die DataTable noch nicht existiert, wird die Datenbank zudem nach Primärschlüsselinformationen abgefragt. |
Error |
Wenn die angegebene Spaltenzuordnung fehlt, wird die Ausnahme InvalidOperation ausgelöst. |
Ignore |
Ignoriert die zusätzlichen Spalten. |
Legen Sie die Eigenschaft MissingSchemaAction auf den Wert AddWithKey fest, werden ähnlich wie mit der Methode FillSchema die Schema-Informationen 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 hierbei nicht berücksichtigt.
Schema-Informationen aus einer XML-Schema-Datei beziehen
Nun kennen Sie zwei Varianten, 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. Daher ist dies wohl eher nur für Ad-hoc-Abfragen geeignet. Alternativ können Sie die Schema-Informationen auch mittels Programmcode beschreiben. Zur Laufzeit ist das sicher effektiv, weil das Netz und die Datenbank nur die tatsächlich benötigten Dateninformationen liefern müssen, während die Metadaten im Code beschrieben werden. Allerdings bedeutet das einen nicht zu vernachlässigenden Programmieraufwand.
Die nun vorgestellte dritte Möglichkeit ist wohl in den meisten Fällen diejenige, die sich am besten eignet. Ausgangspunkt sind zwei Methoden des DataSets: WriteXmlSchema und ReadXmlSchema. Mit WriteXmlSchema können Sie die Schema-Informationen eines DataSets in ein XML-Dokument schreiben und es später mit ReadXmlSchema auswerten. Das Schema enthält Definitionen von Tabellen, Beziehungen und Einschränkungen. XML-Schema-Dateien haben üblicherweise die Dateiendung .XSD.
Bevor Sie das Schema eines DataSets in einer Schema-Datei 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(@"D:\Products.xsd");
Die erzeugte Schema-Datei muss zusammen mit der Anwendung ausgeliefert werden. In Abbildung 26.2 sehen Sie die Schema-Datei, die auf einer Abfrage basiert, die die Spalten ProductID und ProductName der Tabelle Products wiedergibt.
Abbildung 26.2 Beispiel für eine mit der Methode »WriteXmlSchema« erzeugte Schema-Datei
Sie erkennen, dass die Spalte ProductID die Primärschlüsselspalte der Tabelle beschreibt. AutoIncrement=True signalisiert, das der Spaltenwert bei einer neu hinzugefügten Spalte automatisch erhöht wird. Infolgedessen gilt für die Spalte ProductID auch ReadOnly=True. Die Spalte ProductName weist lediglich eine Einschränkung auf, nämlich die Begrenzung auf maximal 40 Zeichen.
Die Auswertung einer Schema-Datei ist sehr einfach. Zur Laufzeit erzeugen Sie zuerst das DataSet-Objekt, lesen anschließend die Schema-Datei ein und füllen danach das DataSet mit den Daten.
... DataSet ds = new DataSet(); ds.ReadXmlSchema(@"D:\Products.xsd"); da.Fill(ds);
Dateninformationen und Schema-Daten in eine Datei schreiben
Mit WriteXmlSchema erzeugen Sie eine Schema-Datei, die die Metadaten des DataSets beinhaltet. Mit der einfach parametrisierten Methode WriteXml des DataSets lassen sich die Daten in einer XML-Datei sichern.
Benötigen Sie beide Informationen, müssen Sie nicht zwangsläufig Metadaten und Dateninformationen jeweils in einer separaten Datei speichern. Mit einer Überladung von WriteXml lässt sich der aktuelle Inhalt des DataSets als XML-Daten mit den Metadaten als XSD-Inline-Schema beschreiben. Sowohl Daten als auch Schema sind in einer Datei gespeichert.
ds.WriteXml(@"D:\Products.xml", XmlWriteMode.WriteSchema);
Der Vorgabewert von XmlWriteMode ist IgnoreSchema. Das ist die Einstellung, wenn Sie die einfach parametrisierte Methode aufrufen.