【问题标题】:EF eqivalent for rows affected of SqlCommand.ExecuteNonQueryEF 等效于受 SqlCommand.ExecuteNonQuery 影响的行
【发布时间】:2010-09-12 17:26:33
【问题描述】:

在审批工作流程中,我想确保提醒电子邮件只发送一次。

使用 SqlCommand.ExecuteNonQuery 我可以通过测试返回值来确保这一点。 使用 EF 的推荐解决方案是什么? 根据文档 ObjectContext.SaveChanges 不返回等效值。

SqlCommand 示例: (TransactionScope 用于在 SendMail 失败时回滚数据库更新。)

Dim sql = "UPDATE LeaveApprovalRequests SET State = 'Reminded'" & " WHERE ID=3 AND State <>'Reminded'" Using scope As New TransactionScope Using cnx As New SqlConnection(My.Settings.connectionString) cnx.Open() Dim cmd As New SqlCommand(sql, cnx) If 1 = cmd.ExecuteNonQuery Then SendMail() End If scope.Complete() End Using End Using

通过启用乐观并发(在 RowVersion 属性上使用 ConcurrencyMode=Fixed)并捕获 OptimisticConcurrencyException,我能够确定对象是否在存储中实际更新。 现在 TransactionScope(用于在 SendMail 失败时回滚数据库更新)引发死锁错误。 为什么?

Using scope As New TransactionScope Using ctx As New ApprovalEntities Try Dim approval = ctx.LeaveApprovalRequests. Where(Function(r) r.ID = 3 And r.State = "Created" ).FirstOrDefault If approval Is Nothing Then Console.WriteLine("not found") Exit Sub End If Threading.Thread.Sleep(4000) approval.State = "Reminded" ctx.SaveChanges() SendMail() Catch ex As OptimisticConcurrencyException Exit Try End Try End Using scope.Complete() End Using

【问题讨论】:

    标签: .net entity-framework-4


    【解决方案1】:

    好吧,事实上,受影响的行数可以通过调用 ObjectContext.SaveChanges() 来推断。

    如果您查看 ObjectContext.SaveChanges documentation,您会看到:

    public int SaveChanges()

    1. 返回值:调用 SaveChanges 时处于已添加、已修改或已删除状态的对象数。
    2. “SaveChanges 在事务中运行。如果任何脏 ObjectStateEntry 对象无法持久化,SaveChanges 将回滚该事务并引发异常。”

    (1) 和 (2) 基本上意味着 如果您对 SaveChanges() 的调用已成功完成并且您没有收到任何异常,则 EF 保证返回值准确地反映了对象的数量已修改。

    因此,您需要做的就是:

    try {
        // Try to save changes, which may cause a conflict.
        int num = context.SaveChanges();
        SendMail();
    }
    catch (OptimisticConcurrencyException) {
        //Refresh the entity, using ClientWins
        context.Refresh(RefreshMode.ClientWins, yourEntityObject);
        //SaveChanges again;
        context.SaveChanges();
    }
    
    当调用用 ClientWins 刷新时,它会执行一个查询来检索当前 数据库中该实体的值,包括新的时间戳。因此,所有原始字段值都已更新以反映最新的数据库值,因此我们可以安全地再次尝试 SaveChanges()。

    更新了您的问题:
    任务是:仅当我能够将状态从已创建更改为已提醒时才发送电子邮件。因此,在处理 OptimisticConcurrencyException 时强制通过 SaveChanges 是没有意义的。如果更改状态导致异常,则错误处理程序应退出,否则重试整个任务(读取和保存)。如果通过 RowVersion 列而不是仅通过状态启用乐观并发,我该怎么做?

    好吧,每次修改一行时,rowversion 字段都会自动更新,因此在您的情况下,最好关闭 rowversion 上的并发模式并将其打开, 对于 State 属性,这样您的代码将非常简单:
    try {
        context.SaveChanges();
        SendMail();
    }
    catch (OptimisticConcurrencyException) {
        // RowVersion Concurrency Mode = Fixed 
        // State Concurrency Mode = None
        // If it reaches here it means that the State has been changed;
        // so you do nothing except than throwing the exception
        throw;
    }
    

    但是,如果您只想为 rowversion 属性设置 Concurrency Mode = Fixed,那么这意味着您可能会获得 OptimisticConcurrencyException 来更改任何包括 State 的字段,所以完成这项工作需要做更多的工作:
    try {
        ctx.SaveChanges();
        SendMail;
    }
    catch (OptimisticConcurrencyException) {
        // RowVersion Concurrency Mode = Fixed 
        // State Concurrency Mode = None
        // If it reches here it means that ANY/All field(s) has changed
        // So we need to see if it was State:
        ctx.Refresh(RefreshMode.ClientWins, approval);
        ObjectStateEntry ose = ctx.ObjectStateManager.GetObjectStateEntry(approval);
        string stateValue = ose.OriginalValues["State"].ToString();
        // If the value is still "Created" then something else should have changed,
        // And caused the exception, so we can proceed with the Save:
        if (stateValue == "Created") {
            ctx.SaveChanges();
            SendMail;
        }
        else {
            // Nope, it was state, so we throw the exception to the caller:
            throw;
        }
    

    【讨论】:

    • 对我来说,文档说 SaveChanges 返回它打算更新的对象数量,而不是它在商店中更新的数量。我通过在 SaveChanges() 处设置断点、更改 SQL Server 中的值然后恢复来验证这一点。
    • 没错。如果您可以调用 SaveChanges() 没有任何异常,那么 EF 保证打算更新的对象数量实际上是在存储中更新的,否则您将得到一个异常。跨度>
    • 对不起,我没有正确阅读您的第一条评论(即使您突出显示了重要部分)。我最初的测试用例没有使用乐观并发,但在我的实际场景中,我会在大多数情况下启用它。这就留下了为什么 TransactionScope throws 会引发死锁错误的问题。
    • 没问题。我将更新我的代码以说明如何可能从 ConcurrencyException 中恢复,这可能会帮助您克服异常,但您仍然需要了解究竟是什么导致了死锁,以及解决此问题的最佳方法即启动 SQL Server Profiler 并从那里跟踪它。
    • 任务是:仅当我能够将状态从已创建更改为已提醒时才发送电子邮件。因此,在处理 OptimisticConcurrencyException 时强制通过 SaveChanges 是没有意义的。如果更改状态导致异常,则错误处理程序应退出,否则重试整个任务(读取和保存)。如果通过 RowVersion 列而不是仅通过状态启用乐观并发,我该怎么做?
    【解决方案2】:

    根据与 Morteza 的讨论,我回答如下问题。

    SaveChanges 返回它打算更新的对象数量,而不是它在存储中更新的数量。因此它必须与 OptimisticConcurrencyException 一起使用来确定更改是否成功。必须考虑到除了打算更改的属性之外的其他属性可能会导致 OptimisticConcurrencyException。

    在同一个 TransactionScope 中读取实体并更新它会导致死锁。

    对于我的任务“仅当我能够将状态从已创建更改为已提醒时才发送电子邮件”,我使用以下解决方案:

    使用 1:1 关联将 ApprovalRequest 实体一分为二,在 OptimisticConcurrencyException 上退出,在 TransactionScope 中使用 SaveChanges 发送邮件。

    ApprovalRequests ID (PK) RequestedBy ... RowVersion (ConcurrencyMode=Fixed) ApprovalRequestStates ApprovalRequest_ID (PK, FK) State (ConcurrencyMode=Fixed) Using ctx As New ApprovalEntities Dim approval = cxt.ApprovalRequests.Where ... Dim state = ctx.ApprovalRequestStates. Where(Function(r) r.ApprovalRequest_ID = approval.ID And r.State = "Created" ).FirstOrDefault() If state Is Nothing Then Exit Sub state.State = "Reminded" Threading.Thread.Sleep(3000) Using scope As New TransactionScope Try ctx.SaveChanges() SendMail() scope.Complete() Catch ex As OptimisticConcurrencyException Exit Try End Try End Using End Using

    小心!通过父实体引用子实体时更新子实体也会导致父实体的数据库更新 - 在这种情况下会引发不需要的 OptimisticConcurrencyException。 因此我没有使用: ApprovalRequests.ApprovalRequestStates.State = "Reended"

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-11-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多