处理错误和消息

Microsoft.Data.SqlClient 通过 SqlException 报告服务器和提供程序故障。 数据库操作可能返回多个错误,因此应检查异常的 Errors 集合,而非仅依赖合并后的 Message。

数据库引擎的警告和信息通过 InfoMessage 传递。 在打开连接或执行能产生该消息的命令前,先订阅。

按以下顺序处理数据库故障:

  1. 在可以增加操作上下文或选择恢复动作的边界处捕捉 SqlException 。
  2. 检查SqlError集合中的每个Errors。
  3. 记录结构化诊断字段,无需记录凭证或敏感值。
  4. 检查连接相关故障后的连接状态。
  5. 只有在错误是暂时且操作安全可以重复时才重试。

本文中的示例假定名为 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 故障排除指南 进行深入调查。