首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> SQL Server 資料庫 >> 關於SqlServer

SQL Server 異常處理進階 (三):SqlException 屬性深度解析與 InfoMessage 事件應用

     編輯:關於SqlServer

前言:從 T-SQL 到 ADO.NET 的異常傳遞鏈

前兩篇建立了「sys.messages 統一錯誤碼 → RAISERROR 拋出 → TRY/CATCH 捕捉 → ADO.NET 捕獲 SqlException」的完整鏈路。本文深入剖析 SqlException 物件屬性映射關係,並介紹 SqlConnection.InfoMessage 事件處理非錯誤訊息 (PRINT、RAISERVERITY ≤ 10) 的機制。

一、SqlException 核心屬性對照表

屬性型別來源映射說明
ErrorsSqlErrorCollectionT-SQL RAISERROR 產生的每一條錯誤記錄包含 Number、Message、Severity、State、Procedure、LineNumber、Server 等詳細資訊,可能多條 (批次多條 RAISERROR)
NumberInt32sys.messages.message_id / @@ERROR錯誤號,**關鍵識別碼**。若 RAISERROR 直接傳字串而非 message_id,預設為 50000
MessageStringsysmessages.text / RAISERROR 字串完整錯誤訊息文字
Severity (Class)ByteRAISERROR severity 參數11-16 可被 TRY/CATCH 捕捉;17-19 系統級;20-25 致命 (斷開連線)
StateByteRAISERROR state 參數同一錯誤號不同狀態碼,用於區分拋出位置
ProcedureString預存程序/觸發器/函數名稱錯誤發生的模組名稱
LineNumberInt32批次/模組內行號錯誤發生的行號 (從 1 起算)
ServerString連線字串 Data Source錯誤來源伺服器名稱
SourceString.NET 提供者名稱固定為 .Net SqlClient Data Provider
ErrorCodeInt32HRESULT (0x80131904)COM 互通性錯誤碼,固定值

二、關鍵陷阱:RAISERROR 字串 vs Message ID 的 Number 差異

情境 A:使用訊息 ID (推薦,可維護、多語系)

-- T-SQL
RAISERROR(50001, 16, 1); -- 50001 為 sys.messages 中自訂 message_id

-- ADO.NET 捕獲
ex.Number → 50001  ✅ 精確識別業務錯誤類型

情境 B:直接傳遞字串 (不推薦,Number 固定 50000)

-- T-SQL
DECLARE @msg NVARCHAR(200) = '使用者已存在';
RAISERROR(@msg, 16, 1); -- 無 message_id

-- ADO.NET 捕獲
ex.Number → 50000  ❌ 所有自訂字串錯誤號相同,無法區分業務類型

破解:WITH SETERROR 強制回傳 @@ERROR

RAISERROR(@msg, 16, 1) WITH SETERROR; -- 強制將當前 @@ERROR 值設為錯誤號
-- 若前一條語句產生錯誤 (如 PK 違反 2627),則 Number = 2627
-- 否則 Number = 50000

三、實戰程式碼:完整 SqlException 解析器

public static bool CreateUser(string userName)
{
    const string proc = "P_USERS_I";
    var parameters = new Dictionary
    {
        ["user_id"] = Guid.NewGuid().ToString(),
        ["user_name"] = userName
    };

    try
    {
        ExecuteCommand(proc, parameters); // 內部使用 SqlCommand + Parameters
        return true;
    }
    catch (SqlException ex)
    {
        // 結構化日誌記錄 (Serilog/NLog/ELK)
        Log.Error(ex, "CreateUser 失敗 | UserName={UserName} | ErrorNumber={Number} | Severity={Class} | Procedure={Procedure} | Line={LineNumber} | Server={Server} | State={State}",
            userName, ex.Number, ex.Class, ex.Procedure, ex.LineNumber, ex.Server, ex.State);

        // 逐條 SqlError 詳細檢查 (批次可能多條)
        foreach (SqlError err in ex.Errors)
        {
            Log.Error("SqlError Detail | Number={Number} | Message={Message} | Severity={Severity} | State={State} | Procedure={Procedure} | Line={Line}",
                err.Number, err.Message, err.Class, err.State, err.Procedure, err.LineNumber);
        }

        // 業務邏輯分流:依錯誤號決定重試/拒絕/告警
        switch (ex.Number)
        {
            case 2627:  // PK/Unique 違反
            case 50001: // 自訂:使用者已存在
                return false; // 友善提示用戶
            case 1205:  // 死結
                throw new RetryableException("資料庫忙碌,請稍後重試", ex);
            default:
                throw; // 未知嚴重錯誤向上拋出
        }
    }
}

四、InfoMessage 事件:捕捉 PRINT 與低嚴重度訊息

RAISERROR severity ≤ 10 或 PRINT 陳述式**不會拋出例外**,而是觸發 SqlConnection.InfoMessage 事件。常用於:長時間作業進度回報、ETL 步驟日誌、除錯資訊傳遞。

T-SQL 端:

CREATE PROC P_CLEAR_DATA
AS
DELETE FROM T_USERS_IN_ROLES;
DELETE FROM T_USERS;
DELETE FROM T_ROLES;
PRINT N'所有資料已清除完成!'; -- Severity 0,觸發 InfoMessage
GO

ADO.NET 端訂閱事件:

using (var conn = new SqlConnection(connStr))
{
    conn.InfoMessage += (sender, e) =>
    {
        // e.Errors 為 SqlErrorCollection,同 SqlException.Errors
        foreach (SqlError err in e.Errors)
        {
            Console.WriteLine($"[INFO] {err.Message} | Proc={err.Procedure} | Line={err.LineNumber}");
            // 可寫入應用日誌、推送 SignalR 進度至前端、更新作業狀態表
        }
    };
    // 必須設定 FireInfoMessageEventOnUserErrors = true 才能接收 severity 1-10 的訊息
    conn.FireInfoMessageEventOnUserErrors = true;

    using (var cmd = new SqlCommand("P_CLEAR_DATA", conn) { CommandType = CommandType.StoredProcedure })
    {
        conn.Open();
        cmd.ExecuteNonQuery();
    }
}
// 輸出:[INFO] 所有資料已清除完成! | Proc=P_CLEAR_DATA | Line=5

五、最佳實務總結

場景T-SQL 寫法.NET 處理關鍵點
業務錯誤 (可預期、可處理)RAISERROR(message_id, 16, 1)Catch SqlException, 依 ex.Number 分流使用 sys.messages 管理訊息,Number 精確映射
系統/不可預期錯誤TRY/CATCH + THROW / RAISERROR(@msg, 16, 1)Catch SqlException, 記錄完整堆疊,向上拋出THROW 保留原始堆疊 (SQL 2012+)
進度/資訊/除錯訊息PRINT / RAISERROR('msg', 0, 1) / RAISERROR('msg', 10, 1)訂閱 conn.InfoMessage, 設定 FireInfoMessageEventOnUserErrors=true不中斷執行流程,非同步接收
批次多錯誤多條 RAISERROR遍歷 ex.Errors / e.Errors 集合單一 SqlException 包含多條 SqlError

結語

掌握 SqlException 屬性映射與 InfoMessage 事件機制,能構建「錯誤精確識別、資訊即時傳遞、日誌結構化、業務優雅降級」的完整異常處理體系。核心原則:**業務錯誤用自訂 message_id (Number 精確)、系統錯誤用 THROW 保留堆疊、資訊流用 InfoMessage 非阻塞傳遞**。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved