Entity Framework和MVC中的乐观并发模型

6

我在ASP.NET MVC控制器中有以下更新代码:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Person(int id, FormCollection form)
{
  var ctx = new DB_Entities(); // ObjectContext
  var person = ctx.Persons.Where(s => s.Id == id).FirstOrDefault(); 
  TryUpdateModel(person, form.ToValueProvider()); 
  ctx.SaveChanges(); 
  return RedirectToAction("Person", id);
}

然而,此更新代码是最后写入者获胜的。现在我想添加一些并发控制。Person表已经有了SQL时间戳列。我需要将时间戳值作为隐藏值发送到客户端,并在回传过程中手动处理吗?或者Entity Framework中是否有标准模式来完成这个任务?
谢谢。
4个回答

3
首先,您需要定义哪个属性或属性将用于执行并发检查,因为在Entity Framework中,并发是基于每个属性的基础上定义的。ConcurrencyMode用于标记要进行并发检查的属性,并可在实体对象属性窗口中找到(只需右键单击模型中的Person实体)。它的选项是None,这是默认值,和Fixed。
在调用SaveChanges期间,如果自从检索行以来DB中的字段已更改,则EF将取消保存并抛出OptimisticConcurrencyException,如果我们将该字段的ConcurrencyMode设置为Fixed。
在底层,EF将该字段的值包含在正在提交给数据存储区的Update或Delete SQL语句中作为WHERE子句。
如果您想在所有属性上使用乐观并发,请将TimeStamp属性ConcurrencyMode设置为Fixed,如果表中任何字段的值发生更改,您将收到OptimisticConcurrencyException(而不是在每个单独属性上设置它)。
编辑 根据下面的Craig评论,您需要将TimeStamp持久化在视图中并将其读回到Person对象中,如果您将ConcurrencyMode设置为TimeStamp属性上的固定值,则EF会处理其余部分。当然,您可以尝试处理EF可能抛出的OptimisticConcurrencyException,并且如果您感兴趣,有方法可以从此异常中恢复。

2
他确实需要在视图中包含并发令牌,因为他想知道是否有人在GET之后更新了实体。只有在GET期间知道值是什么才能做到这一点。然而,在POST期间重新获取实体会使事情变得复杂,因为它会破坏EF内置的并发控制。EF将使用您在POST期间获取的值,而不是从GET获取的值。 - Craig Stuntz
是的,你说得对,他肯定需要从视图中读取时间戳。我被他在POST中再次读取Person这一事实所困惑。我更新了我的答案。Craig,非常感谢你对此的贡献。 - Morteza Manavi
谢谢。我认为Craig的解决方法(使用GetObjectStateEntry.AcceptChanges来假装原始时间戳)更好。顺便说一下,我认为根本原因是在POST中再次读取了Person实体,因此它可能与页面最初呈现时不同。这是EF中正确的编程模型吗?还有其他避免这种情况的方法吗? - Hengyi

2
这实际上比它应该的要难一些。除了像Morteza说的那样将并发模式更改为fixed之外,您还必须在进行POST更新实体之前注入在GET中读取的并发值。思考这个问题的方式是,在更新实体之前,您正在尝试将实体恢复到GET期间的状态。我在这个答案中有一个代码示例:

ASP.NET MVC Concurrency with RowVersion in Edit Action


2

来自MSDN的保存更改和管理并发

try
{
    // Try to save changes, which may cause a conflict.
    int num = context.SaveChanges();
    Console.WriteLine("No conflicts. " +
        num.ToString() + " updates saved.");
}
catch (OptimisticConcurrencyException)
{
    // Resolve the concurrency conflict by refreshing the 
    // object context before re-saving changes. 
    context.Refresh(RefreshMode.ClientWins, orders);

    // Save changes.
    context.SaveChanges();
    Console.WriteLine("OptimisticConcurrencyException "
    + "handled and changes saved");
}

0

我最终在回发函数中完成了这个操作:

var person = ctx.Persons.Where(s => s.Id == id).FirstOrDefault();
string ts = form.Get("person_ts"); // get the persisted value from form
if (person.TimeStamp != ts)
{
   throw new Exception("Person has been updated by other user");
}
TryUpdateModel(person, form.ToValueProvider());     
// EF will check the timestamp again if the timestamp column's 
// ConcurrencyMode is set to fixed.
ctx.SaveChanges();

所以乐观并发性被检查了两次。只是想知道是否有更好的方法来做到这一点?

谢谢。


我认为你的代码甚至无法编译: 运算符“!=”不能应用于类型为“byte[]”和“string”的操作数。无论如何,即使您成功编译了它,这仍然不是一种强大的编程模型来检查并发性,因为在您的if语句之后且在SaveChanges()之前,有人可能会更改该行,而您将无法捕获到这个问题。 - Morteza Manavi
对于“!=”的简洁表示我很抱歉,不过你应该能理解我的意思(假设有一个辅助方法来进行比较)。这就是为什么我在我的方法中说并发检查被执行了两次:一次在我的“if”语句中,另一次在EF SaveChanges()中使用ConcurrencyMode。如果在if语句之后和SaveChanges之前有人修改了行,EF会捕捉到它。 - Hengyi
Ian,在进行POST请求时重新读取实体是可以的。虽然不是必需的,但这样做没有问题。 - Craig Stuntz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接