使用 SqlBatch 执行多个命令

使用 SqlBatch 通过一个已打开的 SqlConnection 发送有序的 SqlBatchCommand 对象集合。 每个批处理命令都有自己的命令文本、命令类型、参数、命令行为以及受影响行数结果。

当应用程序有需要一起发送的多个不同命令时,使用 SqlBatch。 当工作是一个语句或存储过程时,使用一个参数化的 SqlCommand。 对于大型行集,可以使用 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 支持生命周期的支持范围内。

在现代 .NET 上,SqlBatch 派生自 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。 两个批处理命令中的相同占位符名称不共享一个值:

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。 读取器按批处理命令的顺序公开结果。 调用 NextResult 或 NextResultAsync,直到其返回 false。

命令可以产生零个行集、一个行集或多个行集。 不要假设一个 NextResult 调用对应一个 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 开始提供。

在单独命令上设置 CommandBehavior ,用于选项如 SingleRow、 SchemaOnly和 KeyInfo。 对 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();

仅当应用程序接受完整批处理时,才调用 Commit 而不是 Rollback。 在执行前分配事务。 事务和批处理必须使用相同的连接。 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 块内处理读取器。

不要记录包含凭证、令牌或敏感数据的参数值。 关于错误字段和信息消息,请参见 处理错误和消息。