SQL Server:使用原始异常号重新引发异常

Posted

技术标签:

【中文标题】SQL Server:使用原始异常号重新引发异常【英文标题】:SQL Server: Rethrow exception with the original exception number 【发布时间】:2010-12-25 08:43:47 【问题描述】:

我在有两条 INSERT 指令的存储过程中使用 TRY CATCH 块。

如果出现问题,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 指令。

【问题讨论】:

【参考方案1】:

你也许可以像这样重新抛出它:

..
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 error handling: exceptions and the database-client contract

【讨论】:

【参考方案2】:

如果您在 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 异常处理没有无缝集成到 CLR 异常处理中。

【讨论】:

来自 MSDN.... "CATCH 块可以使用 RAISERROR 重新抛出调用 CATCH 块的错误,方法是使用 ERROR_NUMBER 和 ERROR_MESSAGE 等系统函数来检索原始错误信息。@@ERROR 已设置对于严重性从 1 到 10 的消息,默认为 0。”所以重投对2005+有效msdn.microsoft.com/en-us/library/ms178592.aspx 只允许创建用户消息 > 50000 但也可以将错误消息从 13000 提高到 49999。 “没有办法重新抛出原始异常”——SQL Server 2012(兼容级别 110)和更高版本的支持“THROW;”,正是这样做的。【参考方案3】:

这是我用来解决此问题的代码(从 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:"),例如。

【讨论】:

这也是可行的,但以某种方式从带有其他内容的字符串中检索数字对我来说似乎不是一个好习惯。【参考方案4】:

我想了一会儿这个话题,想出了一个我以前没有见过的非常简单的解决方案,所以我想分享一下:

由于不可能重新抛出相同的错误,因此必须抛出一个很容易映射到原始错误的错误,例如通过为每个系统错误添加一个固定数字(如 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 (line: %d procedure: %s)%s',这样您就可以提供原始消息作为第一个参数并附加真实的过程和行以及您自己的消息作为其他参数。

在客户端中,您现在可以进行所有普通的异常处理,就像原始消息会被抛出一样,您只需要记住添加修复偏移量。您甚至可以使用相同的代码处理原始异常和重新引发的异常,如下所示:

switch(errorNumber)

  case   8134:
  case 108134:
  
  

所以你甚至不必知道它是重新抛出的还是原来的错误,它总是正确的,即使你忘记处理你的错误并且原来的错误漏掉了。

在其他地方提到了一些关于提出你不能提出的消息或你不能使用的状态的增强。这些被忽略在这里只显示这个想法的核心。

【讨论】:

【参考方案5】:

由于 SQL Server

例如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;

这是一个适用于托管环境的解决方案(您可能无法创建自己的错误消息)。

虽然不如使用异常方便,但这种方法将保留系统错误代码。它还具有每次执行能够返回多个错误的(不利)优势。

如果您只想解决第一个错误,请插入 return 语句,或者如果您觉得很勇敢,请转到错误块(记住:Go To Statement Considered Harmful),例如:

(其中的元素取自 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;

【讨论】:

【参考方案6】:

从 SQL Server 2012(兼容级别 110)开始,您现在可以在 CATCH 块中执行 THROW; 以重新抛出原始异常,即使是系统错误也保留原始错误号。

【讨论】:

【参考方案7】:

我使用以下模式:

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   

【讨论】:

抱歉,怎么办?这仍然会抛出 50000,不建议用于 SQL Server 2005。OP 还提到已经使用 TRY/CATCH @gbn 你对我们如何做到这一点有任何想法吗?【参考方案8】:

谢谢你们的回答。从重新抛出异常的消息中获取错误是我已经做过的事情。

@gbn 我也喜欢 gbn 答案,但我会坚持这个答案,因为它是最有效的答案,我在这里发布它希望它对其他人也有用。

答案是在应用程序中使用事务。如果我没有在存储过程中捕获异常,我将在 SqlException 对象中获取原始编号。在应用程序中捕获原始异常后,我编写了以下代码

transaction.Rollback();

否则:

transaction.Commit();

这比我最初预期的要简单得多!

http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqltransaction.aspx

【讨论】:

如果您只想捕获原始错误,我认为您甚至不需要事务:​​只是不要在 T-SQL 端使用 TRY/CATCH 或 RAISERROR()。此外,TransactionScope 方法适用于简单的查询或 SP,但在更复杂的 SP 的极端情况下,它不会做正确的事情。 @RickNZ,我忘了在问题中提到我正在使用两个 INSERT 指令,这就是需要交易的原因。不过贡献不错!

以上是关于SQL Server:使用原始异常号重新引发异常的主要内容,如果未能解决你的问题,请参考以下文章

如何从 PowerShell 中的 catch 块中重新引发异常?

将 UDF 从 MS SQL Server 移植到 MySQL 会引发异常不正确的双精度值

在不丢失堆栈跟踪的情况下重新抛出 Java 中的异常

使用不同的类型和消息重新引发异常,保留现有信息

SQL server 获取异常

在 Ruby 中捕获异常后重新引发(相同的异常)