Pobieranie danych przy użyciu klasy DataReader

SqlDataReader odczytuje sekwencję wyników tylko do przodu z bazy danych SQL. Nie ładuje pełnego wyniku do DataSet, więc nadaje się do przetwarzania wierszy w momencie ich pojawiania się.

Przykłady w tym artykule zakładają, że otwarte połączenie SqlConnection o nazwie connection wskazuje na przykładową bazę danych AdventureWorks.

Przykłady w języku C# wymagają using Microsoft.Data.SqlClient;. Przykłady, które używają SqlDbType lub CommandBehavior również wymagają using System.Data;.

Odczytaj wiersze

Podstawowy workflow czytelnika wygląda następująco:

  1. Wykonaj polecenie z ExecuteReader lub ExecuteReaderAsync.
  2. Ustal numery porządkowe kolumn dla bieżącego wyniku.
  3. Wywołaj Read raz dla każdego wiersza.
  4. Sprawdź kolumny nullable za pomocą IsDBNull przed wywołaniem typowanego gettera.
  5. Zwolnij obiekt reader przed ponownym użyciem połączenia lub odczytem parametrów wyjściowych.

Ten przykład buforuje numery porządkowe kolumn, używa typowanych getterów i sprawdza kolumnę nullowalną Color:

using var command = new SqlCommand("""
    SELECT ProductID, Name, Color
    FROM Production.Product
    WHERE ProductSubcategoryID = @subcategoryId
    ORDER BY ProductID;
    """, connection);
command.Parameters.Add("@subcategoryId", SqlDbType.Int).Value = 2;

using SqlDataReader reader = command.ExecuteReader();
int idOrdinal = reader.GetOrdinal("ProductID");
int nameOrdinal = reader.GetOrdinal("Name");
int colorOrdinal = reader.GetOrdinal("Color");

while (reader.Read())
{
    int id = reader.GetInt32(idOrdinal);
    string name = reader.GetString(nameOrdinal);
    string? color = reader.IsDBNull(colorOrdinal)
        ? null
        : reader.GetString(colorOrdinal);

    Console.WriteLine($"{id} | {name} | {color ?? "(no color)"}");
}

Wywołanie GetString dla wartości NULL zgłasza SqlNullValueException, więc najpierw sprawdź IsDBNull.

HasRows raportuje, czy aktualny wynik zawiera wiersze, ale zazwyczaj można wywołać Read bezpośrednio. Dla wyniku pustego pierwsze wywołanie Read zwraca false:

using var command = new SqlCommand("""
    SELECT ProductID
    FROM Production.Product
    WHERE ProductID = @productId;
    """, connection);
command.Parameters.Add("@productId", SqlDbType.Int).Value = -1;

using SqlDataReader reader = command.ExecuteReader();
Console.WriteLine($"HasRows: {reader.HasRows}");
Console.WriteLine($"First Read: {reader.Read()}");

Dla dużych wartości uruchom za pomocą CommandBehavior.SequentialAccess i użyj GetStream, GetTextReader lub GetXmlReader. Więcej informacji można znaleźć w artykule Wsparcie streamingowe SqlClient.

Zamknij DataReader

Zwolnij obiekt czytnika zaraz po zakończeniu przetwarzania. await using Instrukcja lub using zamyka ją nawet wtedy, gdy przetwarzanie wiersza zgłasza wyjątek.

Parametry wyjściowe, parametry wejściowe/wyjściowe oraz wartości powrotu procedur przechowywanych nie są ostateczne dopiero po zamknięciu czytnika:

using var command = new SqlCommand("""
    SELECT TOP (5) ProductID, Name
    FROM Production.Product
    ORDER BY ProductID;
    SET @count = @@ROWCOUNT;
    """, connection);

SqlParameter count = command.Parameters.Add("@count", SqlDbType.Int);
count.Direction = ParameterDirection.Output;

using (SqlDataReader reader = command.ExecuteReader())
{
    while (reader.Read())
    {
        Console.WriteLine(reader.GetString(1));
    }
}

Console.WriteLine($"Rows returned: {count.Value}");

Bez Multiple Active Result Sets (MARS) otwarty czytnik utrzymuje swoje połączenie zajęte i inne polecenie nie może zostać wykonane przy użyciu tego połączenia. Przy włączonym MARS oddzielne polecenia mogą mieć oczekujące wyniki na jednym połączeniu, ale ich działania nadal nie są bezpieczne dla wątków. Więcej informacji można znaleźć w sekcji Wiele aktywnych zbiorów wyników.

Jeśli przekazano polecenie CommandBehavior.CloseConnection, zamknięcie czytnika danych również zamyka połączenie. W przeciwnym razie kod, który otworzył połączenie, pozostaje odpowiedzialny za jego zwolnienie.

Odczytaj liczbę zmodyfikowanych wierszy

RecordsAffected raportuje wiersze zmienione przez instrukcje INSERT, UPDATE i DELETE w poleceniu. Przeczytaj ją po zamknięciu czytnika. Ten przykład wycofuje swoją aktualizację:

using SqlTransaction transaction = connection.BeginTransaction();
using var command = new SqlCommand("""
    UPDATE Production.Product
    SET ModifiedDate = ModifiedDate
    WHERE ProductID IN (706, 707);

    SELECT ProductID, Name
    FROM Production.Product
    WHERE ProductID IN (706, 707)
    ORDER BY ProductID;
    """, connection, transaction);

SqlDataReader reader = command.ExecuteReader();
using (reader)
{
    while (reader.Read())
    {
        Console.WriteLine($"{reader.GetInt32(0)} | {reader.GetString(1)}");
    }
}

Console.WriteLine($"RecordsAffected: {reader.RecordsAffected}");
transaction.Rollback();

Dla polecenia tylko SELECT, RecordsAffected to -1. SET NOCOUNT ON powoduje pominięcie zliczania, co również pozostawia wartość na -1.

Pobierz wiele zestawów wyników za pomocą NextResult

Jedno polecenie lub procedura przechowywana może zwracać wiele wyników. Przetworz bieżący wynik w całości, a następnie wywołaj NextResult lub NextResultAsync:

using var command = new SqlCommand("""
    SELECT TOP (3) ProductID, Name
    FROM Production.Product
    ORDER BY ProductID;

    SELECT COUNT(*) AS ProductCount
    FROM Production.Product;
    """, connection);

using SqlDataReader reader = command.ExecuteReader();
int result = 1;

do
{
    Console.WriteLine($"Result {result++}: {reader.FieldCount} column(s)");
    while (reader.Read())
    {
        var values = new object[reader.FieldCount];
        reader.GetValues(values);
        Console.WriteLine(string.Join(" | ", values));
    }
}
while (reader.NextResult());

Liczba kolumn, nazwy, liczby porządkowe i typy mogą się zmieniać dla każdego wyniku. Przelicz ponownie buforowane liczby porządkowe, gdy NextResult zwróci true.

Na czytniku może być aktywna tylko jedna operacja asynchroniczna naraz. Poczekaj na ReadAsync, NextResultAsync lub asynchroniczne działanie na polu przed rozpoczęciem kolejnej operacji na tym samym czytniku.

Pobieranie informacji o schemacie z DataReader

Wywołaj GetColumnSchema, aby uzyskać silnie typowany opis kolumn bieżącego wyniku. Wywołaj GetSchemaTable, gdy istniejący kod potrzebuje schematu w DataTable:

using var command = new SqlCommand("""
    SELECT ProductID, Name, Color
    FROM Production.Product;
    """, connection);

using SqlDataReader reader = command.ExecuteReader(
    CommandBehavior.SchemaOnly);

foreach (System.Data.Common.DbColumn column in reader.GetColumnSchema())
{
    Console.WriteLine(
        $"{column.ColumnOrdinal}: {column.ColumnName} " +
        $"({column.DataTypeName}), nullable={column.AllowDBNull}");
}

CommandBehavior.SchemaOnly żąda metadanych kolumn bez wierszy. Informacje o schemacie opisują aktualny zbiór wyników. Poproś ponownie po przejściu do następnego wyniku. W zależności od zapytania i uprawnień serwera, niektóre metadane tabeli bazowej mogą być niedostępne.