# 26 ADO.NET – Daten im lokalen Speicher Wäre man gezwungen, eine Rangfolge der ADO.NET-Typen nach ihrer Wichtigkeit aufzustellen, würde DataSet zweifelsfrei an erster Position stehen. Diese Klasse bildet den Kern von ADO.NET, um den herum sich fast alles andere rankt. Ein DataSet ist in erster Linie ein Datencontainer. Organisiert und verwaltet werden die Daten in Form von Tabellen. Wenn Sie sich darunter Tabellen ähnlich denen von Microsoft-Excel vorstellen, liegen Sie gar nicht so falsch. Ob es sich um eine oder auch mehrere Tabellen handelt, hängt von der zugrunde liegenden Abfrage ab, die durch das SqlCommand-Objekt beschrieben wird. Enthält das DataSet mehrere Tabellen, so können zwischen den Tabellen Beziehungen eingerichtet werden – ganz so wie in der Originaldatenbank. In Kapitel 24, »ADO.NET – Das Command-Objekt«, haben Sie den Typ SqlDataReader kennengelernt. Mit einem Objekt dieses Typs können Sie Daten basierend auf einer Abfrage abrufen. Ein SqlDataReader ist aber nicht so weit ausgebildet, dass er die üblichen Aufgaben einer Datenbankanwendung erfüllen kann. Wie Sie wissen, können Sie nur vorwärts navigieren, zudem sind die Daten schreibgeschützt. Damit ist der SqlDataReader in seiner Funktionalität sehr eingeschränkt, er ist allerdings enorm effizient, denn er ist auf Performance ausgelegt. Ein DataSet hingegen bietet Ihnen im Vergleich dazu deutlich mehr Funktionalitäten, schneidet hinsichtlich der Performance aber schlechter ab. Die Daten im DataSet stehen in keinem Kontakt zur Datenbank. Nachdem das DataSet über das SqlDataAdapter-Objekt gefüllt worden ist, gibt es keine Verbindung zwischen DataSet und Datenbank mehr. Nimmt ein Anwender Änderungen an den Daten vor, schreiben sich diese nicht sofort in die Originaldatenbank zurück, sondern werden vielmehr zunächst im DataSet gespeichert. Zum Zurückschreiben der geänderten Daten muss ein Anstoß erfolgen. Häufig kann man sich dazu wieder des SqlDataAdapters bedienen, der die notwendige Aktualisierungslogik bereitstellt. Sollten Sie Erfahrungen mit Datenbanken haben, werden Sie jetzt sicherlich sofort einwenden, dass damit Konfliktsituationen vorprogrammiert sind, wenn ein zweiter Anwender zwischenzeitlich Änderungen am gleichen Datensatz vorgenommen hat. Der Einwand ist korrekt, andererseits gibt uns ADO.NET alle Mittel an die Hand, um eine benutzerdefinierte Konfliktsteuerung und Konfliktanalyse zu codieren. Darüber hinaus können Sie eine Konfliktlösung realisieren, ganz so, wie es Sie es sich vorstellen. Mit der Aktualisierung der Originaldatenbank werden wir uns in diesem Kapitel jedoch noch nicht beschäftigen. Damit sind noch nicht alle Fähigkeiten des DataSets erwähnt. In einem DataSet lässt sich die Ansicht der Abfrageergebnisse ändern. Sie können die Daten basierend auf einer oder mehreren Spalten sortieren. Setzen Sie im DataSet einen Filter, sehen Sie nur Daten, die bestimmte Kriterien erfüllen. Zudem ist die Zusammenarbeit eines DataSets mit XML ausgezeichnet. Der Inhalt eines DataSets kann als XML-Dokument in einer Datei gespeichert und der Inhalt einer XML-Datei in ein DataSet eingelesen werden. Darüber hinaus lassen sich die Schema-Informationen eines DataSets in einer XML-Schema-Datei speichern. ## 26.1 »DataSet«-Objekte verwenden ### 26.1.1 »DataSet«-Objekte erzeugen Die Klasse DataSet befindet sich, wie viele andere Klassen auch, die nicht providerspezifisch sind, im Namespace System.Data. In den meisten Fällen ist der parameterlose Konstruktor vollkommen ausreichend, um ein DataSet-Objekt zu erzeugen.
    DataSet ds = new DataSet();
Soll das DataSet einen Namen erhalten, bietet sich alternativ der einfach parametrisierte Konstruktor an:
    DataSet ds = new DataSet("Bestellungen");
Der Name kann auch über die Eigenschaft DataSetName festgelegt oder abgerufen werden. ### 26.1.2 Anatomie einer »DataTable« Zum Leben erweckt wird ein DataSet-Objekt nicht durch die Instanziierung der Klasse, sondern vielmehr durch den Aufruf der Fill-Methode des DataAdapters:
    ...
    string strSQL = "SELECT * FROM Products";
    SqlDataAdapter da = new SqlDataAdapter(strSQL, con);
    DataSet ds = new DataSet();
    da.Fill(ds);
    ...
Das Ergebnis der Abfrage enthält alle Datensätze der Tabelle Products. Die Datensätze sind in einer Tabelle enthalten, die durch ein DataTable-Objekt beschrieben wird. Ein DataTable-Objekt beschreibt die Spalten, die im SELECT-Statement der Abfrage angegeben sind. Jede Spalte wird dabei als Objekt vom Typ DataColumn behandelt. Um eine einfache Verwaltung und einen einfachen Zugriff auf bestimmte Spalten zu gewährleisten, werden alle Spalten in eine Auflistung der DataTable eingetragen. Über die Eigenschaft Columns der DataTable erhalten Sie Zugriff auf die DataColumnCollection. In ähnlicher Weise ist auch das Ergebnis der Abfrage organisiert. Jeder zurückgelieferte Datensatz wird durch ein Objekt vom Typ DataRow beschrieben. Alle Datenzeilen in einer Tabelle werden von einer Auflistung verwaltet, der DataRowCollection, auf die Sie über die DataTable-Eigenschaft Rows zugreifen können. Eine DataTable hat eine DataColumn- und eine DataRowCollection. Da ein DataSet nicht nur eine, sondern prinzipiell beliebig viele Tabellen enthalten kann, muss auch der Zugriff auf eine bestimmte DataTable im DataSet möglich sein. Wie kaum anders zu erwarten ist, werden auch alle Tabellen in einem DataSet von einer Auflistung organisiert. Diese ist vom Typ DataTableCollection, deren Referenz die Eigenschaft Tables des DataSets liefert. Hier klicken, um das Bild zu vergrößern Abbildung 26.1 Die Struktur eines »DataSets« ### 26.1.3 Zugriff auf eine Tabelle im »DataSet« Wenn ds das DataSet-Objekt beschreibt, genügt eine Anweisung wie die folgende, um auf eine bestimmte Tabelle im DataSet zuzugreifen:
    ds.Tables[2]
Enthält das DataSet mehrere Tabellen, lassen sich die Indizes oft nur schwer einer der Tabellen zuordnen. Wie Sie wissen, weist der SqlDataAdapter per Vorgabe den Tabellen im DataSet ebenfalls Bezeichner (Table, Table1, Table2 usw.) zu. Sowohl die Indizes als auch die Standardbezeichner sind aber wenig geeignet, um den Code gut lesbar zu gestalten. Der SqlDataAdapter unterstützt eine DataTableMappingCollection, um lesbare Tabellennamen abzubilden. Zudem bietet die Überladung der Methode des SqlDataAdapters die Möglichkeit, einer Tabelle einen sprechenden Bezeichner zuzuordnen. Sie sollten eines dieser Angebote nutzen, denn die Anweisung
    ds.Tables["Artikel"]
wird Ihnen später eher hilfreich sein, den eigenen Programmcode zu verstehen, als die Angabe eines nur schlecht zuzuordnenden Index. ### 26.1.4 Zugriff auf die Ergebnisliste Ein DataRow-Objekt stellt den Inhalt eines Datensatzes dar und kann sowohl gelesen als auch geändert werden. Um in einer DataTable von einem Datensatz zum anderen zu navigieren, benutzen Sie Eigenschaft Rows der DataTable, die die Referenz auf das DataRowCollection-Objekt der Tabelle zurückgibt und alle Datensätze enthält, die Ergebnis der Abfrage sind. Die einzelnen DataRows sind über den Index der Auflistung adressierbar. Mit der folgenden Anweisung wird der Verweis auf die dritte Datenzeile in der ersten Tabelle des DataSets der Variablen row zugewiesen:
    DataRow row = ds.Tables["Artikel"].Rows[2];
Eine Datenzeile nur zu referenzieren, ist sicher nicht das von Ihnen verfolgte Ziel. Vielmehr werden Sie daran interessiert sein, den Inhalt einer oder mehrerer Spalten der betreffenden Datenzeile auszuwerten. Dazu veröffentlicht die DataRow einen Indexer, dem Sie entweder den Namen der Spalte, deren Index in der DataColumnCollection der DataTable (die Ordinalposition) oder die Referenz auf die gewünschte Spalte übergeben. Der Rückgabewert ist jeweils vom Typ Object und enthält die Daten der angegebenen Spalte. Häufig ist eine anschließende Konvertierung in den richtigen Datentyp notwendig. Wenn Sie die Überladung einsetzen, die den Spaltenbezeichner erwartet, müssen Sie zwei Ausgangssituationen beachten: Per Vorgabe setzen Sie diejenigen Spaltenbezeichner ein, die auch in der Originaldatenbank bekannt sind. Haben Sie jedoch der DataColumnMappingCollection Spaltenzuordnungen hinzugefügt, müssen Sie diese angeben. Wir wollen uns dies nun an einem Beispiel ansehen.
    // ------------------------------------------------------------------
    // Beispiel: ...\Kapitel 26\ShowDataRows
    // ------------------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        SqlConnection con = new SqlConnection();
        con.ConnectionString = "...";
        SqlCommand cmd = new SqlCommand();
        cmd.Connection = con;
        cmd.CommandText = "SELECT ProductName, UnitPrice " +
                          "FROM Products " +
                          "WHERE UnitsOnOrder > 0";
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(ds, "Artikel");
        // Ausgabe der Ergebnisliste
        DataTable tbl = ds.Tables["Artikel"];
        for (int i = 0; i < tbl.Rows.Count; i++) {
          Console.WriteLine("{0,-35}{1}",
            tbl.Rows[i]["ProductName"], tbl.Rows[i]["UnitPrice"]);
        }
        Console.ReadLine();
      }
    }
Gefragt ist nach allen Artikeln, zu denen aktuell Bestellungen vorliegen. Nach dem Füllen des DataSets wird die Ergebnisliste in einer Schleife durchlaufen. Der Schleifenzähler wird dabei als Index der Datenzeile eingetragen. Damit die einzelnen Anweisungen nicht zu lang werden, wird vor Beginn des Schleifendurchlaufs die DataTable im DataSet in einer Variablen gespeichert.
    DataTable tbl = ds.Tables["Artikel"];
Da alle Datenzeilen von einer Auflistung verwaltet werden, stehen die üblichen Methoden und Eigenschaften zur Verfügung. In diesem Code wird die Eigenschaft Count abgefragt, um festzustellen, wie viele Datenzeilen sich in der Ergebnisliste befinden. Sie können auch statt der for-Schleife eine foreach-Schleife einsetzen. Der folgende Codeausschnitt ersetzt daher vollständig die for-Schleife unseres Beispiels:
    foreach(DataRow row in tbl.Rows)
      Console.WriteLine("{0,-35}{1}", row["ProductName"], row["UnitPrice"]);
### 26.1.5 Dateninformationen in eine XML-Datei schreiben Sie können die Dateninformationen eines DataSets in eine XML-Datei schreiben und später im Bedarfsfall auch wieder laden. Hierzu stehen Ihnen mit WriteXml und ReadXml die passenden Methoden zur Verfügung, die auf die Referenz des DataSet-Objekts aufgerufen werden. Beiden Methoden können Sie als Parameter den Namen der Datei mitgeben, in die die Daten gespeichert bzw. aus der die XML-Daten gelesen werden sollen:
    ds.WriteXml(@"D:\Daten\ContentsOfDataset.xml");
    ...
    ds.ReadXml(@"D:\Daten\ContentsOfDataset.xml");
Der Parameter beschränkt sich nicht nur auf Dateien. Sie können auch einen TextReader, einen Stream oder einen XmlReader angeben. Nachfolgend sehen Sie den Teilausschnitt eines XML-Dokuments, dem die Abfrage
    SELECT ProductID, ProductName FROM Products
zugrunde liegt.
    <?xml version="1.0" standalone="yes"?>
    <NewDataSet>
      <Table>
        <ProductID>1</ProductID>
        <ProductName />
      </Table>
      <Table>
        <ProductID>17</ProductID>
        <ProductName>Alice Mutton</ProductName>
      </Table>
      <Table>
        <ProductID>3</ProductID>
        <ProductName>Aniseed Syrup</ProductName>
      </Table>
      <Table>
        <ProductID>40</ProductID>
        <ProductName>Boston Crab Meat</ProductName>
      </Table>
      <Table>
        <ProductID>60</ProductID>
        <ProductName>Camembert Pierrot</ProductName>
      </Table>
      ...
    </NewDataSet>