24.2 SqlDataReader
Mit der Methode ExecuteNonQuery des DbCommand-Objekts können Sie Datensätze in der Originaldatenbank manipulieren, und mit ExecuteScalar können Sie ein einzelnes Abfrageergebnis abrufen. Möchte man sich die Datensätze einer Tabelle in einer Anwendung anzeigen lassen, ruft man die Methode ExecuteReader des DbCommand-Objekts auf.
Public Function ExecuteReader() As DbDataReader |
Das Ergebnis des Methodenaufrufs ist ein Objekt vom Typ DbDataReader. Dieses ähnelt den anderen Reader-Objekten des .NET-Frameworks (TextReader, StreamReader usw.). Ein DbDataReader-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.
Beabsichtigen Sie, an den Datensätzen Änderungen vorzunehmen, ist der DbDataReader völlig ungeeignet. In einer von einem DbDataReader-Objekt bereitgestellten Datensatzliste kann immer nur zum folgenden Datensatz navigiert werden. Damit hat ein DbDataReader nur sehr eingeschränkte Möglichkeiten, aber eine sehr gute Performance. Er bietet sich insbesondere dann an, wenn Komponenten wie List- oder Comboboxen gefüllt werden sollen.
Das Erzeugen eines DataReader-Objekts funktioniert nur, indem man die Fabrikmethode ExecuteReader einer DbCommand-Referenz aufruft, denn keine providerspezifische DataReader-Klasse hat einen öffentlichen Konstruktor.
Dim dr As DbDataReader = cmd.ExecuteReader()
24.2.1 Das SQL-Kommando SELECT
Das wichtigste SQL-Kommando zum Auslesen von Daten aus der Datenbank ist SELECT. Aufgrund der zentralen Stellung ist die Syntax recht komplex. Sie wird hier nur als Referenz wiedergegeben und soll nicht näher erläutert werden. Die Länge soll Ihnen einen Eindruck von der Mächtigkeit des Befehls geben. Optionale Teile sind in eckige Klammern gesetzt und Alternativen durch | getrennt. Kursive Namen passen Sie Ihren Bedürfnissen an. Nicht alle Datenbankserver unterstützen den kompletten Funktionsumfang, andere kennen zusätzliche Varianten. Häufig dürfen Spalten berechnet sein.
[WITH Tabelle(Spalte(n)) AS (Select)]
SELECT
[DISTINCT|All][TOP Anzahl [PERCENT] [WITH TIES]] Spalte(n)
[INTO NeueTabelle]
FROM Tabelle(n) | join(s) | OPENROWSET(...) | OPENQUERY(...) | OPENXML(...)
[WHERE SelektionsBedingung]
[[GROUP BY Spalte(n)] [HAVING GruppenSelektionsBedingung]]
[ORDER BY Spalte(n)|SpaltenNummer(n) [ASC|DESC]]
[[COMPUTE SkalarFunktion] [BY SpaltenAusOrderBy]]
[FOR BROWSE|XMLOptionen] |
24.2.2 Datensätze einlesen
Im folgenden Beispielprogramm wird ein DbDataReader dazu benutzt, alle Artikel zusammen mit ihrem Preis nach dem Preis sortiert auszugeben.
'...\ADO\Datenbankabfragen\DataReader.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module DataReader Sub Test() Dim con As DbConnection = New SqlConnection("Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi") Dim cmd As DbCommand = New SqlCommand( _ "SELECT ProductName, Unitprice FROM Products ORDER BY UnitPrice") cmd.Connection = con Try con.Open() Dim dr As DbDataReader = cmd.ExecuteReader() ' Einlesen und ausgeben der Datenzeilen While (dr.Read()) Console.WriteLine("{0,-35}{1}",dr("ProductName"),dr("UnitPrice")) End While dr.Close() Catch ex As Exception Console.WriteLine("Fehler: {0}", ex.Message) Finally con.Close() End Try Console.ReadLine() End Sub End Module End Namespace
Hinweis |
Ein DbDataReader kann nur bei geöffneter Verbindung erzeugt werden. |
Nachdem das DbCommand-Objekt mit der SELECT-Abfrage an das DbConnection-Objekt gebunden wurde, wird die Verbindung geöffnet. Danach erzeugt das Kommando mit ExecuteReader einen Cursor auf dem Datenbankserver zum Auslesen. Eine Referenz auf den Cursor wird in der Variablen dr vom Typ DbDataReader gespeichert.
DbDataReader liefert alle Datensätze, die der Reihe nach durchlaufen werden müssen. Nur mit der Methode Read des DataReader-Objekts kann auf die Datensätze zugegriffen werden.
Public MustOverride Function Read() As Boolean |
Jeder Aufruf von Read legt die Position des DbDataReaders neu fest. Die Ausgangsposition vor dem ersten Read-Aufruf befindet sich 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.
Für jeden Durchlauf durch die Daten müssen Sie ExecuteReader erneut aufrufen.
Spaltenindizierung
Mit Read wird die Position des DbDataReaders 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 DbDataReader-Objekts zugegriffen werden kann.
dr(0)
Sie können aber auch den Spaltenbezeichner angeben, also:
dr("ProductName")
Bezüglich der Performance gibt es einen Unterschied. Geben Sie den Spaltennamen an, muss das DbDataReader-Objekt zuerst die Spalte suchen, und das bei jeder Datenzeile.
While (dr.Read()) Console.WriteLine("{0,-35}{1}", dr("ProductName"), dr("UnitPrice")) End While
Das Programm läuft schneller, wenn Sie den Index der betreffenden Spalte angeben:
While (dr.Read()) Console.WriteLine("{0,-35}{1}", dr(0), dr(1)) End While
Ist Ihnen nur der Spaltenbezeichner, jedoch nicht der dazugehörige Index bekannt, haben Sie mit der Methode GetOrdinal der Klasse DbDataReader unter Angabe des Spaltenbezeichners die Möglichkeit, vor dem Aufruf von Read den Index zu ermitteln:
Dim intLname As Integer = dr.GetOrdinal("au_lname")
Dim intFname As Integer = dr.GetOrdinal("au_fname")
Dim intCity As Integer = dr.GetOrdinal("city")
While (dr.Read())
Console.WriteLine("{0,-20}{1,-20}{2,-20}", _
dr(intLname), dr(intFname), dr(intCity))
End While
Typspezifische Auslesemethoden
Der Indexer der Methode ExecuteReader gibt die Spaltenwerte als Object zurück. 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. Wenn Sie eine nicht typgerechte Methode auswählen, kommt es zur Ausnahme InvalidCastException.
Dim dr As DbDataReader = cmd.ExecuteReader() While (dr.Read()) Console.WriteLine("{0,-35}{1}", dr.GetString(0), dr.GetDecimal(1)) End While
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 NULL-Werte enthalten, soweit sie für die jeweilige Spalte zugelassen sind. In der Tabelle Products betrifft das zum Beispiel die Spalte UnitPrice. Wenn Sie die Datenwerte über eine der typisierten Methoden abrufen und der Spaltenwert NULL ist, führt das zu einer Ausnahme.
Daher sollten Sie mit der Methode IsDBNull des DbDataReaders prüfen, ob die entsprechende Spalte einen gültigen Wert oder NULL enthält.
Dim dr As DbDataReader = cmd.ExecuteReader()
While (dr.Read())
Console.WriteLine("{0,-35}{1}", _
If(dr.IsDBNull(0), "-", dr.GetString(0)), dr.GetDecimal(1))
End While
In diesem Codefragment wird demnach geprüft, ob die Spalte mit dem Index 0 den Inhalt Nothing hat. Wenn nicht, wird der Wert über GetString abgerufen.
24.2.3 DataReader schließen
Jeder DbDataReader blockiert standardmäßig das DbConnection-Objekt. Solange DbDataReader 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 DbDataReader-Objekts. Daher sollte die Sperre so schnell wie möglich mit
dr.Close()
aufgehoben werden.
24.2.4 MARS (Multiple Active Resultsets)
Mit SQL Server 2005 wurde ein Feature eingeführt, 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 Neuerung in der aktuellen Version 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 erreichen das entweder, indem Sie die Verbindungszeichenfolge um
MultipleActiveResultSets=True;
ergänzen oder indem Sie die gleichnamige Eigenschaft im SqlConnectionStringBuilder setzen.
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. 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, nämlich die der Tabelle Suppliers, durchlaufen. Hier wird die ID des Lieferanten gesucht und dessen Firmenbezeichnung zusätzlich zum Artikel ausgegeben.
'...\ADO\Datenbankabfragen\MARS.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module MARS Sub Test() Dim con As DbConnection = New SqlConnection("Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi;" & _ "MultipleActiveResultSets=true") Dim cmdProducts As DbCommand = New SqlCommand( _ "SELECT ProductName, UnitsInStock, SupplierID FROM Products") Dim cmdSupplier As DbCommand = New SqlCommand( _ "SELECT CompanyName FROM Suppliers WHERE SupplierID=@SupplierID") cmdProducts.Connection = con : cmdSupplier.Connection = con Dim param As DbParameter = _ New SqlParameter("@SupplierID", SqlDbType.Int) cmdSupplier.Parameters.Add(param) con.Open() Dim drProducts As DbDataReader = cmdProducts.ExecuteReader() While (drProducts.Read()) Console.Write("{0,-35}{1,-6}", _ drProducts("ProductName"), drProducts("UnitsInStock")) param.Value = drProducts("SupplierID") Dim drSupplier As DbDataReader = cmdSupplier.ExecuteReader() While (drSupplier.Read()) Console.WriteLine(drSupplier("Companyname")) End While drSupplier.Close() End While drProducts.Close() con.Close() Console.ReadLine() End Sub End Module End Namespace
Der Vorteil von MARS wird in diesem Beispiel deutlich: Es genügt eine Verbindung, um mit den zwei DbDataReader-Objekten zu operieren. Selbstverständlich kann die dem Programmcode zugrunde liegende Aufgabe auch ohne die Nutzung von MARS erfüllt werden. Allerdings wären dazu zwei Verbindungen notwendig, die einen gewissen Overhead verursachen. Die SQL-Statements sind als parametrisierte Abfragen ausgebildet. DbCommand-Objekte unterstützen diese durch eine spezielle Parameterauflistung. Weiter unten werden wir uns den parametrisierten Abfragen im Detail widmen.
24.2.5 Batch-Abfragen mit NextResult durchlaufen durchlaufen
Bei einigen Datenbanken, zum Beispiel SQL Server, können Sie mehrere Abfragen hintereinander in einer Batch-Abfrage bündeln.
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 generieren, formulieren Sie die beiden SELECT-Statements und trennen diese durch ein Semikolon:
SELECT * FROM Orders;SELECT * FROM Customers
Der Vorteil einer Batch-Abfrage ist, dass Sie nicht mehrfach die Methode ExecuteReader aufrufen und zwischendurch den DataReader schließen müssen. Selbstverständlich können Sie in Batch-Abfragen mehr als zwei SELECT-Anweisungen formulieren.
Das von einer Batch-Abfrage gefüllte DbDataReader-Objekt enthält nach dem Aufruf der ExecuteReader-Methode mehrere Ergebnislisten. Um zwischen diesen zu wechseln, verwendet man die Methode NextResult. Sie arbeitet ähnlich wie Read und 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))
End While
Loop While dr.NextResult()
Die Überprüfung mit NextResult muss in jedem Fall im Schleifenfuß erfolgen, da sie zur nächsten Datensatzliste springt und dann ein Ergebnis liefert. 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'
Solche gemischten Abfragen rufen Sie ebenfalls mit der Methode ExecuteReader auf.
24.2.6 Informationen über eine Tabelle
Meistens wird das DbDataReader-Objekt sicherlich für die Abfrage von Daten benutzt. Darüber hinaus ist es aber auch möglich, Metadaten abzufragen. Das sind Informationen über die einzelnen Spalten. Im Einzelnen handelt es sich dabei um folgende Fähigkeiten:
- 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.
GetSchemaTable
Der Rückgabetyp der Methode GetSchemaTable ist ein Objekt vom Typ DataTable. An dieser Stelle wollen wir diesen Typ nicht weiter betrachten, denn das folgt später noch detailliert. Es genügt am Anfang zu wissen, dass sich ein DataTable-Objekt aus Datenzeilen und Spalten zusammensetzt, ähnlich einer Excel-Tabelle.
Dieser Tabelle liegt eine SELECT-Abfrage zugrunde, die mit ExecuteReader gegen die Datenbank ausgeführt wird. Dazu wird ExecuteReader ein Parameter vom Typ der Enumeration CommandBehavior übergeben. Der Wert CommandBehavior.SchemaOnly sorgt dafür, dass die Abfrage nur Spalteninformationen zurückliefert.
Dim dr As DbDataReader = cmd.ExecuteReader(CommandBehavior.SchemaOnly)
Auf der DbDataReader-Referenz rufen wir anschließend die Methode GetSchemaTable auf. 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.
Dim table As DataTable = dr.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 Metainformationen zu ermitteln.
'...\ADO\Datenbankabfragen\GetSchemaTable.vb |
Option Strict On Imports System.Data.Common, System.Data.SqlClient Namespace ADO Module GetSchemaTable Sub Test() Dim con As DbConnection = New SqlConnection("Data Source=.;" & _ "Initial Catalog=Northwind;Integrated Security=sspi") Dim cmd As DbCommand = New SqlCommand( _ "SELECT ProductID, ProductName, UnitsInStock FROM Products") cmd.Connection = con con.Open() Dim dr As DbDataReader = _ cmd.ExecuteReader(CommandBehavior.SchemaOnly) Dim table As DataTable = dr.GetSchemaTable() For i As Integer = –1 To table.Rows.Count – 1 For j As Integer = 0 To 3 Console.Write("{0,-15}", _ If(i < 0, table.Columns(j).ColumnName, table.Rows(i)(j))) Next j Console.WriteLine() Next i dr.Close() con.Close() Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt die ermittelten Metainformationen:
ColumnName ColumnOrdinal ColumnSize NumericPrecision ProductID 0 4 10 ProductName 1 40 255 UnitsInStock 2 2 5
Bezeichner einer Spalte
Möchten Sie den Bezeichner einer bestimmten Spalte in der Ergebnisliste ermitteln, rufen Sie die Methode GetName des DbDataReader-Objekts auf und übergeben dabei den Index der betreffenden Spalte in der Ergebnisliste. Der Rückgabewert ist eine Zeichenfolge.
Console.WriteLine(dr.GetName(3))
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(dr.GetOrdinal("UnitPrice"))
Datentyp einer Spalte
Sie können sowohl den .NET-Datentyp als auch den Datenbank-Datentyp eines bestimmten Feldes im DbDataReader abfragen. Wenn Sie sich für den .NET-Datentyp interessieren, rufen Sie die Methode GetFieldType des DataReaders auf, ansonsten GetDataTypeName.
Console.WriteLine(dr.GetFieldType(4)) Console.WriteLine(dr.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.