處理錯誤與訊息

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.

Catch 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 故障排除指南 來進行更深入的調查。