Microsoft.Data.SqlClient 通过 SqlException 报告服务器和提供程序故障。 数据库操作可能返回多个错误,因此应检查异常的 Errors 集合,而非仅依赖合并后的 Message。
数据库引擎的警告和信息通过 InfoMessage 传递。 在打开连接或执行能产生该消息的命令前,先订阅。
按以下顺序处理数据库故障:
- 在可以增加操作上下文或选择恢复动作的边界处捕捉
SqlException。 - 检查
SqlError集合中的每个Errors。 - 记录结构化诊断字段,无需记录凭证或敏感值。
- 检查连接相关故障后的连接状态。
- 只有在错误是暂时且操作安全可以重复时才重试。
本文中的示例假定名为 connection 的打开的 SqlConnection 以 AdventureWorks 示例数据库 为目标。 C# 示例要求 using Microsoft.Data.SqlClient;。
接收信息类消息
事件 InfoMessage 接收低严重度服务器消息,包括来自 PRINT的输出。 一个事件可以包含多个 SqlError 对象。 在执行可生成消息的命令前,先订阅相关主题:
connection.InfoMessage += (_, eventArgs) =>
{
foreach (SqlError message in eventArgs.Errors)
{
Console.WriteLine(
$"Info {message.Number}, class {message.Class}: {message.Message}");
}
};
using var command = new SqlCommand(
"PRINT N'Connected and ready.';",
connection);
command.ExecuteNonQuery();
摘录如下:
Info 0, class 0: Connected and ready.
捕获 SqlException
在可处理或补充信息的数据库操作周围捕获 SqlException。 不要用一个宽泛的 catch 块来把失败的数据库操作变成成功的结果。
数据库操作可能返回多个错误。
Errors 中的每个 SqlError 都包含结构化字段:
| Property | Meaning |
|---|---|
Number |
数据库引擎或提供者错误编号。 |
Class |
严重性。 |
State |
由服务器定义的状态,可用于区分具有相同错误编号的错误来源。 |
LineNumber |
已报告的行号。 |
Procedure |
提供的过程或触发器名称(如已提供)。 |
Server |
错误中报告的服务器名称。 |
Message |
便于阅读的错误文本。 |
SqlException 上的便捷属性(包括 Number、Class、State、LineNumber、Procedure 以及 Server)仅反映第一个错误。 以下摘录打印一条消息,然后在同一批中引发错误。 它读取 exception.Number 并迭代 Errors 报告每个条目:
using (var failing = new SqlCommand("""
PRINT N'Starting controlled operation.';
RAISERROR (N'The controlled operation failed.', 16, 1);
""", connection))
{
try
{
await failing.ExecuteNonQueryAsync();
}
catch (SqlException exception)
{
Console.WriteLine($"First error number: {exception.Number}");
foreach (SqlError error in exception.Errors)
{
Console.WriteLine(
$"Number={error.Number}; Class={error.Class}; " +
$"State={error.State}; Message={error.Message}");
}
}
}
摘录如下:
First error number: 50000
Number=50000; Class=16; State=1; Message=The controlled operation failed.
Number=0; Class=0; State=1; Message=Starting controlled operation.
PRINT 输出不会引发 InfoMessage。 当一个批次返回触发 SqlException 的错误时,提供者会将该批次的信息消息附加到 SqlException.Errors 的末尾。
本摘录仅捕获该受控错误以展示其字段。 在应用程序代码中,除非 catch 块选择真正的恢复操作,否则用 throw; 重新抛出异常。 根据有文档记录的错误编号和操作上下文做出决策。 不要解析本地化的消息文本。
检查连接状态
数据库引擎为每个消息分配一个严重性级别。 较低的类属于信息性。 被视为错误的类会产生 SqlException。 高严重性错误可能导致连接关闭或中断。
出现错误后,请在重新使用该连接前检查 State :
using var command = new SqlCommand(
"RAISERROR (N'Controlled failure.', 16, 1);",
connection);
try
{
command.ExecuteNonQuery();
}
catch (SqlException)
{
Console.WriteLine($"Connection state: {connection.State}");
}
不要因为捕获到异常就认为连接还能用。 严重性级别为 16 的错误通常会让连接保持打开,但导致连接中断的故障可能会关闭它。 释放已关闭或已损坏的连接,并从池中获取另一个连接。 不要仅仅根据严重程度来编码重试策略。 该操作还必须能够安全地重复执行。
识别失败的批处理命令
当 SqlBatch 失败时,SqlException.BatchCommand 可以引用相关的 SqlBatchCommand。 当提供者无法将失败归因于某个批处理命令时,该属性为 null。 以下摘录运行了两个批处理命令。 第二个失败了:
using (var batch = new SqlBatch(connection))
{
batch.BatchCommands.Add(new SqlBatchCommand("SELECT 1;"));
batch.BatchCommands.Add(new SqlBatchCommand("SELECT 1 / 0;"));
try
{
await batch.ExecuteNonQueryAsync();
}
catch (SqlException exception)
{
Console.WriteLine($"Error {exception.Number}: {exception.Message}");
if (exception.BatchCommand is SqlBatchCommand failedCommand)
{
Console.WriteLine($"Failed command: {failedCommand.CommandText}");
}
}
}
摘录如下:
Error 8134: Divide by zero error encountered.
Failed command: SELECT 1 / 0;
在参数化不佳的应用中,命令文本可能包含敏感的字面量。 在生产日志中优先使用稳定的应用操作名称,默认不记录参数值。
将用户错误路由到 InfoMessage
FireInfoMessageEventOnUserErrors 默认为 false。 将其设置为 true 时,严重性级别为 16 或更低的错误会转入 InfoMessage 处理程序,而不会引发 SqlException:
connection.InfoMessage += (_, eventArgs) =>
{
foreach (SqlError message in eventArgs.Errors)
{
Console.WriteLine(
$"Info {message.Number}, class {message.Class}: {message.Message}");
}
};
connection.FireInfoMessageEventOnUserErrors = true;
using var command = new SqlCommand(
"RAISERROR (N'Routed to InfoMessage.', 16, 1);",
connection);
command.ExecuteNonQuery();
不会引发异常。 摘录如下:
Info 50000, class 16: Routed to InfoMessage.
只有当应用程序采用有意的基于事件的错误模型时,才使用此选项,因为调用代码不再接收到异常。 严重性级别为 17 或更高的错误仍会引发 SqlException。 在连接配置中将该属性设置一次,而不是针对各个单独命令切换。
日志诊断上下文
捕捉足够的结构化上下文,以关联故障,同时不暴露数据。 以下摘录记录了应用操作名称、 ClientConnectionId、 以及每个 SqlError的字段。
ClientConnectionId 有助于将客户端故障与服务器和网络诊断联系起来。 它不是凭据。
const string operation = "LoadOrders";
using (var load = new SqlCommand("SELECT 1 / 0;", connection))
{
try
{
await load.ExecuteScalarAsync();
}
catch (SqlException exception)
{
Console.WriteLine(
$"Operation={operation}; " +
$"ClientConnectionId={exception.ClientConnectionId}");
foreach (SqlError error in exception.Errors)
{
Console.WriteLine(
$"Number={error.Number}; Class={error.Class}; " +
$"State={error.State}; Procedure={error.Procedure}; " +
$"Line={error.LineNumber}");
}
}
}
每个连接上的 ClientConnectionId 值都不同。 摘录打印的输出内容类似于:
Operation=LoadOrders; ClientConnectionId=<connection ID>
Number=8134; Class=16; State=1; Procedure=; Line=1
还要记录某个事务是否处于激活状态,以及 BatchCommand 如果有的话,哪个批处理命令失败了。 只有在组织政策允许的情况下,才包含经过净化的服务器和数据库标识符。 不要记录连接字符串、访问令牌、密码或敏感参数值。 使用 诊断计数器、事件源追踪和 SqlClient 故障排除指南 进行深入调查。
相关内容
- 返回 命令执行工作流程。
- 要识别组中失败的命令,使用 SqlBatch执行多个命令。
- 要添加有界重试,可以使用 可配置的重试逻辑。
- 当例外字段不足时,可以使用 SqlClient 的故障排除指南。