对于受SqlCommand.ExecuteNonQuery影响的行的EF等效项

时间:2010-09-12 17:26:33

标签: .net entity-framework-4

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

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

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


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

通过启用乐观并发(使用ConcurrencyMode = Fixed on RowVersion属性)并捕获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

2 个答案:

答案 0 :(得分:2)

嗯,事实上,可以通过调用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();
}
当调用Refresh with ClientWins时,它会执行查询以检索当前 数据库中此实体的值,包括新的时间戳。因此,所有原始字段值都已更新以反映最新的数据库值,因此我们可以再次安全地尝试SaveChanges()。

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

好吧,每次修改一行时, 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;
    }

答案 1 :(得分:0)

由于与Morteza的讨论,我回答了我的问题如下。

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

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

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

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


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

小心!通过父实体引用它时更新子实体会导致父实例的DB更新 - 在这种情况下抛出不需要的OptimisticConcurrencyException。 因此我没有使用:ApprovalRequests.ApprovalRequestStates.State =“Reminded”