SqlBatchで複数のコマンドを実行する

SqlBatchを使って、順序付けられたSqlBatchCommandオブジェクトの集合を1つの開いたSqlConnectionに送ります。 各バッチコマンドは独自のコマンドテキスト、コマンドタイプ、パラメータ、コマンドの挙動、そして影響を受けた行数の結果を持っています。

アプリケーションがそれぞれ異なるコマンドを持っていて、それを一緒に送りたい場合に SqlBatch 使う。 作業が1つの文やストアドプロシージャの場合は、パラメータ付き SqlCommand を1つ使います。 大きな行セットには SqlBulkCopy またはテーブル値パラメータを使いましょう。

この記事の例では、connection という名前のオープン SqlConnection が AdventureWorks サンプル データベース をターゲットとしていることを前提としています。

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

バージョンおよびプラットフォームサポート

これらの例は Microsoft.Data.SqlClient 7.1.0 を対象としています。 アプリケーションに対して、Microsoft.Data.SqlClient サポート ライフサイクルでサポートされているパッケージ バージョンとターゲット フレームワークを選択してください。

SqlBatch は現代の .NET では DbBatch から派生しています。 Microsoft.Data.SqlClient は、.NET Framework と .NET Standard 2.0 のパッケージ資産においても SqlBatch を提供しています。 公開されるインターフェースはターゲットのフレームワークによって異なる場合があるため、配布するすべてのターゲット向けにアプリケーションをコンパイルしてください。

バッチを作成する

オープン接続用のバッチを作成し、実行順にコマンドを追加します:

using var batch = new SqlBatch(connection);
batch.BatchCommands.Add(new SqlBatchCommand("""
    SELECT TOP (3) ProductID, Name
    FROM Production.Product
    ORDER BY ProductID;
    """));
batch.BatchCommands.Add(new SqlBatchCommand("""
    SELECT TOP (3) SalesOrderID, OrderDate
    FROM Sales.SalesOrderHeader
    ORDER BY SalesOrderID;
    """));

using DbDataReader reader = batch.ExecuteReader();
do
{
    while (reader.Read())
    {
        Console.WriteLine($"{reader.GetValue(0)} | {reader.GetValue(1)}");
    }
}
while (reader.NextResult());

CommandType.Text はデフォルト値です。 ストアドプロシージャ用にバッチコマンドを CommandType.StoredProcedure に設定します。 CommandType.TableDirect を設定すると ArgumentOutOfRangeException がスローされます。

バッチコマンドにパラメータを追加する

パラメーターは個々の SqlBatchCommand に属します。 2つのバッチコマンドで同じプレースホルダー名でも、値は共有されません:

using var batch = new SqlBatch(connection);

var products = new SqlBatchCommand("""
    SELECT ProductID, Name
    FROM Production.Product
    WHERE ProductSubcategoryID = @id
    ORDER BY ProductID;
    """);
products.Parameters.Add("@id", SqlDbType.Int).Value = 2;
batch.BatchCommands.Add(products);

var orders = new SqlBatchCommand("""
    SELECT SalesOrderID, OrderDate
    FROM Sales.SalesOrderHeader
    WHERE SalesPersonID = @id
    ORDER BY SalesOrderID;
    """);
orders.Parameters.Add("@id", SqlDbType.Int).Value = 279;
batch.BatchCommands.Add(orders);

using DbDataReader reader = batch.ExecuteReader();

各パラメータのSQL型と、データベース契約から適用可能なサイズ、精度、スケールを設定します。 詳細については、パラメーターのコンフィギュレーション を参照してください。

データ変更コマンドの実行

バッチでデータが変更されて結果行が不要なときに ExecuteNonQuery や ExecuteNonQueryAsync を使うと良いでしょう。 返される値は、行数が利用可能な場合に影響を受けた行の合計です。 実行後、各コマンドの RecordsAffected はそれぞれのカウントを報告します。

この例は変更を元に戻します:

using SqlTransaction transaction = connection.BeginTransaction();
using var batch = new SqlBatch(connection, transaction);

var firstUpdate = new SqlBatchCommand("""
    UPDATE Sales.SalesOrderHeader
    SET Status = @status
    WHERE SalesOrderID = @salesOrderId;
    """);
firstUpdate.Parameters.Add("@status", SqlDbType.TinyInt).Value = 2;
firstUpdate.Parameters.Add("@salesOrderId", SqlDbType.Int).Value = 43659;
batch.BatchCommands.Add(firstUpdate);

var secondUpdate = new SqlBatchCommand("""
    UPDATE Sales.SalesOrderHeader
    SET Status = @status
    WHERE SalesOrderID = @salesOrderId;
    """);
secondUpdate.Parameters.Add("@status", SqlDbType.TinyInt).Value = 2;
secondUpdate.Parameters.Add("@salesOrderId", SqlDbType.Int).Value = 43660;
batch.BatchCommands.Add(secondUpdate);

int totalRows = batch.ExecuteNonQuery();
Console.WriteLine($"Total rows: {totalRows}");

foreach (SqlBatchCommand command in batch.BatchCommands)
{
    Console.WriteLine(command.RecordsAffected);
}

transaction.Rollback();

複数の結果を読み込む

コマンドが結果を返した場合は、ExecuteReader または ExecuteReaderAsync を使用します。 リーダーは結果をバッチコマンドの順序で出力します。 false が表示されるまで、NextResult または NextResultAsync を押してください。

コマンドは行セットを生成しない場合もあれば、1つまたは複数の行セットを生成する場合もあります。 1回のNextResult呼び出しが1つのSqlBatchCommandに対応すると決めつけないでください。

コマンドの動作を使用する

リーダーが順次アクセスが必要な場合や接続を閉じる必要がある場合、バッチレベルの CommandBehavior を ExecuteReader または ExecuteReaderAsync に渡します:

using var batch = new SqlBatch(connection);
batch.BatchCommands.Add(new SqlBatchCommand("""
    SELECT DocumentSummary
    FROM Production.Document
    WHERE DocumentSummary IS NOT NULL;
    """));

using DbDataReader reader =
    batch.ExecuteReader(CommandBehavior.SequentialAccess);

char[] buffer = new char[4096];
while (reader.Read())
{
    using TextReader summary = reader.GetTextReader(0);
    int count;
    while ((count = summary.Read(buffer, 0, buffer.Length)) > 0)
    {
        Console.Out.Write(buffer, 0, count);
    }
    Console.WriteLine();
}

バッチ レベルのオーバーロードは、CommandBehavior.Default、CommandBehavior.SequentialAccess、CommandBehavior.CloseConnection、およびそれらの値の組み合わせのみを受け入れます。 SingleRow、SchemaOnly、KeyInfoなどの他の有効な値は、バッチレベルのオーバーロードに渡されるとArgumentOutOfRangeExceptionを投げます。 バッチレベルの動作オーバーロードは、Microsoft.Data.SqlClient 7.1 以降で利用可能です。

SingleRow、SchemaOnly、KeyInfo などのオプションについては、個別のコマンドで CommandBehavior を設定してください。 バッチレベルのオーバーロードを SequentialAccess と CloseConnectionに活用してください。

トランザクションの使用

SqlBatch はそのコマンドをアトミックにしません。 すべてのコマンドを一緒にコミットまたはロールバックする必要がある場合は、明示的なトランザクションを使用してください。 この例はロールバックするため、AdventureWorksは変更されません:

using SqlTransaction transaction = connection.BeginTransaction();
using var batch = new SqlBatch(connection, transaction);

batch.BatchCommands.Add(new SqlBatchCommand("""
    UPDATE Sales.SalesOrderHeader
    SET Status = 2
    WHERE SalesOrderID = 43659;
    """));
batch.BatchCommands.Add(new SqlBatchCommand("""
    UPDATE Sales.SalesOrderHeader
    SET Status = 2
    WHERE SalesOrderID = 43660;
    """));

batch.ExecuteNonQuery();
transaction.Rollback();

アプリケーションがバッチ全体を受け入れた場合にのみ、Rollback の代わりに Commit を呼び出してください。 実行前にトランザクションを割り当ててください。 トランザクションとバッチは同じ接続を使用しなければなりません。 SqlClientは非同期トランザクションメソッドを上書きしないため、 BeginTransactionAsync、 CommitAsync、 RollbackAsync が同期的に完了します。 詳細については、「 タスクベースのSQLトランザクションメソッドを使う」をご覧ください。

タイムアウト、キャンセル、エラーの処理

バッチの Timeout を秒単位で設定します。 このバッチは、接続の Command Timeout 設定を構成した場合、その設定を使用します。 それ以外の場合は、デフォルトで30秒です。 0の値には時間制限がなく、負の値を指定するとArgumentExceptionがスローされます。 呼び出し側が待機を中止する必要がある場合、または Cancel を呼び出す必要がある場合は、非同期実行メソッドにキャンセル トークンを渡します。 キャンセルはベストエフォートです。 処理はキャンセルが効力を発揮する前に完了するかもしれません。

実行に失敗したら、SqlException を捕まえてその Errors コレクション内のすべてのアイテムを検査してください。 BatchCommand その情報が利用可能な場合に関連する SqlBatchCommand を特定します:

using var batch = new SqlBatch(connection);
batch.BatchCommands.Add(new SqlBatchCommand(
    "SELECT TOP (1) ProductID FROM Production.Product;"));
batch.BatchCommands.Add(new SqlBatchCommand("SELECT 1 / 0;"));

try
{
    using DbDataReader reader = batch.ExecuteReader();
    do
    {
        while (reader.Read())
        {
            Console.WriteLine(reader.GetValue(0));
        }
    }
    while (reader.NextResult());
}
catch (SqlException exception)
{
    Console.WriteLine(
        exception.BatchCommand is null
            ? "The batch failed."
            : $"Failed command: {exception.BatchCommand.CommandText}");
}

リーダーによる実行は段階的です。 後のバッチコマンドからのエラーは、Read または NextResult がそのコマンドに進む際に表面化する可能性があるため、try ブロック内でリーダーを処理してください。

認証情報、トークン、機密データを含むパラメータ値は記録しないでください。 エラーフィールドや情報メッセージについては「エラーとメッセージの処理」を参照してください。