为什么EntityFramework为具有事务和RAISERROR的storedprocedure抛出事务计数错误?

时间:2017-07-11 09:37:33

标签: c# sql-server entity-framework tsql

这是我的输出字符串参数的存储过程。

Create
 PROCEDURE [dbo].[SpTest]
    (@ReturnMessage varchar(50) output)
AS
    BEGIN
        SET NOCOUNT ON;
        BEGIN TRY
            BEGIN TRANSACTION;
            RAISERROR('asdf',16,1)
            COMMIT TRANSACTION;
        END TRY
        BEGIN CATCH
            ROLLBACK TRANSACTION;
            DECLARE @ErrorMessage VARCHAR(50)= ERROR_MESSAGE();

            IF ( @ErrorMessage = 'asdf' )
                BEGIN
                    SET @ReturnMessage = @ErrorMessage;
                    RETURN;
                END;
            ELSE


            THROW;
        END CATCH;
    END;

我正在尝试使用Raiserror手动捕获错误并将错误作为输出参数发送。 当我尝试从管理工作室执行它时,该过程正常,但是当我从我的应用程序执行时,实体框架会抛出这个错误 :

SqlException: Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.

但如果我删除了RAISERROR,它可以正常工作。

我正在使用实体框架版本6的C#MVC应用程序。 这是我的C#代码:

public ReturnMessageModel Test()
        {
            ReturnMessageModel result = new ReturnMessageModel();
            ObjectParameter returnMessage = new ObjectParameter("ReturnMessage", typeof(String));
            using (InsurestEntities db = new InsurestEntities())
            {
           db.SpTest(returnMessage);
            }
                result.ReturnMessage = returnMessage.Value.ToString();
            }
            return result;
        }

更新:我在删除此代码时发现错误,事务计数错误已自动解决。

IF ( @ErrorMessage = 'asdf' )
                BEGIN
                    SET @ReturnMessage = @ErrorMessage;
                    RETURN;
                END;

现在工作正常。我认为与RAISEERROR

一起使用时,THROW必须始终由EntityFramework语句处理

2 个答案:

答案 0 :(得分:1)

在C#代码中有交易,你只是不知道它们而没有明确地创建它们。 EF正在为某些操作创建自己的TransactionScope。

在此存储过程中存在事务时,您的错误处理将不起作用。你已经发现了一个原因。您还将遇到其他问题,例如在没有事务时尝试回滚。 CATCH块必须检查块内的XACT_STATE()值并采取相应措施。

如果您想在有交易的情况下使用正确的错误处理模式,请参阅Exception handling and nested transactions

create procedure [usp_my_procedure_name]
as
begin
    set nocount on;
    declare @trancount int;
    set @trancount = @@trancount;
    begin try
        if @trancount = 0
            begin transaction
        else
            save transaction usp_my_procedure_name;

        -- Do the actual work here

lbexit:
        if @trancount = 0   
            commit;
    end try
    begin catch
        declare @error int, @message varchar(4000), @xstate int;
        select @error = ERROR_NUMBER(), @message = ERROR_MESSAGE(), @xstate = XACT_STATE();
        if @xstate = -1
            rollback;
        if @xstate = 1 and @trancount = 0
            rollback
        if @xstate = 1 and @trancount > 0
            rollback transaction usp_my_procedure_name;

        raiserror ('usp_my_procedure_name: %d: %s', 16, 1, @error, @message) ;
    end catch   
end
go

答案 1 :(得分:1)

  

当我尝试从管理工作室执行它时,该过程正常工作

它有效,因为您不在活动交易中。让我们模仿与C#代码相同的行为:

Create PROCEDURE [dbo].[SpTest]    (@ReturnMessage varchar(50) output)
AS
    BEGIN
        SET NOCOUNT ON;
        BEGIN TRY
            BEGIN TRANSACTION;
            RAISERROR('asdf',16,1)
            COMMIT TRANSACTION;
        END TRY
        BEGIN CATCH
            ROLLBACK TRANSACTION;
            DECLARE @ErrorMessage VARCHAR(50)= ERROR_MESSAGE();

            IF ( @ErrorMessage = 'asdf' )
                BEGIN
                    SET @ReturnMessage = @ErrorMessage;
                    RETURN;
                END;
            ELSE


            THROW;
        END CATCH;
    END;

BEGIN TRANSACTION
EXEC dbo.[spTest] 'a'
COMMIT;

<强> DBFiddle

  

Msg 266 Level 16 State 2 Line 0

     

EXECUTE之后的事务计数表示BEGIN和COMMIT语句的数量不匹配。先前的计数= 1,当前计数= 0。

     

Msg 3902 Level 16 State 1 Line 4

     

COMMIT TRANSACTION请求没有相应的BEGIN TRANSACTION。

使用C#/ EF,您已经打开了交易并且它很重要。现在在SQL Server中没有嵌套事务这样的东西。我强烈建议您阅读A SQL Server DBA myth a day: (26/30) nested transactions are real

  

嵌套事务的回滚会回滚整个事务集 - 因为没有嵌套事务。

您应该做的是正确处理存储过程中的事务。例如,使用SAVE TRANSACTION

  

以下示例显示如果在执行存储过程之前启动活动事务,如何使用事务保存点仅回滚存储过程所做的修改。

CREATE PROCEDURE SaveTranExample  
    @InputCandidateID INT  
AS  
    -- Detect whether the procedure was called  
    -- from an active transaction and save  
    -- that for later use.  
    -- In the procedure, @TranCounter = 0  
    -- means there was no active transaction  
    -- and the procedure started one.  
    -- @TranCounter > 0 means an active  
    -- transaction was started before the   
    -- procedure was called.  
    DECLARE @TranCounter INT;  
    SET @TranCounter = @@TRANCOUNT;  
    IF @TranCounter > 0  
        -- Procedure called when there is  
        -- an active transaction.  
        -- Create a savepoint to be able  
        -- to roll back only the work done  
        -- in the procedure if there is an  
        -- error.  
        SAVE TRANSACTION ProcedureSave;  
    ELSE  
        -- Procedure must start its own  
        -- transaction.  
        BEGIN TRANSACTION;  
    -- Modify database.  
    BEGIN TRY  
        DELETE HumanResources.JobCandidate  
            WHERE JobCandidateID = @InputCandidateID;  
        -- Get here if no errors; must commit  
        -- any transaction started in the  
        -- procedure, but not commit a transaction  
        -- started before the transaction was called.  
        IF @TranCounter = 0  
            -- @TranCounter = 0 means no transaction was  
            -- started before the procedure was called.  
            -- The procedure must commit the transaction  
            -- it started.  
            COMMIT TRANSACTION;  
    END TRY  
    BEGIN CATCH  
        -- An error occurred; must determine  
        -- which type of rollback will roll  
        -- back only the work done in the  
        -- procedure.  
        IF @TranCounter = 0  
            -- Transaction started in procedure.  
            -- Roll back complete transaction.  
            ROLLBACK TRANSACTION;  
        ELSE  
            -- Transaction started before procedure  
            -- called, do not roll back modifications  
            -- made before the procedure was called.  
            IF XACT_STATE() <> -1  
                -- If the transaction is still valid, just  
                -- roll back to the savepoint set at the  
                -- start of the stored procedure.  
                ROLLBACK TRANSACTION ProcedureSave;  
                -- If the transaction is uncommitable, a  
                -- rollback to the savepoint is not allowed  
                -- because the savepoint rollback writes to  
                -- the log. Just return to the caller, which  
                -- should roll back the outer transaction.  

        -- After the appropriate rollback, echo error  
        -- information to the caller.  
        DECLARE @ErrorMessage NVARCHAR(4000);  
        DECLARE @ErrorSeverity INT;  
        DECLARE @ErrorState INT;  

        SELECT @ErrorMessage = ERROR_MESSAGE();  
        SELECT @ErrorSeverity = ERROR_SEVERITY();  
        SELECT @ErrorState = ERROR_STATE();  

        RAISERROR (@ErrorMessage, -- Message text.  
                   @ErrorSeverity, -- Severity.  
                   @ErrorState -- State.  
                   );  
    END CATCH  
GO