在审批工作流程中,我想确保提醒电子邮件只发送一次。
使用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
答案 0 :(得分:2)
嗯,事实上,可以通过调用ObjectContext.SaveChanges()来推断出受影响的确切行数。
如果你看一下 ObjectContext.SaveChanges documentation ,你会看到:
public int 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()。
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;
}
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”