在以前的两个教程你对关联数据进行了操作。本教程展示如何处理并发性。您将创建工作与各Department
实体的 web 页和页,编辑和删除Department
实体将处理并发错误。下面的插图显示索引和删除的页面,包括一些如果发生并发冲突,则显示的消息。
并发冲突
当一个用户要编辑它,显示实体数据,然后另一个用户更新相同的实体数据第一个用户的更改写入到数据库之前,将发生并发冲突。如果您不启用此类冲突检测,最后谁更新数据库覆盖其他用户的更改。在许多应用中,这种风险是可以接受: 如果有几个用户或一些更新,或者如果不是真的很重要,如果覆盖了一些变化,并发性编程的费用可能超过其益处。在这种情况下,您不需要配置应用程序以处理并发冲突。
保守式并发 (锁定)
如果您的应用程序确实需要防止数据意外丢失在并发性的场景中,做到这一点的一种方法是使用数据库锁。这就被所谓保守式并发。例如,从数据库中读取的行之前,你请求锁定为只读或更新访问权限。如果您锁定行更新访问权限,允许没有其他用户锁定该行要么为只读或更新访问权限,因为他们会得到正在更改的数据的一个副本。如果您锁定的只读访问权限的行,别人也可以锁定它为只读访问权限而不是更新。
管理锁定也有缺点。它可以是复杂的程序。它需要大量的数据库管理资源,它可能导致性能问题的应用程序的用户数增加了 (也就是说,它并不很好地扩展)。基于这些原因,并不是所有的数据库管理系统支持保守式并发。实体框架提供了,没有内置的支持,本教程不会告诉你如何实现它。
开放式并发
保守式并发的替代方案是开放式并发。开放式并发意味着允许并发冲突发生,然后适当地反应,如果他们这样做。例如,John运行部门编辑页面,改变为英语系的预算金额从 $ 350000.00改为 $0.00。
John单击保存之前,Jane运行相同的页面,并更改开始日期字段从 2007/9/1 改为 2013/8/8。
John第一次单击保存,看到他的变化,当浏览器返回到索引页上,然后Jane再单击保存。下一步会发生什么取决于你如何处理并发冲突。一些选项包括以下内容:
-
你可以跟踪用户已修改的属性和更新仅在数据库中相应的列。在示例场景中,没有的数据将会丢失,因为不同的属性由两个用户更新。在下一次有人浏览English department时,他们就会看到John和Jane的修改 —一开始日期 2013/8/8 和预算零美元。
这种更新方法可以减少冲突,可能会导致数据丢失,但它不能避免 如果有两个人更改到同一个实体的属性 数据丢失。 Entity Framework是否用这种方式取决于您如何实现您更新代码。它往往是不实际的Web应用程序,因为它需要维护大量的状态,以及新值保持一个实体的所有原始属性值的轨道。维护大量的状态会影响应用程序性能,因为它需要消耗服务器资源,或者必须包括在 web 页 (例如,在隐藏字段)。
-
您可以让Jane的更改覆盖John的更改。在下一次有人浏览English department时,他们会看到 2013/8/8 和还原的 $ 350,000.00 值。这就被所谓Client Wins 或 Last in Wins的场景。(客户端的值将优先于在数据存储区中的是什么。)因为在这一节,导言中指出,如果你不做任何编码的并发处理,这会自动发生。
-
你可以阻止Jane的更改对数据库的更新。通常情况下,如果她仍然想要保存,会显示一条错误消息,显示她的数据的当前状态和允许她重新打开页面修改。这就被所谓一个 Store Wins的场景。(数据存储区值将优先于提交的客户端的值)。在本教程中,您将实现 Store Wins方案。此方法确保没有更改将被覆盖没有用户,注意到了发生了什么事。
检测并发冲突
可以通过实体框架将引发的OptimisticConcurrencyException异常处理来解决冲突。为了知道什么时候会引发这些异常,实体框架必须能够检测到冲突。因此,你适当地必须配置数据库和数据模型。启用冲突检测的一些选项包括以下内容:
-
在数据库表中,包含可用于记录确定何时更改行的跟踪列。你可以配置实体框架
Update
或Delete
命令中包含Where
SQL 语句中列的。跟踪列的数据类型通常是rowversion
.
rowversion值是一个已更新的行每次递增的顺序编号。在Update
或Delete
的命令中,Where
子句包括跟踪列 (原始行版本) 的原始值。如果由另一个用户更改了正在更新的行,rowversion
列中的值是不同于原始值,因此Update
或Delete
语句无法找到要更新的Where
子句的行。当实体框架发现没有行被更新过的Update
或Delete
命令 (那就是,当受影响的行数为零) 时,它将这解释为并发冲突。 -
配置实体框架可以在
Update
和Delete
命令的Where
子句中的表中包含每个列的原始值。在第一个选项,如果在首次读取时发现有任何的改变,
Where
子句不返回行更新,而实体框架解释作为一个并发冲突。对于有多个列的数据库表,此方法可能会导致非常大的Where
子句,并可以要求你保持大量的状态。因为它消耗服务器资源,或者必须包括在 web 页本身,如前文所述,保持大量的状态可以影响应用程序性能可能。因此一般不建议使用此方法,并在本教程中不使用此方法。如果你想要实现这种并发性的方法,你有来标记您想要跟踪并发性,通过将ConcurrencyCheck属性添加到他们的实体中的所有非主键属性。这种变化使实体框架可以在
UPDATE
语句的WHERE
SQL 子句中包括的所有列。
在本教程的其余部分会添加到Department
实体跟踪属性的rowversion、 创建一个控制器和视图,和测试,以验证一切工作正常。
将开放式并发属性添加到Department实体
在ModelsDepartment.cs,添加一个名为 RowVersion
的跟踪属性:
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
public DateTime StartDate { get; set; }
[Display(Name = "Administrator")]
public int? InstructorID { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public virtual Instructor Administrator { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
Timestamp属性指定此列的Update
和Delete
命令发送到数据库的Where
子句中。该属性称为Timestamp,因为以前版本的 SQL Server 使用 SQLTimestamp数据类型之前 SQLrowversion替换它。.Net 类型为
rowversion是一个字节数组。如果你喜欢使用 fluent API,你可以使用IsConcurrencyToken方法来指定跟踪的属性,如下面的示例所示:
modelBuilder.Entity<Department>()
.Property(p => p.RowVersion).IsConcurrencyToken();
通过添加一个属性更改的数据库模型,所以你需要做另一次迁移。在程序包管理器控制台 (PMC) 中,输入以下命令:
Add-Migration RowVersion
Update-Database
创建一个部控制器
创建Department
控制器和视图的相同的方式,你的其他控制器,使用以下设置:
在ControllersDepartmentController.cs,添加using
语句:
using System.Data.Entity.Infrastructure;
更改"LastName"为"FullName"任何一个此文件 (四个点) 以便部管理员下拉列表将包含讲师的完整名称,而不是只是最后的名字。
ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName");
HttpPost
Edit
方法的现有代码替换为以下代码:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, InstructorID")]
Department department)
{
try
{
if (ModelState.IsValid)
{
db.Entry(department).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseValues = (Department)entry.GetDatabaseValues().ToObject();
if (databaseValues.Name != clientValues.Name)
ModelState.AddModelError("Name", "Current value: "
+ databaseValues.Name);
if (databaseValues.Budget != clientValues.Budget)
ModelState.AddModelError("Budget", "Current value: "
+ String.Format("{0:c}", databaseValues.Budget));
if (databaseValues.StartDate != clientValues.StartDate)
ModelState.AddModelError("StartDate", "Current value: "
+ String.Format("{0:d}", databaseValues.StartDate));
if (databaseValues.InstructorID != clientValues.InstructorID)
ModelState.AddModelError("InstructorID", "Current value: "
+ db.Instructors.Find(databaseValues