使用 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 故障排除指南 。 只有在重复命令安全时才应用 可配置重试逻辑 。