1. 程式人生 > >SQL Server 數據庫try catch 存儲過程

SQL Server 數據庫try catch 存儲過程

時間 程序員 sage sys tps datetime varchar count alt

SQL Server 在生產環境中這樣寫存儲過程的坑都避免了嗎?

原文鏈接: http://www.cnblogs.com/chenmh/p/7856777.html

概述

最近因為業務的需求寫了一段時間存儲過程,發現之前寫的存儲過程存在一些不嚴謹的地方,特別是TRY...CATCH中嵌套事務的寫法;雖然之前寫的並沒有錯,但是還是埋藏著很大的隱患在裏面。希望這篇文章能給大家一些參考;文章內容有點長還望耐心閱讀。

1.插入測試數據

技術分享圖片
----創建表
DROP TABLE score
GO
CREATE TABLE [dbo].[score](
    id INT NOT NULL PRIMARY KEY IDENTITY(1,1),
    name VARCHAR(50) NOT NULL,
    score INT NOT NULL CHECK (score>=0),
    months INT NOT NULL,
    createtime DATETIME NOT NULL DEFAULT GETDATE()
)

---根據姓名月份查詢分數
CREATE INDEX IX_score_name ON score(name,months) include(score)
---根據月份查詢最高分數
CREATE INDEX IX_score_months ON score(months) include(name,score)
---創建姓名和月份組合的唯一索引
CREATE UNIQUE INDEX IX_score_months_name ON score(months,name)

------插入測試數據
TRUNCATE TABLE score

INSERT INTO score(name,score,months) 
VALUES(li‘,50,10),(chen‘,70,10),(zhang‘,80,10),(wang‘,90,10),(li‘,50,11),(chen‘,70,11),(zhang‘,80,11),(wang‘,90,11)

SELECT * FROM score;
技術分享圖片

技術分享圖片

2、THROW

THROW是在2012版本中引入的,在有些場景當中,應用程序端不做一些合法性的驗證,這些驗證會被放在數據庫端來驗證。當數據庫端驗證輸入的信息不合法時需要主動拋出異常來中斷代碼的執行。

THROW既可以接收錯誤信息拋錯提示,同時也可以手動拋出錯誤到CATCH中。語法如下:

技術分享圖片
;THROW

THROW [ { error_number | @local_variable }, 

        { message | @local_variable }, 

        { state | @local_variable } ]  

[ ; ]

參數

error_number
表示異常的常量或變量。 error_number是int並且必須為大於或等於 50000 且小於或等於 2147483647,如果CATCH中使用RAISERROR來接收錯誤信息那麽指定的error_number必須在sys.messages 中存在;如果使用CATCH來接收則不需要。

消息
描述異常的字符串或變量。 消息是nvarchar(2048)。

狀態
在 0 到 255 之間的常量或變量,指示與消息關聯的狀態。 狀態是tinyint。
技術分享圖片

註意:

1.THROW代碼前必須要用分號,因為THROW會中斷代碼的執行,所以如果將THROW放在CATCH中時必須放在ROLLBACK TRAN之後,否則不會回滾事務導致對象一直處於提交狀態被鎖。

2.THROW放CATCH中可以達到RAISERROR一樣的效果,同時還簡便了代碼。

3. THROW能返回正確的錯誤代碼行號,而RAISERROR沒辦法

參考:https://docs.microsoft.com/zh-cn/sql/t-sql/language-elements/throw-transact-sql

3.sp_addmessage

自定義錯誤號

技術分享圖片
EXEC sp_addmessage  
    @msgnum = 60000,  
    @severity = 16,  
    @msgtext = NManual cast wrong ,
    @lang = us_english; 

EXEC sp_addmessage  
    @msgnum = 60000,  
    @severity = 16,  
    @msgtext = N手動拋錯,  
    @lang = 簡體中文‘;
技術分享圖片

註意:自定義錯誤號必須大於50000

二、調用存儲過程

1.查詢存儲過程

技術分享圖片
----查詢存儲過程
CREATE PROCEDURE  Pro_score
(@Option VARCHAR(50),
 @name VARCHAR(50)=‘‘,
 @months INT=‘‘
)
AS
BEGIN

---查詢指定人分數
IF @Option=GetScoreBEGIN
        SELECT name,
               score
        FROM score
        WHERE name=@name

   END

----查詢指定月份最高分數
IF @Option=MonthMaxScoreBEGIN
        SELECT Top 1 
             name,
             score
        FROM score
        WHERE months=@months
        ORDER BY score

   END



END
技術分享圖片

調用存儲過程:

EXEC Pro_score @Option=‘GetScore‘,@name=‘li‘
EXEC Pro_score @Option=‘MonthMaxScore‘,@months=11

技術分享圖片

3.修改存儲過程

技術分享圖片
 1 CREATE PROCEDURE [dbo].[Pro_Insert_score]
 2 (@Option VARCHAR(50),
 3  @name VARCHAR(50)=‘‘,
 4  @months INT=0,
 5  @score INT=0
 6 ) 
 7 AS
 8 BEGIN
 9 DECLARE @ErrorNum INT,@ErrorSeverity INT,@ErrorState INT,@ErrorLine INT,@ErrorPro VARCHAR(200),@ErrorMessage NVARCHAR(4000);
10 IF @Option=InsertScore11     BEGIN
12     
13              -----使用事務
14                   BEGIN TRY  
15                         BEGIN TRAN 
16                         INSERT INTO score(name,score,months) 
17                         VALUES(@name,@score,@months)
18                     
19                         ----插入重復值報錯事務回滾
20                         INSERT INTO score(name,score,months) 
21                         VALUES(@name,@score,@months)
22 
23                         COMMIT TRAN  
24         
25                     END TRY  
26                     BEGIN CATCH 
27                          SELECT  @ErrorMessage = ERROR_MESSAGE(),@ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE();
28                          RAISERROR (@ErrorMessage,@ErrorSeverity,@ErrorState) ;
29                         ROLLBACK TRAN
30                         ;THROW
31                         ----執行失敗
32                         RETURN 1
33                     END CATCH
34 
35         ----執行成功
36         RETURN 0
37     END
38 
39 END
技術分享圖片

調用存儲過程

----調用存儲過程2
DECLARE @status INT
EXEC @status=Pro_Insert_score @Option=InsertScore‘,@name=chen‘,@months=12,@score=90
SELECT @status

技術分享圖片

可以發現使用RAISERROR拋錯出來的行號和消息號都是錯誤的,50000這個消息號其實是不存在的,它是保留的一個統一的消息號。

可以通過查詢sys.message查詢對應的消息號

SELECT * FROM score WHERE name=chenSELECT * FROM sys.messages WHERE message_id=2601 and language_id=2052

技術分享圖片

4.手動拋錯中斷

手動拋錯也是這篇文章主要要講的一個知識點,在有一些業務場景當中有一些驗證操作需要在數據庫中進行,甚至必須在更新之後進行但是又是正常的提交操作,在這種情況下就需要手動進行驗證是否需要執行下面的代碼。,見過很多程序員寫存儲過程喜歡在每一個判斷的地方加上RETURN操作,目的是為了不執行後面的代碼,同時又在RETURN前加上ROLLBACK操作。這雖然是一個辦法,但是在事務中運用RETURN是一個很危險的操作,弄不好會導致事務一直處於打開操作導致表一直被鎖住,在生成環境是很危險的操作。

建議使用THROW來手動進行拋錯,THROW拋錯會相當於觸發一個11-19級別的錯誤,這樣會跳到CATCH中做ROLLBACK操作。

註意:THROW前必須以分號開頭,如果THROW前有代碼以分號結尾也可以。

技術分享圖片
CREATE PROCEDURE [dbo].[Pro_score_throw]
(@Option VARCHAR(50),
 @name VARCHAR(50)=‘‘,
 @months INT=0,
 @score INT=0
) 
AS
BEGIN
DECLARE @ErrorNum INT,@ErrorSeverity INT,@ErrorState INT,@ErrorLine INT,@ErrorPro VARCHAR(200),@ErrorMessage NVARCHAR(4000);
IF @Option=UpdateScoreBEGIN
    
             -----使用事務
                  BEGIN TRY  
                        BEGIN TRAN 
                        UPDATE score
                        SET score=score+@score
                        WHERE name=@name AND months=@months

                        ----在有些業務場景有些判斷必須等操作完了才能去做判斷是否能繼續執行下去
                        IF (SELECT score FROM score WHERE name=@name AND months=@months)>100
                        BEGIN
                             
                             ;THROW 60000,分數不能大於100‘,111 

                        END
                        COMMIT TRAN  
        
                    END TRY  
                    BEGIN CATCH 

ROLLBACK TRAN ;THROW END CATCH ----執行成功 RETURN 0 END END
技術分享圖片

調用存儲過程

DECLARE @status INT
EXEC @status=Pro_score_throw @Option=UpdateScore‘,@name=chen‘,@months=10,@score=40
SELECT @status

技術分享圖片

5.存儲過程調用存儲過程

技術分享圖片
CREATE PROCEDURE [dbo].[Pro_score_ProcToProc]
(@Option VARCHAR(50),
 @name VARCHAR(50)=‘‘,
 @months INT=0,
 @score INT=0
) 
AS
BEGIN
DECLARE @ErrorNum INT,@ErrorSeverity INT,@ErrorState INT,@ErrorLine INT,@ErrorPro VARCHAR(200),@ErrorMessage NVARCHAR(4000);
    IF @Option=UpdateBEGIN
             ----判斷修改的人是否存在
             IF NOT EXISTS(SELECT * FROM score WHERE name=@name)
             BEGIN
                 ---修改人不存在
                 RETURN 2
             END
             ELSE
             BEGIN
             -----使用事務
                  BEGIN TRY  
                        BEGIN TRAN 
                               UPDATE score
                               SET createtime=1900-01-01 00:00:000WHERE name=@name AND months=@months

                               SELECT name,months,createtime,score FROM score  WHERE name=@name AND months=@months
                               ---定義事務保存點
                               ---SAVE TRAN TRAN1
                               ----調用別的存儲過程
                               EXEC Pro_score_ProcToProc @Option=UpdateScore‘,@name=@name,@months=@months,@score=@score

                        COMMIT TRAN  
        
                    END TRY  
                    BEGIN CATCH 
                        SELECT name,months,createtime,score FROM score  WHERE name=@name AND months=@months
                        IF @@TRANCOUNT > 0
                        ROLLBACK TRAN ;
                        SELECT name,months,createtime,score FROM score  WHERE name=@name AND months=@months
                        ;THROW
                    END CATCH
             END
        ----執行成功     
         RETURN 0
     END

    IF @Option=UpdateScoreBEGIN
    
             ---使用事務
                  BEGIN TRY  
                        BEGIN TRAN 
                        UPDATE score
                        SET score=score+@score
                        WHERE name=@name AND months=@months

                        ----在有些業務場景有些判斷必須等操作完了才能去做判斷是否能繼續執行下去
                        IF (SELECT score FROM score WHERE name=@name AND months=@months)>100
                        BEGIN
                             
                             ;THROW 60000,‘分數不能大於100‘,111 

                        END
                        COMMIT TRAN 
        
                    END TRY  
                    BEGIN CATCH 
                         ----回滾到指定保存點
                         ----ROLLBACK TRAN TRAN1

                         --回滾事務
                         ROLLBACK TRAN
                        ----執行失敗
                        ;THROW
                    END CATCH

    END
END
技術分享圖片

存儲過程調用存儲過程事務的三種處理方法:

1.內部存儲過程不要包含事務,因為內部ROLLBACK會直接回滾到外部的BEGIN TRAN導致外部的ROLLBACK沒有對應的COMMIT;

2.還有一種方法是在調用內部存儲過程之前使用保存點“SAVE TRAN TRAN1”,同時內部存儲過程的ROLLBACK TRAN必須指定事務保存點,例如“ROLLBACK TRAN TRAN1”,這樣內部存儲過程回滾就只會回滾到保持點.

3.在外部存儲過程的CATCH塊的ROLLBACK前加上IF @@TRANCOUNT > 0判斷條件

事務嵌套事務的理解

技術分享圖片
---事務1
BEGIN TRAN
        ---事務2
        BEGIN TRAN

        COMMIT TRAN /ROLLBACK TRAN 

COMMIT TRAN /ROLLBACK TRAN 
技術分享圖片

對於事務嵌套事務,事務2的ROLLBACK操作會直接回滾到事務1的BEGIN TRAN,會導致事務1的ROLLBACK沒有對應的BEGIN TRAN。處理方法可以在調用事務2之前定義一個事務保存點或者在事務1的ROLLBACK前加上IF @@TRANCOUNT > 0判斷條件是否存在事務需要回滾。

SET XACT_ABORT ON

並不是所有的錯誤都能被CATCH所接收。對於嚴重級別為0-10(信息性消息)和20-25(致命的消息)是不能被CATCH所接收的,這時如果在事務中遇到了這類的報錯那麽通用會導致事務處理打開狀態,這時就需要開啟XACT_ABORT。當開啟XACT_ABORT後只要代碼中存在報錯就會執行回滾操作,而不管錯誤的級別。例如:

技術分享圖片
CREATE TABLE [dbo].[AA](
    [id] [int] NULL
) ON [PRIMARY]
GO
CREATE PROC Pro_bb
(@Option VARCHAR(50))
AS
BEGIN
IF @OPTION=aBEGIN
       TRUNCATE TABLE AA;
       SELECT * FROM AA;
        ----事務1
        BEGIN TRY  
        BEGIN TRAN  
            INSERT INTO AA SELECT 2  
            SELECT * FROM AA;
            INSERT INTO #BB SELECT 1 
            COMMIT TRAN;  
        END TRY  
        BEGIN CATCH  
            IF @@TRANCOUNT > 0
            ROLLBACK TRAN;  
            ;THROW
        END CATCH 
    END
END
技術分享圖片

由於臨時表#BB不存在,導致插入報錯,但是嚴重級別又小於11導致CATCH接收不到錯誤,這時查看發現事務處於打開狀態,而且表AA也被鎖住。

EXEC Pro_bb @OPTION=a;
DBCC OPENTRAN;

技術分享圖片

加上事務前加上 SET XACT_ABORT ON

技術分享圖片
ALTER TABLE [dbo].[AA](
    [id] [int] NULL
) ON [PRIMARY]
GO
CREATE PROC Pro_bb
(@Option VARCHAR(50))
AS
BEGIN
IF @OPTION=aBEGIN
   SET XACT_ABORT ON  
       TRUNCATE TABLE AA;
       SELECT * FROM AA;
        ----事務1
        BEGIN TRY  
        BEGIN TRAN  
            INSERT INTO AA SELECT 2  
            SELECT * FROM AA;
            INSERT INTO #BB SELECT 1 
            COMMIT TRAN;  
        END TRY  
        BEGIN CATCH  
            IF @@TRANCOUNT > 0
            ROLLBACK TRAN;  
            ;THROW
        END CATCH 
    END
END
技術分享圖片

再次執行

EXEC Pro_bb @OPTION=a;
DBCC OPENTRAN;

技術分享圖片

沒有處於打開的事務而且事務也執行了回滾操作。

總結

1.建議2012以後版本所有的接收拋錯改成使用THROW,不要使用THROW拋錯又使用RAISERROR來介紹錯誤,在事務嵌套事務的寫法中如果內部事務使用RAISERROR來接收THROW返回的報錯不會執行後面的ROLLBACK。

2.建議在ROLLBACK前統一加上IF @@TRANCOUNT > 0判斷條件,這樣可以避免因為內部的ROLLBACK回滾或者RETURN操作導致ROLLBACK沒有對應的COMMIT。

3.建議不要在事務內使用RETURN返回代碼錯誤位置,RETURN會跳出事務導致提示ROLLBACK沒有對應的COMMIT,嚴重的會導致事務一直處於打開不提交,THROW也可以指定錯誤位置。

SQL Server 數據庫try catch 存儲過程