DataReaderを使ってデータを取得

SqlDataReaderはSQLデータベースから順方向専用の結果セットを読み込みます。 結果をすべて DataSet に読み込むわけではないため、行が到着するたびに処理する場合に適しています。

この記事の例は、名前がSqlConnectionである開いているconnectionがAdventureWorks のサンプル データベースを対象としていることを前提としています。

C#の例は using Microsoft.Data.SqlClient;を必要とします。 SqlDbTypeやCommandBehaviorを使う例もusing System.Data;が必要です。

行の読み取り

基本的なリーダーワークフローは以下の通りです:

  1. ExecuteReaderまたはExecuteReaderAsyncでコマンドを実行します。
  2. 現在の結果について、列の順序番号を決定します。
  3. 各行ごとに Read を1回呼び出してください。
  4. 型付けされたゲッターを呼び出す前に、 IsDBNull でnullable列をチェックしてください。
  5. 接続を再利用したり、出力パラメーターを読み取ったりする前に、リーダーを破棄してください。

この例は列順序数をキャッシュし、型付けゲッターを使い、nullable 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)"}");
}

GetString値に対してNULLを呼び出すとSqlNullValueExceptionがスローされるので、まずIsDBNullを確認してください。

HasRows は現在の結果に行が含まれているかどうかを報告しますが、通常は Read を直接呼び出すことができます。 空の結果の場合、最初の Read 呼び出しは 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()}");

大きな数値の場合は CommandBehavior.SequentialAccess で実行し、 GetStream、 GetTextReader、または GetXmlReaderを使いましょう。 詳細については、 SqlClientストリーミングサポートをご覧ください。

DataReader を閉じる

処理が完了次第、リーダーを解放してください。 await using または using という文があれば、行処理で例外が発生した場合でも、その文によって処理が終了します。

出力パラメーター、入出力パラメーター、ストアド プロシージャの戻り値は、リーダーが閉じられるまで確定しません:

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}");

複数のアクティブ結果セット(MARS)がなければ、開いているリーダーは接続を忙しく保ち、別のコマンドはその接続で実行できません。 MARSを有効にしていると、別々のコマンドが1つの接続で保留結果を持つことはできますが、それでもそれらの操作はスレッド安全ではありません。 詳細については 、複数のアクティブ結果セットをご覧ください。

コマンドに CommandBehavior.CloseConnection より小さい値が渡された場合、リーダーを閉じると接続も閉じられます。 そうでなければ、接続を開いたコードがその接続を破棄する責任を負います。

影響を受けた行数を読み取る

RecordsAffected は、コマンド内の INSERT、UPDATE、DELETE ステートメントによって変更された行を報告します。 リーダーが閉じた後に読み込みます。 この例では、更新をロールバックしています:

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();

SELECTのみのコマンドなら、RecordsAffectedは-1です。 SET NOCOUNT ON カウントを抑制し、値も-1のままになります。

NextResultを使って複数の結果セットを取得する

1つのコマンドやストアドプロシージャで複数の結果を返すことができます。 現在の結果を完全に処理してから、NextResult または 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());

カラム数、名前、順序数、型は結果ごとに変わることがあります。 NextResult が true を返した後、キャッシュされた順序番号を再計算します。

リーダーに対しては、同時に1つの非同期操作のみが保留可能です。 同じリーダーで別の操作を開始する前に、ReadAsync、NextResultAsync、または非同期フィールド操作を待機してください。

DataReader からのスキーマ情報の取得

現在の結果の列に関する型指定付きの説明を確認するには、GetColumnSchema を押してください。 既存のコードで DataTable のスキーマが必要になった場合は、GetSchemaTable に呼び出します:

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 行を返さずに列メタデータをリクエストします。 スキーマ情報は現在の結果セットを記述します。 次の結果に進んだ後に再度リクエストしてください。 クエリやサーバーの権限によっては、一部のベーステーブルメタデータが利用できない場合があります。