执行命令

使用 SqlCommand 执行 Transact-SQL(T-SQL)语句或存储过程。 命令需要一个已打开的 SqlConnection。 使用后释放命令,并在重用连接前释放任何读取器。

本文中的示例假设有一个名为 connection 的打开的 SqlConnection,其目标为 AdventureWorks 示例数据库。

C# 示例要求 using Microsoft.Data.SqlClient;。 使用 SqlDbType、CommandType 或 CommandBehavior 的示例也需要 using System.Data;。

关于连接设置、认证和加密,请参见 连接字符串 以及 加密与证书验证。

选择一种执行方法

选择与预期结果相符的方法:

方法 何时使用 结果
ExecuteNonQuery 该命令会更改数据或模式。 受影响的行数。 -1 在没有可用计数时也可能发生,包括当 SET NOCOUNT ON 抑制计数时。
ExecuteScalar 你需要一个数值。 第一行的第一列,或 null 当结果没有行时。 NULL数据库作为 Value 返回。
ExecuteReader 你需要行、多个结果集或模式。 一个 SqlDataReader。
ExecuteXmlReader 该命令返回符合 ExecuteXmlReader 要求格式的 XML。 一个 XmlReader。

每种方法都有基于任务的异步对应方法。 当调用方需要停止等待时,传递 CancellationToken。 关于取消和超时行为,请参见 异步编程。

执行文本命令

CommandType.Text 是默认值。 将语句放入 CommandText 中,并通过参数传递值:

const string sql = """
    SELECT ProductID, Name, ListPrice
    FROM Production.Product
    WHERE ProductSubcategoryID = @subcategoryId
    ORDER BY ListPrice DESC;
    """;

using var command = new SqlCommand(sql, connection);
command.Parameters.Add("@subcategoryId", SqlDbType.Int).Value = 2;

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

关于参数类型、大小、空值和方向,请参 见配置参数。 不要将不可信的值串接到 CommandText。

执行存储过程

设置 CommandType.StoredProcedure 并使用模式限定的程序名称:

using var command = new SqlCommand(
    "dbo.uspGetEmployeeManagers",
    connection);
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add(
    "@BusinessEntityID",
    SqlDbType.Int).Value = 6;

using SqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{
    Console.WriteLine(
        $"{reader.GetString(2)} {reader.GetString(3)}");
}

Microsoft.Data.SqlClient 支持 CommandType.Text 和 CommandType.StoredProcedure。 设置 CommandType.TableDirect 会抛出 ArgumentOutOfRangeException。

检查每个方法返回的内容

需要一个值时使用 ExecuteScalar :

using var command = new SqlCommand(
    "SELECT COUNT(*) FROM Production.Product WHERE SellEndDate IS NULL;",
    connection);

int currentProductCount = (int)command.ExecuteScalar()!;
Console.WriteLine($"Current products: {currentProductCount}");

ExecuteScalar在结果没有行时返回DBNull.Value,而当第一个值是数据库null时返回NULL。

对数据更改语句使用 ExecuteNonQuery。 此示例回滚了变更,使AdventureWorks保持不变:

using SqlTransaction transaction = connection.BeginTransaction();

using var command = new SqlCommand("""
    UPDATE Sales.SalesOrderHeader
    SET Status = @status
    WHERE SalesOrderID = @salesOrderId;
    """, connection, transaction);
command.Parameters.Add("@status", SqlDbType.TinyInt).Value = 2;
command.Parameters.Add("@salesOrderId", SqlDbType.Int).Value = 43659;

int rowsAffected = command.ExecuteNonQuery();
Console.WriteLine($"Rows affected: {rowsAffected}");

transaction.Rollback();

SET NOCOUNT ON 抑制受影响行计数,因此 ExecuteNonQuery 即使语句修改了数据,也能返回 -1 。

使用 ExecuteXmlReader 可获得 FOR XML 结果:

using var command = new SqlCommand("""
    SELECT TOP (3) ProductID, Name
    FROM Production.Product
    ORDER BY ProductID
    FOR XML PATH('Product'), ROOT('Products');
    """, connection);

using System.Xml.XmlReader reader = command.ExecuteXmlReader();
reader.MoveToContent();
Console.WriteLine(reader.ReadOuterXml());

使用本地事务

当连接有活跃的本地 SqlTransaction 时,将该事务分配给连接上执行的每一个命令。 此示例回滚了变更,使AdventureWorks保持不变:

using SqlTransaction transaction = connection.BeginTransaction();

using var command = new SqlCommand("""
    UPDATE Sales.SalesOrderHeader
    SET Status = @status
    WHERE SalesOrderID = @salesOrderId;
    """, connection, transaction);
command.Parameters.Add("@status", SqlDbType.TinyInt).Value = 2;
command.Parameters.Add("@salesOrderId", SqlDbType.Int).Value = 43659;

command.ExecuteNonQuery();
transaction.Rollback();

仅当应用程序接受完整事务时,才调用 Commit 而不是 Rollback。 不引用当前事务的命令会抛出InvalidOperationException。 如果事务属于另一个连接,执行也会失败。 关于事务边界和回滚,请参见 本地事务。

设置命令超时

CommandTimeout 限制命令执行和累计网络读取等待时间,以秒为单位。 如果已配置,命令会使用连接的 Command Timeout 设置。 否则,默认是30秒。 值为 0 时没有时间限制。 负值则抛出 ArgumentException。

超时到期时,命令会抛出 SqlException,并将 Number 设置为 -2:

using var command = new SqlCommand(
    "WAITFOR DELAY '00:00:05';",
    connection)
{
    CommandTimeout = 1
};

try
{
    command.ExecuteNonQuery();
}
catch (SqlException exception) when (exception.Number == -2)
{
    Console.WriteLine("The command timed out.");
}

Connect Timeout 限制连接建立和池等待。 CommandTimeout 在命令执行开始后启动。 更多信息请参见 连接选项。

同时使用有限超时和取消令牌。 超时对数据库操作设置了限制。 取消令牌可以在超时到期前将控制权还给调用方。 不要以为取消和超时会产生相同的异常。

重用和准备命令

添加一次参数,然后每次执行结束后更改其值:

using var command = new SqlCommand("""
    SELECT TOP (1) Name, ListPrice
    FROM Production.Product
    WHERE ProductSubcategoryID = @subcategoryId
    ORDER BY ListPrice DESC;
    """, connection);
SqlParameter subcategoryId = command.Parameters.Add(
    "@subcategoryId",
    SqlDbType.Int);

foreach (int id in new[] { 1, 2, 3 })
{
    subcategoryId.Value = id;
    using SqlDataReader reader = command.ExecuteReader();
    if (reader.Read())
    {
        Console.WriteLine($"{id}: {reader.GetString(0)}");
    }
}

只有在测量结果证明有必要时才调用 Prepare。 Prepare 不会准备不带参数的存储过程或文本命令。 对于参数化文本,在调用 Scale 之前,在每个参数上设置完整的元数据,如 Prepare、Size 和 Precision。

排查命令

在数据库操作周围捕获 SqlException,并在服务器返回多个错误时检查其 Errors 集合。 关于命令错误、警告和信息性消息,请参见 处理错误和消息。

当出现错误需要更多上下文时,可以使用 诊断计数器、 事件源追踪和 SqlClient 故障排除指南 。 只有在重复命令安全时才应用 可配置重试逻辑 。