简体   繁体   English

Sql异常未传递给C#捕获

[英]Sql Exception Not Being Passed to C# Catch

I have an automated script written in C# that runs a stored procedure on SQL Server 2014. The stored procedure is running multiple select, update, and insert statements and utilizes a try catch rollback pattern to catch and rollback the entire transaction when there's an exception. 我有一个用C#编写的自动化脚本,该脚本在SQL Server 2014上运行存储过程。该存储过程正在运行多个select,update和insert语句,并在出现异常时利用try catch rollback模式来捕获和回滚整个事务。

It looks similar to this: 它看起来类似于:

BEGIN TRY
    BEGIN TRANSACTION TransName
    --Lots of SQL!
    COMMIT TRANSACTION TransName
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION TransName;
    THROW
END CATCH    

My C# that calls the procedure looks similar to this: 我的C#调用过程看起来与此类似:

using (SqlCommand Command = new SqlCommand(query, Connection))
{
    // Retry several times if the query fails.
    for (var retry = 0; retry < 5 && !Success; ++retry)
    {
        try
        {
            Command.ExecuteNonQuery();
            Success = true;
        }
        catch (SqlException e)
        {
            // Handling for Timeout or deadlocks.
            // If not a timeout or deadlock and retry hasn't happened 4 times already.
            if (!(e.Number == 1205 || e.Number == 1204 || e.Number == -2) || retry == 4)
            {
                LogException(e);
            }
            else if (e.Number == 1205 || e.Number == 1204)
            {
                // Wait to avoid hammering the database.
                Thread.Sleep(500);
            }
            else if (e.Number == -2)
            {
                // Wait to avoid hammering the database.
                Thread.Sleep(5000);
            }

            Success = false;
        }
    }
}

I have it looping to make sure the SQL goes through if there is a deadlock or timeout since it's an automated script. 我循环它来确保SQL是通过自动脚本执行的,如果有死锁或超时,SQL将会通过。

In my logs for the script I can see that the stored procedure did not log any exceptions, but none of the data exists in the tables that the procedure touches which brings me to my question: 在脚本的日志中,我可以看到存储过程没有记录任何异常,但是该过程所涉及的表中没有任何数据,这使我想到了这个问题:

Is it possible for an exception to be caught in T-SQL and then thrown again using a T-SQL THROW statement but then the exception is not thrown in a C# client? 是否有可能在T-SQL中捕获异常,然后使用T-SQL THROW语句再次抛出该异常,但随后在C#客户端中没有抛出该异常?

Let me know if I can clarify anything. 让我知道是否可以澄清任何事情。 Thanks! 谢谢!

The try...catch in SQL works a little differently, what I have done in the past is to use OUTPUT variables on the stored procedure: SQL中的try...catch的工作原理略有不同,我过去所做的就是在存储过程中使用OUTPUT变量:

ALTER PROCEDURE dbo.yourStoredProcedure
    (-- your parameters
     @errNumber  INT OUTPUT,
     @errLine    INT OUTPUT,
     @errMessage VARCHAR(MAX) OUTPUT)
AS
BEGIN

    SET @errNumber  = 0
    SET @errLine    = 0
    SET @errMessage = ''

    BEGIN TRY
        BEGIN TRANSACTION TransName
        --Lots of SQL!
        COMMIT TRANSACTION TransName
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION TransName;

        SELECT   @errNumber  = ERROR_NUMBER()
        ,        @errLine    = ERROR_LINE()
        ,        @errMessage = ERROR_MESSAGE()
    END CATCH   
END
GO

And you would need to adjust the try within your C# to add the parameters and read the return values 并且您需要在C#中调整try来添加参数并读取返回值

    try
    {
        SqlParameter errNumber = new SqlParameter("@errNumber", 0);
        SqlParameter errLine = new SqlParameter("@errLine", 0);
        SqlParameter errMessage = new SqlParameter("@errMessage", "");

        Command.ExecuteNonQuery();

        int SqlError = (int)(errNumber.Value);
        int SqlLine = (int)(errNumber.Value);
        string SqlMessage = (string)errMessage.Value;

        if (SqlError == 0 ) { Success = true; }
        else {
            Success = false;
            // whatever else you want to do with the error data
        }
    }

Your SqlException catch would still catch the errors that were not within the procedures TRY...CATCH , and you should also have a generic Catch(Exception ex) block as well for other errors and finally don't forget the finally {} for any cleanup that may be needed. 您的SqlException捕获仍然会捕获TRY...CATCH过程中未包含的错误,并且您还应该具有通用的Catch(Exception ex)块来处理其他错误,最后不要忘记对任何其他命令使用finally {}可能需要清理。

Update 05/03/2017 更新05/03/2017

In most cases, wrapping a transaction within a try...catch leads to uncommitable transactions. 在大多数情况下,将事务包装在try...catch会导致无法提交的事务。 So we can flip the wrapping to have the try-catch within the transaction. 因此,我们可以翻转包装以在事务内进行try-catch。 If an error is caught then we should be able to get the error values and if again a transaction exists (@@transcount >0) it will be rolled back and @@transcount would be reduced to 0. After the the try-catch block is closed we again check @@transount and commit if one exists 如果捕获到错误,则我们应该能够获取错误值,并且如果再次存在事务(@@transcount >0) ,它将回滚并将@@ transcount减少为0。try-catch块之后关闭后,我们再次检查@@ transount并提交(如果存在)

BEGIN TRANSACTION TransName

BEGIN TRY
    --Lots of SQL!
END TRY

BEGIN CATCH
    SELECT   @errNumber  = ERROR_NUMBER()
    ,        @errLine    = ERROR_LINE()
    ,        @errMessage = ERROR_MESSAGE()

    IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION TransName
END CATCH

IF (@@TRANCOUNT > 0) COMMIT TRANSACTION TransName

This is covered in this blog: http://www.dbdelta.com/the-curious-case-of-undetected-sql-exceptions/ 该博客对此进行了介绍: http : //www.dbdelta.com/the-curious-case-of-undetected-sql-exceptions/

ExecuteScalar will not raise an exception if a T-SQL error occurs after the first row is returned. 如果在返回第一行之后发生T-SQL错误,则ExecuteScalar将不会引发异常。 Also, if no rows are returned because the row-returning statement erred and the error was caught in T-SQL, ExecuteScalar returns a null object without raising an exception. 另外,如果由于行返回语句错误并且在T-SQL中捕获了错误而未返回任何行,则ExecuteScalar返回空对象而不会引发异常。

The same issue can happen with ExecuteNonQuery for the same reason. 出于相同的原因,ExecuteNonQuery可能发生相同的问题。

Do you catch and log other exception types somewhere down the road? 您是否会捕获并记录其他异常类型? What happens if something other than SqlException is thrown? 如果抛出SqlException以外的东西会怎样? Is it logged? 是否已记录?

Regarding retry logic - I would also handle InvalidOperationException. 关于重试逻辑-我还将处理InvalidOperationException。 ExecuteNonQuery will throw InvalidOperationException if Connection is not open. 如果未打开连接,则ExecuteNonQuery将抛出InvalidOperationException。 For example a connection may go into ConnectionState.Broken state due to a brief network outage or something. 例如,由于短暂的网络中断或其他原因,连接可能进入ConnectionState.Broken状态。 Similar to how you retry for deadlocks and timeouts I would catch InvalidOperationException, check connection state and if it is not open - reopen it and retry. 与您重试死锁和超时的方式类似,我将捕获InvalidOperationException,检查连接状态,如果连接状态未打开,请重新打开它并重试。

First off, thank you @MadMyche for your suggestion, adding the output parameters helped me know that the catch was never getting hit for some reason. 首先,感谢@MadMyche的建议,添加输出参数使我知道由于某些原因该捕获从未成功。

What was happening is that when the query was running it would occasionally timeout and get returned back into the retry loop in the C# code, when this would happen the transaction that had been opened in the query wasn't getting closed. 发生的事情是,当查询运行时,它有时会超时并返回C#代码中的重试循环,而这时,在查询中打开的事务并没有关闭。 When the retry loop would finally loop back around and finish the query successfully it would then close the sql connection and when that happens the SQL engine goes through and closes and does a rollback on any open transactions, which was removing the data that had been saved. 当重试循环最终返回并成功完成查询时,它将关闭sql连接,当发生这种情况时,SQL引擎将通过关闭并对所有打开的事务进行回滚,这将删除已保存的数据。

I found this article that explains what happens and offered a solution: 我发现这篇文章解释了发生的情况并提供了解决方案:

http://weblogs.sqlteam.com/dang/archive/2007/10/20/Use-Caution-with-Explicit-Transactions-in-Stored-Procedures.aspx http://weblogs.sqlteam.com/dang/archive/2007/10/20/Use-Caution-with-Explicit-Transactions-in-Stored-Procedures.aspx

Then I found another article that re-enforced the solution: 然后,我发现另一篇文章加强了该解决方案:

http://www.sommarskog.se/error_handling/Part1.html http://www.sommarskog.se/error_handling/Part1.html

What I did to solve it was setting XACT_ABORT at the beginning of the proc: 我要做的是在proc开头设置XACT_ABORT

SET XACT_ABORT ON;

Setting XACT_ABORT "Specifies whether SQL Server automatically rolls back the current transaction when a Transact-SQL statement raises a run-time error" (See documentation ) 设置XACT_ABORT“指定当Transact-SQL语句引发运行时错误时,SQL Server是否自动回滚当前事务”(请参阅文档

With XACT_ABORT set to ON the transaction will rollback before the connection can be closed by the c# client so that there aren't any open transactions to be rolled back. 在XACT_ABORT设置为ON的情况下,事务将回滚,然后c#客户端才能关闭连接,这样就不会回滚任何打开的事务。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM