對SQL Server 2005應用TRY...CATCH回傳事務

本文英文原版:
http://www.4guysfromrolla.com/webtech/041906-1.shtml

對SQL Server 2005應用TRY...CATCH回傳事務

導言:

SQL Server 2005相比以前的老版本提供了一些新的特性.本文我們將注意力集中在SQL Server 2005所支持的TRY...CATCH模塊.它是現代語言處理異常的標準方法,包括:

.一個TRY模塊—該模塊用來包含可能引發異常的指令
.一個CATCH模塊—如果在TRY模塊發生異常的話,程序控制將轉到CATCH模塊處理異常.

關於exception handling的概念以及TRY...CATCH構造器的更多信息請參閱文章《Exception Handling》

在SQL Server 2005之前,要檢查返回的T-SQL statement是否出錯,只能檢查一個全局的error變量@@ERROR.由於每執行一條SQL statement後,都會對@@ERROR進行重置,在每個statement執行後對該變量的檢查將會使儲過程顯的臃腫.

SQL Server 2005支持的TRY...CATCH模塊提供了一種易讀性更強、開發者更熟悉的方法來進行處理.本文,我們將看到在出現問題時如何運用TRY...CATCH模塊來回滾事務.


檢查@@ERROR值——T-SQL裏處理Errors的一種舊方法

SQL Server用@@ERROR變量來指示剛執行的SQL statement的狀態。如果執行完全成功,那麼值爲0,如果發生了錯誤,那麼@@ERROR就被設爲error message的條數.

@@ERROR變量是怎麼使用的呢?假定我們有一個數據驅動web應用程序,它包含員工信息.假設在數據庫裏有Employees 和 EmployeePhoneNumbers以及其它的table.這2個表的關係爲一對多;也就是說每一條Employees記錄在 EmployeePhoneNumbers表裏有相應的任意多條記錄,比如可能是辦公室號碼、呼機號碼等.假定我們有一個名爲 DeleteEmployee的存儲過程,它有2條DELETE statements,一個是刪除員工的相關電話號碼,而另一個是刪除實際的員工記錄:

CREATE PROCEDURE DeleteEmployee ( @EmployeeID int )
AS

-- Delete the Employee's phone numbers
DELETE FROM EmployeePhoneNumbers
WHERE EmployeeID = @EmployeeID

-- Delete the Employee record
DELETE FROM Employees
WHERE EmployeeID = @EmployeeID

因爲我們想讓這2條delete statement同時執行成功或失敗,因此我們要將這2個statements封裝到一個事務裏.利用事務,在出現異常時我們便可以回滾事務,因而不會對數據庫進行改動.爲此,我們最開始可能要使用這樣的語法:

CREATE PROCEDURE DeleteEmployee ( @EmployeeID int )
AS

BEGIN TRANSACTION    -- Start the transaction

-- Delete the Employee's phone numbers
DELETE FROM EmployeePhoneNumbers
WHERE EmployeeID = @EmployeeID

-- Delete the Employee record
DELETE FROM Employees
WHERE EmployeeID = @EmployeeID


-- See if there is an error
IF @@ERROR <> 0
  -- There's an error b/c @ERROR is not 0, rollback
  ROLLBACK
ELSE
  COMMIT   -- Success!  Commit the transaction

該存儲過程(似乎)開啓了一個事務,運行這2個DELETE statement,再檢查看是否有異常,如果有哪怕一異常,它都會回滾事務,如果沒有那就提交事務.我說“似乎”,那是因爲嚴格來說,該語法在語義上是不正確的,因爲每執行一個DELETE statement後都會重置@@ERROR變量的值.因此,如果第一個DELETE statement有異常的話,那麼@@ERROR變量的值就會重置爲其error的條數.然後再執行第二個DELETE.如果第二個DELETE執行成功,那麼@@ERROR的值將會設置爲0,在這種情況下,就算第一個statement有問題也會提交事務!多麼可怕!

因此,我們必須在每一個SQL statement執行後進行檢查,看是否有錯誤.如果有則回滾事務並退出存儲過程.這樣的話就會使存儲過程變得臃腫,比如,對5個statement就要檢查5次@@ERROR變量.如果你漏掉了某條statement的檢查那麼就會出現潛在的問題.

關於事務和@@ERROR語法的更多信息,請參閱文章《Managing Transactions in SQL Server Stored Procedures》


用SQL Server 2005的TRY...CATCH模塊處理Errors

雖然SQL Server 2005依然支持@@ERROR方法,但還有一個更好的處理方法,那就是新特性TRY...CATCH模塊.就像程序語言Visual Basic, C#,以及Java一樣,SQL Server 2005的TRY...CATCH模塊在TRY模塊裏執行一系列的statement,如果執行這些statement都沒有問題那麼程序就跳過 CATCH模塊.相反,如果有異常那麼程序就轉入CATCH模塊。此外,類似程序設計語言,TRY...CATCH模塊允許進行鑲套,那就是說在外部 TRY...CATCH模塊的TRY 或 CATCH裏我們可以再鑲套一個完整的TRY...CATCH模塊.

BEGIN TRY
   Try Statement 1
   Try Statement 2
   ...
   Try Statement M
END TRY
BEGIN CATCH
   Catch Statement 1
   Catch Statement 2
   ...
   Catch Statement N
END CATCH

以下是在CATCH模塊裏可以用到的系統函數,我們可以用它來探測錯誤信息:

我們應該意識到並非TRY模塊裏statement引發的所有的錯誤都會使程序控制轉到CATCH模塊.那些安全級別爲10及以下的錯誤會被判斷爲“警告”級別的錯誤,因而不會轉入CATCH模塊.另外有關數據庫連接的錯誤也不會導致轉入CATCH模塊.

讓我們快速的看一個TRY...CATCH的例子,然後我們將注意力轉移到出現異常時用TRY...CATCH回滾事務的情況.下面的示例將向 Northwind數據庫的Products表添加一個簡單的INSERT查詢. 由於Products表的ProductID列是一個值自增列(IDENTITY column),因此當添加一個新記錄時不能指定該列的值,不過我在下面的INSERT statement裏指定了該列的值.因此,程序控制將轉入CATCH模塊,並將錯誤信息展示出來.

BEGIN TRY
   -- This will generate an error, as ProductID is an IDENTITY column
   -- Ergo, we can't specify a value for this column...
   INSERT INTO Products(ProductID, ProductName)
   VALUES(1, 'Test')
END TRY
BEGIN CATCH
   SELECT 'There was an error! ' + ERROR_MESSAGE()
END CATCH

這樣的話將返回一個單列的記錄,內容爲:"There was an error! Cannot insert explicit value for identity column in table 'Products' when IDENTITY_INSERT is set to OFF."

出現異常時,使用TRY...CATCH來回滾事務

正如在本文前面所探討的那樣, 在事務裏的每一個SQL statement之後都應該用一個@@ERROR變量來探測是否發生了異常,如果發生了那就回滾事務.不過用SQL Server 2005的TRY...CATCH模塊的話就可以極大的進行簡化,如下所示:

CREATE PROCEDURE DeleteEmployee ( @EmployeeID int )
AS

BEGIN TRY
   BEGIN TRANSACTION    -- Start the transaction

   -- Delete the Employee's phone numbers
   DELETE FROM EmployeePhoneNumbers
   WHERE EmployeeID = @EmployeeID

   -- Delete the Employee record
   DELETE FROM Employees
   WHERE EmployeeID = @EmployeeID

   -- If we reach here, success!
   COMMIT
END TRY
BEGIN CATCH
  -- Whoops, there was an error
  IF @@TRANCOUNT > 0
     ROLLBACK

  -- Raise an error with the details of the exception
  DECLARE @ErrMsg nvarchar(4000), @ErrSeverity int
  SELECT @ErrMsg = ERROR_MESSAGE(),
         @ErrSeverity = ERROR_SEVERITY()

  RAISERROR(@ErrMsg, @ErrSeverity, 1)
END CATCH

在TRY模塊,開啓一個事務並執行2個DELETE statements.如果這2個DELETE執行成功那麼就用COMMIT來提交事務.如果有異常,那麼程序控制就會轉入CATCH模塊,回滾事務,此外,CATCH模塊還使用RAISERROR來再次引發錯誤,這樣該error信息就會傳遞到調用該存儲過程的應用程序.對ASP.NET web應用程序而言,這就意味着,在調用該存儲過程的.NET代碼處引發一個異常,我們不僅希望在此回傳事務而且還希望在web應用程序處理一些出錯信息以告知終端用戶他們的操作失敗了.

在CATCH模塊裏調用RAISERROR的效果等同於在編程語言的TRY...CATCH模塊的CATCH裏拋出一個異常.上面示例的最終結果將回滾事務.如果您忽略了RAISERROR,那麼在執行數據庫命令時,ASP.NET應用程序就不會拋出一個異常.使用RAISERROR的話你就可以在 ASP.NET裏拋出異常(不然的話,如果操作失敗了你還不知道是怎麼回事情).

結語:

SQL Server 2005所支持的TRY...CATCH模塊將我們熟悉的TRY...CATCH處理方式引入到T-SQL裏.在2005之前的版本里,要探測錯誤只能通過使用@@ERROR變量.在每執行一條SQL statement之後都要對它進行檢查,很煩人.這將導致代碼臃腫且在複製、粘貼的時候很容易出問題,而導致潛在的重大威脅.有了SQL Server 2005的TRY...CATCH模塊,那就是另一番光景了。任何時候TRY模塊裏的任何一條statement所引發的錯誤都會使程序控制轉入 CATCH模塊,正如我們在本文所看到的那樣,TRY...CATCH提供了更易懂更簡潔的處理方式.


祝編程愉快!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章