[英]SQL Server: Rethrow exception with the original exception number
我在存儲過程中使用TRY CATCH塊,其中有兩個INSERT指令。
如果出現問題,CATCH塊會負責回滾所做的所有更改並且工作正常,除了一件事!
我的ASP.NET應用程序捕獲的異常是一個數字為50000的SqlException。這不是原始數字! (我期待的數字是2627)
在異常的Message屬性中,我可以看到原始的異常編號和格式化的消息。
我怎樣才能獲得原始的例外號碼?
try
{
// ... code
}
catch
(SqlException sqlException)
{
switch (sqlException.Number)
{
// Name already exists
case 2627:
throw new ItemTypeNameAlreadyExistsException();
// Some other error
// As the exception number is 50000 it always ends here!!!!!!
default:
throw new ItemTypeException();
}
}
現在,返回值已被使用。 我想我可以使用輸出參數來獲取異常編號,但這是個好主意嗎?
我該怎么做才能獲得例外號碼? 謝謝
PS:這是必需的,因為我有兩個INSERT指令。
您可以像這樣重新拋出它:
..
END TRY
BEGIN CATCH
DECLARE @errnum int;
SELECT @errnum = ERROR_NUMBER();
RAISERROR (@errnum, 16, 1);
END CATCH
但是,由於ERROR_NUMBER()的sys.messages行中的%s等占位符,您很可能會失去意義。
你可以做這樣的事情來包括數字並重新拋出原始信息
..
END TRY
BEGIN CATCH
DECLARE @errnum nchar(5), @errmsg nvarchar(2048);
SELECT
@errnum = RIGHT('00000' + ERROR_NUMBER(), 5),
@errmsg = @errnum + ' ' + ERROR_MESSAGE();
RAISERROR (@errmsg, 16, 1);
END CATCH
前5個字符是原始數字。
但是如果你有嵌套代碼,那么你最終會得到“00123 00456錯誤文本”。
就個人而言,我只處理SQL異常數字以將我的錯誤(50000)與我的代碼未運行的引擎錯誤(例如缺少參數)分開。
最后,您可以將其傳遞給返回值。
我問了一個問題: SQL Server錯誤處理:異常和數據庫 - 客戶端契約
如果在T-SQL中使用BEGIN TRY / BEGIN CATCH,則會丟失原始引擎引發的異常。 您不應該手動引發系統錯誤,因此您無法重新提出原始錯誤號2627.T-SQL錯誤處理與C#/ C ++錯誤處理不相似,沒有辦法重新拋出原始錯誤例外。 存在這種限制的原因有很多,但足以說明這一點存在,你不能忽視它。
但是,只要它們高於50000范圍,提出自己的錯誤代碼就沒有限制。 安裝應用程序時,使用sp_addmessage注冊自己的消息:
exec sp_addmessage 50001, 16, N'A primary key constraint failed: %s';
在你的T-SQL中你會引發新的錯誤:
@error_message = ERROR_MESSAGE();
raiserror(50001, 16, 1, @error_message;
在C#代碼中,您將查找錯誤號50001而不是2627:
foreach(SqlError error in sqlException.Errors)
{
switch (error.Number)
{
case 50001:
// handle PK violation
case 50002:
//
}
}
我有一個更簡單的答案,但遺憾的是這就是事情的方式。 T-SQL異常處理沒有將seamlesly集成到CLR異常處理中。
由於SQL Server <2010無法重新拋出,正確的方法是使用事務並顯式檢查錯誤狀態(比'C ++ / C#'更多地考慮'C')。
例如,SP的主體看起來像:
CREATE PROCEDURE [MyProcedure]
@argument1 int,
@argument2 int,
@argument3 int
AS BEGIN
DECLARE @return_code int;
IF (@argument1 < 0) BEGIN
RAISERROR ("@argument1 invalid", 16, 1);
END;
/* Do extra checks here... */
/* Now do what we came to do. */
IF (@@ERROR = 0) BEGIN
BEGIN TRANSACTION;
INSERT INTO [Table1](column1, column2)
VALUES (@argument1, @argument2);
IF (@@ERROR = 0) BEGIN
INSERT INTO [Table2](column1, column2)
VALUES (@argument1, @argument3);
END;
IF (@@ERROR = 0) BEGIN
COMMIT TRANSACTION;
SET @return_code = 0;
END
ELSE BEGIN
ROLLBACK TRANSACTION;
SET @return_code = -1; /* Or something more meaningful... */
END;
END
ELSE BEGIN
SET @return_code = -1;
END;
RETURN @return_code;
END;
這是一個可以在托管環境中工作的解決方案(您可能無法創建自己的錯誤消息)。
雖然不如使用異常那么方便,但這種方法將保留系統錯誤代碼。 它還具有(dis)優勢,能夠在每次執行時返回多個錯誤。
如果你只是想在第一個錯誤上彈出,要么插入return語句,要么你感覺很勇敢,GOTO是一個錯誤塊(記住: Go To Statement認為有害 ),例如:
(取自ASP.NET帳戶管理的元素)
CREATE PROCEDURE [MyProcedure]
@argument1 int,
@argument2 int,
@argument3 int
AS BEGIN
DECLARE @return_code int = 0;
DECLARE @tranaction_started bit = 0; /* Did we start a transaction? */
IF (@argument1 < 0) BEGIN
RAISERROR ("@argument1 invalid", 16, 1);
RETURN -1; /* Or something more specific... */
/* Alternatively one could:
SET @return_code = -1;
GOTO ErrorCleanup;
*/
END;
/* Do extra checks here... */
/* Now do what we came to do. */
/* If no transaction exists, start one.
* This approach makes it safe to nest this SP inside a
* transaction, e.g. in another SP.
*/
IF (@@TRANCOUNT = 0) BEGIN
BEGIN TRANSACTION;
SET @transaction_started = 1;
END;
INSERT INTO [Table1](column1, column2)
VALUES (@argument1, @argument2);
IF (@@ERROR <> 0) BEGIN
SET @return_code = -1; /* Or something more specific... */
GOTO ErrorCleanup;
END;
INSERT INTO [Table2](column1, column2)
VALUES (@argument1, @argument3);
IF (@@ERROR <> 0) BEGIN
SET @return_code = -1; /* Or something more specific... */
GOTO ErrorCleanup;
END;
IF (@transaction_started = 1) BEGIN
/* ONLY commit the transaction if we started it! */
SET @transaction_started = 0;
COMMIT TRANSACTION;
END;
RETURN @return_code;
ErrorCleanup:
IF (@transaction_started = 1) BEGIN
/* We started the transaction, so roll it back */
ROLLBACK TRANSACTION;
END;
RETURN @return_code;
END;
這是我用來解決這個問題的代碼(從CATCH調用)。 它在消息文本中嵌入原始錯誤號:
CREATE PROCEDURE [dbo].[ErrorRaise]
AS
BEGIN
DECLARE @ErrorMessage NVARCHAR(4000)
DECLARE @ErrorSeverity INT
SET @ErrorMessage = CONVERT(VARCHAR(10), ERROR_NUMBER()) + ':' +
ERROR_MESSAGE()
SET @ErrorSeverity = ERROR_SEVERITY()
RAISERROR (@ErrorMessage, @ErrorSeverity, 1)
END
然后,您可以檢查SqlException.Message.Contains("2627:")
,例如。
我想了一下這個話題,想出了一個我以前沒見過的非常簡單的解決方案,所以我想分享一下:
因為不可能重新拋出相同的錯誤,所以必須拋出一個很容易映射到原始錯誤的錯誤,例如通過向每個系統錯誤添加一個固定數字(如100000)。
將新映射的消息添加到數據庫后,可能會拋出固定偏移量為100000的任何系統錯誤。
下面是創建映射消息的代碼(對於整個SQL Server實例,這只需要執行一次。通過在這種情況下添加適當的偏移量,例如100000,避免與其他用戶定義的消息發生沖突):
DECLARE messageCursor CURSOR
READ_ONLY
FOR select
message_id + 100000 as message_id, language_id, severity, is_event_logged, [text]
from
sys.messages
where
language_id = 1033
and
message_id < 50000
and
severity > 0
DECLARE
@id int,
@severity int,
@lang int,
@msgText nvarchar(1000),
@withLog bit,
@withLogString nvarchar(100)
OPEN messageCursor
FETCH NEXT FROM messageCursor INTO @id, @lang, @severity, @withLog, @msgText
WHILE (@@fetch_status <> -1)
BEGIN
IF (@@fetch_status <> -2)
BEGIN
set @withLogString = case @withLog when 0 then 'false' else 'true' end
exec sp_addmessage @id, @severity, @msgText, 'us_english', @withLogString, 'replace'
END
FETCH NEXT FROM messageCursor INTO @id, @lang, @severity, @withLog, @msgText
END
CLOSE messageCursor
DEALLOCATE messageCursor
這是用於引發新創建的錯誤代碼的代碼,該代碼具有與原始代碼的修復偏移量:
SELECT
@ErrorNumber = ERROR_NUMBER(),
@ErrorSeverity = ERROR_SEVERITY(),
@ErrorState = ERROR_STATE()
set @MappedNumber = @ErrorNumber + 100000;
RAISERROR
(
@MappedNumber,
@ErrorSeverity,
1
);
有一個小警告:在這種情況下,您無法自己提供消息。 但是這可以通過在sp_addmessage調用中添加額外的%s或通過將所有映射的消息更改為您自己的模式並在raiseerror調用中提供正確的參數來規避。 最好的事情是將所有消息設置為相同的模式,如'%s(行:%d過程:%s)%s',這樣您就可以將原始消息作為第一個參數提供並附加實際過程和行以及您自己的消息消息作為其他參數。
在客戶端中,您現在可以執行所有普通的異常處理,就像拋出原始消息一樣,您只需要記住添加修復偏移量。 您甚至可以使用相同的代碼處理原始和重新拋出的異常,如下所示:
switch(errorNumber)
{
case 8134:
case 108134:
{
}
}
因此,您甚至不必知道它是重新拋出還是原始錯誤,它總是正確的,即使您忘記處理錯誤並且原始錯誤也會消失。
其他地方提到了一些增強功能,用於提高您無法提出的消息或指出您無法使用的消息。 這些被遺漏在這里只是為了表明這個想法的核心。
從SQL Server 2012(兼容級別110)開始,您現在可以執行THROW;
在您的CATCH
塊中重新拋出原始異常,保留原始錯誤號,即使它是系統錯誤。
我使用以下模式:
CreatePROCEDURE [dbo].[MyProcedureName]
@SampleParameter Integer,
[Other Paramaeters here]
As
Set NoCount On
Declare @Err Integer Set @Err = 0
Declare @ErrMsg VarChar(300)
-- ---- Input parameter value validation ------
Set @ErrMsg = ' @SampleParameter ' +
'must be either 1 or 2.'
If @SampleParameter Not In (1, 2) Goto Errhandler
-- ------------------------------------------
Begin Transaction
Set @ErrMsg = 'Failed to insert new record into TableName'
Insert TableName([ColumnList])
Values [ValueList])
Set @Err = @@Error If @Err <> 0 Goto Errhandler
-- ------------------------------------------
Set @ErrMsg = 'Failed to insert new record into Table2Name'
Insert TableName2([ColumnList])
Values [ValueList])
Set @Err = @@Error If @Err <> 0 Goto Errhandler
-- etc. etc..
Commit Transaction
Return 0
/* *************************************************/
/* ******* Exception Handler ***********************/
/* *************************************************/
/* *************************************************/
ErrHandler:
If @@TranCount > 0 RollBack Transaction
-- ------------------------------------
RaisError(@ErrMsg, 16, 1 )
If @Err = 0 Set @Err = -1
Return @Err
謝謝你們的回答。 從重新拋出的異常消息中獲取錯誤是我已經完成的事情。
@gbn我也很喜歡gbn的答案,但我會堅持這個答案,因為它是最有效的,我在這里發帖,希望它對其他人也有用。
答案是在應用程序中使用事務。 如果我沒有在存儲過程中捕獲異常,我將在SqlException對象中獲取原始數字。 在應用程序中捕獲原始異常后,我編寫以下代碼
transaction.Rollback();
除此以外:
transaction.Commit();
這比我最初預期的要簡單得多!
http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqltransaction.aspx
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.