## 24.3 Das »SqlDataReader«-Objekt Mit der Methode ExecuteNonQuery des SqlCommand-Objekts können Sie Datensätze in der Originaldatenbank manipulieren und mit ExecuteScalar ein einzelnes Abfrageergebnis abrufen. Möchte man sich die Datensätze einer Tabelle in einer Anwendung anzeigen lassen, wird die Methode ExecuteReader des SqlCommand-Objekts aufgerufen.
    public SqlDataReader ExecuteReader()
Das Ergebnis des Methodenaufrufs ist ein Objekt vom Typ SqlDataReader. Dieses ähnelt den anderen Reader-Objekten des .NET-Frameworks (TextReader, StreamReader usw.). Ein SqlDataReader-Objekt liest aus einer Ergebnisliste, die schreibgeschützt ist und sich in einem serverseitigen Puffer befindet, also auf der Seite der Datenbank. Sie sollten daher den Ratschlag beherzigen, die Ergebnisliste so schnell wie möglich abzurufen, damit die beanspruchten Ressourcen wieder freigegeben werden. In einer von einem SqlDataReader-Objekt bereitgestellten Datensatzliste kann immer nur zum folgenden Datensatz navigiert werden. Eine beliebige Navigation in der Ergebnisliste ist nicht möglich – ebensowenig wie das Ändern der gelieferten Daten. Damit hat ein SqlDataReader nur sehr eingeschränkte Funktionalität. Dieses Manko wird andererseits durch die sehr gute Performance wettgemacht – das ist die Stärke des SqlDataReaders. Das Erzeugen eines DataReader-Objekts funktioniert nur über den Aufruf der Methode ExecuteReader auf die SqlCommand-Referenz, denn die Klasse SqlDataReader weist keinen öffentlichen Konstruktor auf.
    SqlDataReader reader = cmd.ExecuteReader();
### 24.3.1 Datensätze einlesen Im folgenden Beispielprogramm wird ein SqlDataReader dazu benutzt, alle Artikel zusammen mit ihrem Preis nach dem Preis sortiert auszugeben.
    // ------------------------------------------------------------------
    // Beispiel: ...\Kapitel 24\DataReaderSample
    // ------------------------------------------------------------------
    SqlConnection con = new SqlConnection("...");
    string strSQL = "SELECT ProductName, Unitprice " +
                    "FROM Products " +
                    "ORDER BY[UnitPrice]";
    SqlCommand cmd = new SqlCommand(strSQL, con);
    con.Open();
    SqlDataReader reader = cmd.ExecuteReader();
    while (reader.Read())
      Console.WriteLine("{0,-35}{1}", reader["ProductName"], reader["UnitPrice"]);
    reader.Close();
    con.Close();
Zuerst wird die Zeichenfolge des SELECT-Statements definiert, die im nächsten Schritt zusammen mit der Referenz auf das SqlConnection-Objekt dazu dient, ein SqlCommand-Objekt zu erzeugen. Auf dem SqlCommand-Objekt wird nach dem Öffnen der Verbindung die Methode ExecuteReader ausgeführt. Der Rückgabewert wird in der Objektvariablen reader vom Typ SqlDataReader gespeichert. SqlDataReader liefert alle Datensätze, die der Reihe nach durchlaufen werden müssen. Um auf die Datensätze zuzugreifen, gibt es nur eine Möglichkeit: die Methode Read des DataReader-Objekts.
    public override bool Read()
Jeder Aufruf von Read legt die Position des SqlDataReaders neu fest. Die Ausgangsposition vor dem ersten Read-Aufruf ist vor dem ersten Datensatz. Nach dem Aufruf von Read ist der Rückgabewert true, falls noch eine weitere Datenzeile abgerufen werden kann. Ist der Rückgabewert false, ist kein weiterer Datensatz mehr verfügbar. Damit eignet sich Read, um die Datensatzliste in einer while-Schleife zu durchlaufen. Beabsichtigen Sie, wiederholt die Datensätze im SqlDataReader auszuwerten, müssen Sie die Methode ExecuteReader erneut aufrufen. #### Auswerten der einzelnen Spalten in DataReader Mit Read wird die Position des SqlDataReaders auf die folgende Datenzeile verschoben. In unserem Beispiel hat jede Datenzeile zwei Feldinformationen, nämlich die der Spalten ProductName und UnitPrice. Die einzelnen Spalten einer Abfrage werden in einer Auflistung geführt, auf die Sie über den Index des SqlDataReader-Objekts zugreifen können:
    reader[0]
Sie können auch den Spaltenbezeichner angeben, also:
    reader["ProductName"]
Diese Angaben sind gleichwertig. Bezüglich der Performance gibt es jedoch einen Unterschied. Geben Sie den Spaltennamen an, muss das SqlDataReader-Objekt zuerst die Spalte in der Auflistung suchen – und das bei jeder Datenzeile.
    while (reader.Read())
      Console.WriteLine("{0,-35}{1}", reader["ProductName"], reader["UnitPrice"]);
Um die Leistung Ihrer Anwendung zu steigern, sollten Sie daher den Index der betreffenden Spalte angeben:
    while(reader.Read())
      Console.WriteLine("{0,-35}{1}",reader[0], reader[1]);
Ist Ihnen nur der Spaltenbezeichner, jedoch nicht der dazugehörige Index bekannt, haben Sie mit der Methode GetOrdinal der Klasse DataReader unter Angabe des Spaltenbezeichners die Möglichkeit, vor dem Aufruf von Read den Index zu ermitteln:
    int intName = reader.GetOrdinal("ProductName");
    int intPrice = reader.GetOrdinal("UnitPrice");
    while(reader.Read())
       Console.WriteLine("{0,-20}{1,-20}{2,-20}",
                     reader[intName], reader[intPrice]);
#### Spalten mit den typspezifischen Methoden abrufen Mit dem Indexer der Methode ExecuteReader werden die Spaltenwerte vom Typ Object zurückgegeben. Das hat Leistungseinbußen zur Folge, weil der tatsächliche Typ erst in Object umgewandelt werden muss. Anstatt über den Indexer die Daten auszuwerten, können Sie auch eine der vielen GetXxx-Methoden anwenden, die für die wichtigsten .NET-Datentypen bereitgestellt werden, beispielsweise GetString, GetInt32 oder GetBoolean. Sie müssen nur die passende Methode aus einer (langen) Liste auswählen und beim Aufruf die Ordinalzahl der entsprechenden Spalte übergeben. Wählen Sie eine nicht typgerechte Methode aus, kommt es zur Ausnahme InvalidCastException.
    SqlDataReader reader = cmd.ExecuteReader();
    while (reader.Read()) {
      Console.WriteLine(reader.GetString(0));
      Console.WriteLine(reader.GetString(1));
    }
Auch wenn der Programmieraufwand größer ist, zur Laufzeit werden Sie dafür mit einem besseren Leistungsverhalten belohnt. #### NULL-Werte behandeln Spalten einer Tabelle können, sofern sie zugelassen sind, NULL-Werte enthalten. In der Tabelle Products betrifft das zum Beispiel die Spalte UnitPrice. Rufen Sie die Datenwerte über eine der typisierten Methoden ab und ist der Spaltenwert NULL, führt das zu einer Ausnahme. Um diesem Problem zu begegnen, können Sie mit der Methode IsDBNull des SqlDataReaders prüfen, ob die entsprechende Spalte einen gültigen Wert oder NULL enthält.
    SqlDataReader reader = cmd.ExecuteReader();
    while (reader.Read()) {
      Console.WriteLine(reader.GetString(0));
      if(! reader.IsDBNull(1))
        Console.WriteLine(reader.GetString(1));
    }
### 24.3.2 Schließen des »SqlDataReader«-Objekts Der SqlDataReader blockiert standardmäßig das SqlConnection-Objekt. Solange SqlDataReader durch den Aufruf von ExecuteReader geöffnet ist, können keine anderen Aktionen auf Basis der Verbindung durchgeführt werden, auch nicht das Öffnen eines zweiten SqlDataReader-Objekts. Daher sollte die Sperre so schnell wie möglich mit
    reader.Close();
aufgehoben werden. ### 24.3.3 MARS (Multiple Active Resultsets) Der SQL Server hat ein Feature, das es gestattet, mehrere Anforderungen auf einer Verbindung auszuführen. Damit wird eine Verbindung nicht mehr blockiert, wenn diese einem geöffneten SqlDataReader zugeordnet ist. Diese Technik von SQL Server wird als Multiple Active Resultsets, kurz MARS, bezeichnet. MARS ist per Vorgabe deaktiviert und muss zuvor aktiviert werden, um es zu nutzen. Sie aktivieren MARS entweder durch Ergänzen der Verbindungszeichenfolge um
    MultipleActiveResultSets=True;
oder durch Setzen der gleichnamigen Eigenschaft im SqlConnectionStringBuilder. MARS bietet sich an, wenn auf Basis der Ergebnismenge eines SqlDataReaders eine untergeordnete Tabellenabfrage gestartet werden soll. Das folgende Beispiel demonstriert dies. Dazu soll zu jedem Artikel auch der dazugehörige Lieferant ausgegeben werden. Damit stehen die beiden Tabellen Products und Suppliers im Mittelpunkt unserer Betrachtung. Sie stehen in einer 1:n-Beziehung zueinander. Für jede Tabelle werden ein SqlCommand-Objekt sowie ein SqlDataReader-Objekt benötigt. Das erste DataReader-Objekt durchläuft die Artikeltabelle. Mit der in der Spalte SupplierID enthaltenen ID des Lieferanten wird eine untergeordnete Ergebnisliste – die der Tabelle Suppliers – durchlaufen. Hier wird die ID des Lieferanten gesucht und dessen Firmenbezeichnung zusätzlich zum Artikel ausgegeben.
    // ------------------------------------------------------------------
    // Beispiel: ...\Kapitel 24\MarsSample
    // ------------------------------------------------------------------
    SqlConnection con = new SqlConnection(
                        " ...;MultipleActiveResultSets=true");
    string textProducts =
           "SELECT ProductName, UnitsInStock, SupplierID " +
           "FROM Products";
    string textSupplier =
           "SELECT CompanyName " +
           "FROM Suppliers " +
           "WHERE SupplierID=@SupplierID";
    // SqlCommand-Objekte erzeugen
    SqlCommand cmdProducts, cmdSupplier;
    cmdProducts = new SqlCommand(textProducts, con);
    cmdSupplier = new SqlCommand(textSupplier, con);
    SqlParameter param = cmdSupplier.Parameters.Add(
                        "@SupplierID", SqlDbType.Int);
    // Verbindung öffnen
    con.Open();
    SqlDataReader readerProducts = cmdProducts.ExecuteReader();
    // Einlesen und Ausgabe der Datenzeilen an der Konsole
    while (readerProducts.Read()) {
      Console.Write("{0,-35}{1,-6}",
      readerProducts["ProductName"], readerProducts["UnitsInStock"]);
      param.Value = readerProducts["SupplierID"];
      SqlDataReader readerSupplier = cmdSupplier.ExecuteReader();
      while (readerSupplier.Read()) {
        Console.WriteLine(readerSupplier["Companyname"]);
      }
      readerSupplier.Close();
      Console.WriteLine(new string('-', 80));
    }
    readerProducts.Close();
    con.Close();
Der Vorteil von MARS wird in diesem Beispiel deutlich: Es genügt eine Verbindung, um mit den beiden SqlDataReader-Objekten zu operieren. Selbstverständlich kann die dem Programmcode zugrunde liegende Forderung auch ohne die Nutzung von MARS erfüllt werden. Allerdings wären dazu zwei Verbindungen notwendig, die einen gewissen Overhead verursachen. Ein SQL-Statement kann eine parametrisierte Abfrage beschreiben. SqlCommand-Objekte unterstützen parametrisierte Abfragen durch eine Parameterliste. Weiter unten werden wir uns den parametrisierten Abfragen im Detail widmen. ### 24.3.4 Batch-Abfragen mit »NextResult« durchlaufen Wenn Sie mehrere Abfragen hintereinander absetzen müssen, können Sie eine Batch-Abfrage ausführen. Allerdings werden Batch-Abfragen nicht von allen Datenbanken unterstützt, der SQL Server gehört aber dazu. Nehmen wir an, Sie benötigen alle Datensätze sowohl der Tabelle Orders als auch der Tabelle Customers. Um eine syntaktisch korrekte Batch-Abfrage zu formulieren, werden die beiden SELECT-Statements innerhalb einer Zeichenfolge durch ein Semikolon getrennt angegeben:
    SELECT * FROM Orders;SELECT * FROM Customers
Der Vorteil einer Batch-Abfrage ist, dass Sie die Methode ExecuteReader nicht zweimal aufrufen und nach dem ersten Aufruf den SqlDataReader schließen müssen. Selbstverständlich sind Batch-Abfragen nicht nur auf zwei SELECT-Anweisungen beschränkt, es können beliebig viele festlegt werden. Das von einer Batch-Abfrage gefüllte SqlDataReader-Objekt enthält nach dem Aufruf der ExecuteReader-Methode mehrere Ergebnislisten. Um zwischen diesen zu wechseln, verwendet man die Methode NextResult. Die Funktionsweise ähnelt der von Read. Sie liefert true, wenn eine Datensatzliste durchlaufen wurde und sich noch eine weitere im DataReader befindet.
    do {
      while(dr.Read())
        Console.WriteLine("{0}{1}{2}", dr[0], dr[1], dr[2]);
      Console.WriteLine();
    } while(dr.NextResult());
Die Überprüfung mit NextResult muss in jedem Fall im Schleifenfuß erfolgen. Eine Prüfung im Schleifenkopf hätte zur Folge, dass die erste Datensatzliste überhaupt nicht durchlaufen wird. #### Gemischte Batch-Abfragen Manchmal ist es erforderlich, eine Batch-Abfrage zu definieren, die sich aus einer oder mehreren Auswahl- und Aktionsabfragen zusammensetzt. Vielleicht möchten Sie eine SELECT-, eine DELETE- sowie eine UPDATE-Abfrage in einer Batch-Abfrage behandeln? Kein Problem. Erstellen Sie eine solche Abfrage genauso wie jede andere, also beispielsweise mit:
    SELECT * FROM Products;
    UPDATE Products SET ProductName='Senfsauce' WHERE ProductName='Chai'
In dieser Weise gemischte Abfragen rufen Sie ebenfalls mit der Methode ExecuteReader auf. ### 24.3.5 Schema eines »SqlDataReader«-Objekts untersuchen Das Haupteinsatzgebiet des SqlDataReader-Objekts ist sicherlich die Abfrage von Daten. Darüber hinaus weist dieser Typ aber auch weitere Fähigkeiten auf. Im Einzelnen handelt es sich dabei um die folgenden: Abrufen der Schemadaten der Spalten mit der Methode GetSchemaTable. Die gelieferten Informationen beschreiben unter anderem, ob eine Spalte Primärschlüsselspalte ist, ob sie schreibgeschützt ist, ob der Spaltenwert innerhalb der Tabelle eindeutig ist oder ob die Spalte einen NULL-Wert zulässt. Der Name einer bestimmten Spalte lässt sich mit der Methode GetName ermitteln. Die Ordinalposition einer Spalte lässt sich anhand des Spaltenbezeichners ermitteln. Die Methode GetOrdinal liefert den entsprechenden Index. #### Die Methode »GetSchemaTable« Der Rückgabetyp der Methode GetSchemaTable ist ein Objekt vom Typ DataTable. An dieser Stelle wollen wir diesen Typ nicht weiter betrachten. Es genügt am Anfang, zu wissen, dass sich ein DataTable-Objekt aus Datenzeilen und Spalten zusammensetzt, ähnlich einer Excel-Tabelle. Dieser Tabelle liegt ein SELECT-Statement zugrunde, das mit ExecuteReader gegen die Datenbank ausgeführt wird. ExecuteReader haben wir bisher nur parameterlos kennengelernt; es akzeptiert aber auch einen Übergabeparameter vom Typ der Enumeration CommandBehavior. Das Member CommandBehavior.SchemaOnly gibt vor, dass die Abfrage nur Spalteninformationen zurückliefert.
    SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly);
Auf der SqlDataReader-Referenz kann man anschließend die Methode GetSchemaTable aufrufen. Das ist vorteilhaft, denn die übermittelten Metadaten werden nun für alle Spalten, die im SELECT-Statement angegeben sind, in der Tabelle eingetragen. Dabei wird für jede im SELECT-Statement angegebene Spalte der Originaltabelle eine Datenzeile geschrieben.
    DataTable table = reader.GetSchemaTable();
Die Spalten in der Schema-Tabelle werden durch festgelegte Bezeichner in einer bestimmten Reihenfolge ausgegeben. Die erste Spalte ist immer ColumnName, die zweite ColumnOrdinal, die dritte ColumnSize. Insgesamt werden 28 Spalten zur Auswertung bereitgestellt. Falls Sie nähere Informationen benötigen, sehen Sie sich in der .NET-Dokumentation die Hilfe zur Methode GetSchemaTable an. Das folgende Beispiel untersucht die Spalten ProductID, ProductName und UnitsInStock der Tabelle Products. Es soll dabei genügen, nur die ersten vier Metainformationen zu ermitteln.
    // ------------------------------------------------------------------
    // Beispiel: ...\Kapitel 24\GetSchemaTableSample
    // ------------------------------------------------------------------
    SqlConnection con = new SqlConnection("...");
    string strSQL = "SELECT ProductID, ProductName, " +
                    "UnitsInStock FROM Products";
    SqlCommand cmd = new SqlCommand(strSQL, con);
    con.Open();
    // Schema-Informationen einlesen
    SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly);
    // Schema-Tabelle erstellen
    DataTable table = reader.GetSchemaTable();
    // Ausgabe der Schema-Tabelle
    for(int col = 0; col < 4; col++)
      Console.Write("{0,-15}", table.Columns[col].ColumnName);
    Console.WriteLine("\n" + new string('-', 60));
    for(int i = 0; i < table.Rows.Count; i++) {
      for(int j = 0; j < 4; j++) {
        Console.Write("{0,-15}", table.Rows[i][j]);
      }
      Console.WriteLine();
    }
Die resultierende Konsolenausgabe sehen Sie in Abbildung 24.1. Hier klicken, um das Bild zu vergrößern Abbildung 24.1 Ausgabe des Beispiels »GetSchemaTableSample« #### Bezeichner einer Spalte ermitteln Möchten Sie den Bezeichner einer bestimmten Spalte in der Ergebnisliste ermitteln, rufen Sie die Methode GetName des SqlDataReader-Objekts auf und übergeben dabei den Index der betreffenden Spalte in der Ergebnisliste. Der Rückgabewert ist eine Zeichenfolge.
    Console.WriteLine(reader.GetName(3));
#### Index einer Spalte ermitteln Ist der Index einer namentlich bekannten Spalte in der Ergebnisliste nicht bekannt, können Sie diesen mit GetOrdinal unter Angabe des Spaltenbezeichners ermitteln.
    Console.WriteLine(reader.GetOrdinal("UnitPrice"));
#### Datentyp einer Spalte ermitteln Sie können sowohl den .NET-Datentyp als auch den Datenbank-Datentyp eines bestimmten Feldes im SqlDataReader abfragen. Interessieren Sie sich für den .NET-Datentyp, rufen Sie die Methode GetFieldType des DataReaders auf, ansonsten GetDataTypeName.
    Console.WriteLine(reader.GetFieldType(4));
    Console.WriteLine(reader.GetDataTypeName(0));