SQL Server:使用原始异常编号的Rethrow异常

时间:2009-12-10 17:54:27

标签: c# asp.net sql sql-server-2008 exception-handling

我在存储过程中使用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指令。

8 个答案:

答案 0 :(得分:12)

您可以像这样重新抛出它:

..
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

答案 1 :(得分:8)

如果在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异常处理中。

答案 2 :(得分:1)

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

答案 3 :(得分:1)

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

由于无法重新抛出相同的错误,因此必须抛出一个非常容易映射到原始错误的错误,例如通过向每个系统错误添加固定数字(如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:
  {
  }
}

所以你甚至不必知道它是否是一个重新抛出或原始错误,它总是正确的,即使你忘记处理你的错误并且原来的错误也会消失。

在其他地方提到了一些增强功能,用于提出无法提出的消息或无法使用的状态。这些被遗漏在这里只是为了表明这个想法的核心。

答案 4 :(得分:1)

作为SQL Server&lt; 2010没有能力重新抛出,正确的方法是使用事务并明确检查错误状态(想想'C'比'C ++ / C#'更多)。

E.g。 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 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;

答案 5 :(得分:1)

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

答案 6 :(得分:0)

我使用以下模式:

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   

答案 7 :(得分:0)

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

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

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

transaction.Rollback();

否则:

transaction.Commit();

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

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