32.4 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.
Der Rückgabewert 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 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 und ebenso wenig das Ändern der gelieferten Daten. Damit hat ein SqlDataReader nur eine 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();
32.4.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 32\DataReaderSample
static void Main(string[] args) {
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();
}
Listing 32.6 Daten abrufen mit »ExecuteReader«
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 das 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. 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 über den Index des SqlDataReader-Objekts zugegriffen werden kann:
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]);
Listing 32.7 Die Indizes der Spalten mit »GetOrdinal« abrufen
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));
}
Listing 32.8 Beste Performance beim Datenabruf
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, soweit zugelassen, 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));
}
Listing 32.9 NULL-Inhalte behandeln
32.4.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.
32.4.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
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, die miteinander in einer 1:n-Beziehung stehen.
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 32\MarsSample
static void Main(string[] args) {
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();
}
Listing 32.10 Multiple Active Resultsets (MARS)
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.
32.4.4 Batchabfragen mit »NextResult« durchlaufen
Müssen Sie mehrere Abfragen hintereinander absetzen, können Sie eine Batchabfrage ausführen. Allerdings werden Batchabfragen 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 Batchabfrage zu formulieren, werden die beiden SELECT-Statements innerhalb einer Zeichenfolge durch ein Semikolon getrennt angegeben:
SELECT * FROM Orders;SELECT * FROM Customers
Der Vorteil einer Batchabfrage ist, dass Sie die Methode ExecuteReader nicht zweimal aufrufen und nach dem ersten Aufruf den SqlDataReader schließen müssen. Selbstverständlich sind Batchabfragen nicht nur auf zwei SELECT-Anweisungen beschränkt, es können beliebig viele festlegt werden.
Das von einer Batchabfrage 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 Batchabfragen
Manchmal ist es erforderlich, eine Batchabfrage zu definieren, die sich aus einer oder mehreren Auswahl- und Aktionsabfragen zusammensetzt. Vielleicht möchten Sie eine SELECT-, eine DELETE- und eine UPDATE-Abfrage in einer Batchabfrage 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.
32.4.5 Das Schema eines SqlDataReader-Objekts untersuchen
Haupteinsatz 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.
- Es lässt sich der Name einer bestimmten Spalte 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. Der Member CommandBehavior.SchemaOnly gibt vor, dass die Abfrage nur Spalteninformationen zurückliefert.
SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly);
Auf die 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 Schematabelle 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 Metadateninformationen zu ermitteln.
// Beispiel: ..\Kapitel 32\GetSchemaTableSample
static void Main(string[] args) {
SqlConnection con = new SqlConnection("...");
string strSQL = "SELECT ProductID, ProductName, " +
"UnitsInStock FROM Products";
SqlCommand cmd = new SqlCommand(strSQL, con);
con.Open();
// Schemainformationen einlesen
SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly);
// Schematabelle erstellen
DataTable table = reader.GetSchemaTable();
// Ausgabe der Schematabelle
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();
}
}
Listing 32.11 Abrufen von Metadateninformationen
Die resultierende Konsolenausgabe sehen Sie in Abbildung 32.1.
Abbildung 32.1 Ausgabe des Beispiels »GetSchemaTableSample«
Ermitteln des Bezeichners einer Spalte
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));
Ermitteln des Index einer Spalte
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"));
Ermitteln des Datentyps einer Spalte
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));
Beide Methoden erwarten den Ordinalwert der betreffenden Spalte.
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.