这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译,这里是第十篇:为ASP.NET MVC应用程序处理并发
原文:Handling Concurrency with the Entity Framework 6 in an ASP.NET MVC 5 Application
译文版权所有,谢绝全文转载——但您可以在您的网站上添加到该教程的链接。
在之前的教程中,您已经学习了如何更新数据。在本节教程中将展示当多个用户在同一时间更新同一实体时如何处理冲突。
你将修改web页面来处理Department实体,使得它们能够处理并发错误。下面的截图显示了索引和删除页面,以及一些并发冲突的错误消息。
当一个用户显示实体的数据并对其进行编辑,然后另一个用户在第一个用户的更改写入到数据库之前更新同一实体的数据,将发生并发冲突。如果您不启用这种冲突的检测,最后一次更新数据库的用户将覆盖其他用户对数据库所做的更改。在大部分应用程序中,这种风险是可以接收的:如果仅有几个用户或很少更新,或者数据更新覆盖的问题真的不是很重要,实现并发冲突的开销可能会大于它带来的益处。在这种情况下,您不需要配置应用程序以处理并发冲突。
如果您的应用程序需要防止并发带来的意外数据丢失,要做到这一点的一个方法是使用数据库锁。即所谓的悲观并发。例如,您从数据库读取行之前,先请求一个只读或更新的访问锁。如果你锁定了某行的更新访问,没有其他用户可以给该行加锁,无论是只读或是更新。因为他们得到的数据只是变更过程中的一个副本。如果你锁定了某行的只读访问,其他人也可以将其锁定为只读访问,但不能进行更新。
管理锁也有缺点。它会使编程更复杂。并且它需要数据库的管理资源——大量的,以及它可能导致性能的问题比如应用程序的用户数量增加。出于这些原因,并不是所有的数据库管理系统都支持悲观并发。实体框架内置了悲观并发的支持,单本教程中不会讨论如何实现它。
悲观并发的替代方案就是乐观并发。乐观并发意味着运行并发冲突发生,然后对发生的变化做出适当的反应。例如,路人甲在系编辑页面,更改自然科学的预算从50更改为50000。
在路人甲保存该更改之前,路人乙也同样打开了该页面,并更改起始日期字段到2012-12-12。
路人甲首先点击保存,他在索引页面上看到了他所做的修改,之后路人乙也点击了保存。下一步会发生什么取决于你如何处理并发冲突,下面列出了一些选择:
您可以通过实体框架引发的OptimisticConcurrencyException异常处理来解决冲突。为了知道何时何地会引发这些异常,实体框架必须能够检测到冲突。因此,你必须对数据库和数据模型进行适当的配置,包括以下内容:
。行版本的值是一个每次在更新时都会递增的顺序编号。在更新或删除命令中,Where字句将包含跟踪列的原始值。如果有另一个用户更改了正在更新的行,行版本中的值会和原来的不一致。因此更新和删除语句无法找到要更新的行。当在更新或删除时没有行被更新时,实体框架将认定该命令为并发冲突。在本教程的剩余部分,你会添加行版本用来跟踪Department实体的属性。
在Models\Department.cs中,添加一个名为RowCersion的跟踪属性:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace ContosoUniversity.Models { 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)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] [Display(Name = "起始日期")] public DateTime StartDate { get; set; } [Display(Name = "系主任")] 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特性指定该列将会包含在发送到数据库的更新或删除命令的Where子句中。该属性被称为时间戳,因为之前版本的SQL Server使用SQL Timestamp数据类型。行版本的.Net类型是一个字节数组。
如果您更喜欢使用fluent API,您可以使用IsConcurrencyToken方法来指定跟踪属性,如下面的示例:
modelBuilder.Entity<Department>().Property(p => p.RowVersion).IsConcurrencyToken();
现在您已经更改了数据库模型,所以您需要再做一次迁移。在软件包管理器控制台中,输入以下命令:
Add-Migration RowVersion
Update-Database
在DepartmentController.cs中,添加using语句:
using System.Data.Entity.Infrastructure;
将文件中所有的"LastName"更改为"FullName"以便下拉列表使用教师的全名,而不是姓。
ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName");
使用下面的代码替换HttpPost的Edit方法:
[HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> Edit([Bind(Include = "DepartmentID,Name,Budget,StartDate,InstructorID")] Department department) { try { if (ModelState.IsValid) { db.Entry(department).State = EntityState.Modified; await db.SaveChangesAsync(); return RedirectToAction("Index"); } } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var clientValues = (Department)entry.Entity; var databaseEntry = entry.GetDatabaseValues(); if (databaseEntry == null) { ModelState.AddModelError(string.Empty, "无法保存更改,系已经被其他用户删除。"); } else { var databaseValues = (Department)databaseEntry.ToObject(); if (databaseValues.Name != clientValues.Name) ModelState.AddModelError("Name", "当前值: " + databaseValues.Name); if (databaseValues.Budget != clientValues.Budget) ModelState.AddModelError("Budget", "当前值: " + String.Format("{0:c}", databaseValues.Budget)); if (databaseValues.StartDate != clientValues.StartDate) ModelState.AddModelError("StartDate", "当前值: " + String.Format("{0:d}", databaseValues.StartDate)); if (databaseValues.InstructorID != clientValues.InstructorID) ModelState.AddModelError("InstructorID", "当前值: " + db.Instructors.Find(databaseValues.InstructorID).FullName); ModelState.AddModelError(string.Empty, "当前记录已经被其他人更改。如果你仍然想要保存这些数据," + "重新点击保存按钮或者点击返回列表撤销本次操作。"); department.RowVersion = databaseValues.RowVersion; } } catch (RetryLimitExceededException) { ModelState.AddModelError(string.Empty, "无法保存更改,请重试或联系管理员。"); } ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", department.InstructorID); return View(department); }
视图在隐藏字段中存储原始的RowVersion值。 当模型绑定器创建系的实例,对象将有原始的RowVersion属性值及其他属性的新值,比如在编辑页面上输入的用户。然后实体框架创建一个更新命令,命令将在Where子句中包括RowVersion值来进行查询。
如果没有任何行被更新(没有找到匹配原始RowVersion值的行),实体框架将引发DbUpdateConcurrencyException异常,并从catch代码块中异常对象中获取受影响的Department实体。
var entry = ex.Entries.Single();
该对象的Entity属性拥有用户输入的新值,您也可以调用GetDatabaseValues方法来从数据库中读取原始值。
var clientValues = (Department)entry.Entity; var databaseEntry = entry.GetDatabaseValues();
如果有人将行从数据库中删除,GetDataBaseValue方法将返回null,否则,您必须返回的对象强制转换为Department类以访问Department中的属性。
if (databaseEntry == null) { ModelState.AddModelError(string.Empty, "无法保存更改,系已经被其他用户删除。"); } else { var databaseValues = (Department)databaseEntry.ToObject();
下一步,代码将添加每一列数据库和用户输入不同值的自定义错误消息:
if (databaseValues.Name != clientValues.Name) ModelState.AddModelError("Name", "当前值: " + databaseValues.Name);
一个较长的错误消息向用户解释发生了什么事情:
ModelState.AddModelError(string.Empty, "当前记录已经被其他人更改。如果你仍然想要保存这些数据," + "重新点击保存按钮或者点击返回列表撤销本次操作。");
最后,代码将Department对象的RowVersion值设置为从数据库检索到的新值。新的值在重新显示编辑页面时被存储在隐藏字段。下一次用户单击保存时,重新显示的编辑页面会继续捕获并发错误。
在Views\Department\Edit.cshtml中,在DepartmentID隐藏字段后添加一个RowVersion隐藏字段。
@model ContosoUniversity.Models.Department @{ ViewBag.Title = "Edit"; } <h2>Edit</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div class="form-horizontal"> <h4>Department</h4> <hr /> @Html.ValidationSummary(true) @Html.HiddenFor(model => model.DepartmentID) @Html.HiddenFor(model => model.RowVersion)
运行应用程序,单击系选项卡并复制一个选项卡,重复打开两个系页面。
同时在两个窗口中打开同一系的编辑页面,编辑其中的一个页面并保存。
你会看到值已经被保存到数据库中。
修改第二个窗口中的字段并保存。
你会看到并发错误的消息:
再次单击保存,你在第二个浏览器中数据库值会覆盖掉第一个窗口中的保存到数据库中。
对于删除页面,实体框架使用类似的方式来检测并发冲突。当HttpGet的Delete方法显示确认视图时,视图的隐藏字段中包括了原始RowVersion值。当用户确认删除时,该值在HttpPost的Delete方法中就够被传递并调用。当实体框架创建SQL Delete命令时,Where子句中将包括原始的RowVersion值。如果命令执行后没有行受到影响,就会引发并发异常。HttpGet的Delete方法被调用,标志位将被设置为true以重新显示确认页并显示错误。但同时要考虑如果有另一个用户正好也删除了该行,同样会导致一个0行受影响的结果。在这种情况下,我们将显示一个不同的错误消息。
在DepartmentController.cs中,使用下面的代码替换HttpGet的Delete方法:
public async Task<ActionResult> Delete(int? id,bool? concurrencyError) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Department department = await db.Departments.FindAsync(id); if (department == null) { if (concurrencyError == true) { return RedirectToAction("Index"); } return HttpNotFound(); } if (concurrencyError.GetValueOrDefault()) { if (department == null) { ViewBag.ConcurrencyErrorMessage = "你想要删除的记录" + "已经被另一个用户删除了,点击列表超链接返回。"; } else { ViewBag.ConcurrencyErrorMessage = "你想要删除的记录" + "被另一个用户修改了原始值,如果您仍然想要删除该条记录" + "再次点击删除按钮,或者点击列表超链接返回。"; } } return View(department); }
该方法接受一个可选参数,指示发生并发冲突错误时页面是否将被重新显示。如果此标志为true,将使用ViewBag发送一条错误到视图上。
使用下面的代码替换HttpPost Delete方法(名为DeleteConfirmed):
[HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> Delete(Department department) { try { db.Entry(department).State = EntityState.Deleted; await db.SaveChangesAsync(); return RedirectToAction("Index"); } catch (DbUpdateConcurrencyException) { return RedirectToAction("Delete", new { concurrencyError = true, id = department.DepartmentID }); } catch (DataException) { ModelState.AddModelError(string.Empty, "无法删除,请重试或联系管理员。"); return View(department); } }
在您尚未修改的脚手架代码中,该方法接收一个记录ID
public async Task<ActionResult> DeleteConfirmed(int id)
您更改了此参数,使用模型绑定器来创建一个Department实体,这使您可以访问到RowVersion属性值。
public async Task<ActionResult> Delete(Department department)
同时您修改了方法名称从DeleteConfirmed到Delete。脚手架代码为HttpPost的Delete方法使用了Delete的名称,因为这样能够给HttpPost方法一个唯一的签名。(CLR需要方法有不同的参数来重载。现在签名是唯一的,你可以保持MVC的约定,在HttpPost和HttpGet方法上使用相同的方法名。)
如果捕捉到并发错误,该代码重新显示删除确认页,并提供了一个标志来指示显示并发错误消息。
在Views\Department\Delete.cshtml,为视图添加错误消息字段和隐藏字段。将脚手架的代码替换为下面的:
@model ContosoUniversity.Models.Department @{ ViewBag.Title = "Delete"; } <h2>Delete</h2> <p class="error">@ViewBag.ConcurrencyErrorMessage</p> <h3>Are you sure you want to delete this?</h3> <div> <h4>Department</h4> <hr /> <dl class="dl-horizontal"> <dt> Administrator </dt> <dd> @Html.DisplayFor(model => model.Administrator.FullName) </dd> <dt> @Html.DisplayNameFor(model => model.Name) </dt> <dd> @Html.DisplayFor(model => model.Name) </dd> <dt> @Html.DisplayNameFor(model => model.Budget) </dt> <dd> @Html.DisplayFor(model => model.Budget) </dd> <dt> @Html.DisplayNameFor(model => model.StartDate) </dt> <dd> @Html.DisplayFor(model => model.StartDate) </dd> </dl> @using (Html.BeginForm()) { @Html.AntiForgeryToken() @Html.HiddenFor(model => model.DepartmentID) @Html.HiddenFor(model => model.RowVersion) <div class="form-actions no-color"> <input type="submit" value="Delete" class="btn btn-default" /> | @Html.ActionLink("Back to List", "Index") </div> } </div>
代码在h2和h3之间添加了一条错误消息:
<p class="error">@ViewBag.ConcurrencyErrorMessage</p>
使用FullName替换了LastName:
<dt> Administrator </dt> <dd> @Html.DisplayFor(model => model.Administrator.FullName) </dd>
最后,它在Html.BeginForm语句之后添加了隐藏字段:
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)
运行应用程序,打开系索引页面,右键点击自然科学的删除超链接,选择在新窗口中打开。然后在第一个窗口上点击编辑,修改预算并保存。
更改已经保存到数据库。
点击第二个窗口中的删除按钮,会看到一个并发错误信息。
如果此时再次点击删除,实体将被删除,你会被重定向到索引页面。
在本节中我们介绍了如何处理并发冲突。关于更多处理并发冲突的信息,请参阅MSDN上的和。下一节中我们将介绍如何实现Instructor和Student实体的表-每个层次继承。
Tom Dykstra - Tom Dykstra是微软Web平台及工具团队的高级程序员,作家。
[渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序处理并发,布布扣,bubuko.com
[渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序处理并发
原文:http://www.cnblogs.com/Bce-/p/3725868.html