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.
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 故障排除指南 來進行更深入的調查。
相關內容
- 返回 指令執行工作流程。
- 要識別群組中失敗的指令,請 使用 SqlBatch 執行多個指令。
- 要加有界重試,請使用 可設定的重試邏輯。
- 當例外欄位不夠時,請使用 SqlClient 的故障排除指南。