原文:Handling Concurrency with the Entity Framework 6 in an ASP.NET MVC 5 Application
1.并发冲突:
当一个用户编辑一个实体数据时,另一个用户在第一个用户的改变提交到数据库之前同时也在编辑这个实体数据,这时就会发生冲突。如果不处理这种冲突,最后更新数据库的用户的更改将覆盖其他用户的修改。在许多程序中,这种风险是可以接受的:如果程序具有较少的用户和较少的更新操作,或者不是覆盖关键的变化,这种情况下处理并发的成本可能大于好处。在这种情况下我们不必配置程序处理并发。
1.1.保守式并发(Pessimistic Concurrency)(加锁):
如果我们的程序不需要避免在并发时意外丢失数据,一种方式是使用数据库锁,这种方式叫做保守式并发。例如,我们在从数据库读取一行数据之前,可以请求只读或者更新访问锁。如果我们对一行进行更新访问锁定,其他用户就不能再该该行请求只读或者更新访问锁,因为他们得到的数据副本在程序中被改变了。如果对一行加只读锁,其他用户也可以对其加只读锁,但是不能加更新锁。
管理锁是有缺点的,它会使程序变复杂。它需要大量的数据库管理资源,并且随着用户数量的增加可能会导致性能问题。因此,不是所有的数据库管理系统都支持保守式并发。EF没有对其提供内置支持,本教程也不会展示如何实现它。
1.2.开放式并发(Optimistic Concurrency):
开放式并发意味着允许并发冲突发生,然后在并发冲突发生时做适当的反应。例如,John在Department的Edit页面,把名为English的department的Budget数量从$350,000.00修改为$0.00:
在John点击保存之前,Jane把English的department的Start Date从9/1/2007修改为8/8/2013:
John首先点击Save,然后Jane点击Save。接下来会发生什么取决于我们如何处理并发冲突。下面是一些选择:
1.3.检测并发冲突:
我们可以通过处理EF抛出的OptimisticConcurrencyException异常来解决冲突。为了知道什么时候抛出这些异常,必须启用EF检测冲突。因此,我们必须适当地配置数据库和数据模型。启用冲突检测的方法如下:
如果选择配置EF,如果该行被第一次读取时做了任何改变,Where子句将不会返回一行被更新,EF将认为发生了并发冲突。因为数据表中有许多列,这种做法将导致庞大的Where子句,并且要求我们保持大量的状态。如前所述,保持大量的状态会出现性能问题。因此这种做法通常是不推荐的,因此本教程也不使用此种方法。
如果我们要采用这种方法处理并发,我们必须为所有想要跟踪并发的非主键属性添加ConcurrencyCheck属性。这种改变将启用EF在Update的Where子句中包含所有的列。
本教程将采用添加时间戳来跟踪Department实体的属性,创建一个控制器和视图,并且添加测试以确保一切工作正常。
2.为Department实体添加开放式并发属性:
修改Models\Department.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)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] [Display(Name = "Start 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和删除命令的Where子句中。该属性被叫做Timestamp是因为在使用SQL rowversion之前,SQL Server的早期版本中使用一个SQL timestamp数据类型。.NET中rowversion是字节数组。
如果我们选择使用fluent API,则使用IsConcurrencyToken方法指定跟踪属性:
modelBuilder.Entity<Department>()
.Property(p => p.RowVersion).IsConcurrencyToken();
在Package Manager Console输入命令:
Add-Migration RowVersion
Update-Database
3.修改Department控制器:
在DepartmentController.cs中,
ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "LastName");
使用下面语句替换:
ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName");
修改Edit的POST:
[HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> Edit(int? id, byte[] rowVersion) { string[] fieldsToBind = new string[] { "Name", "Budget", "StartDate", "InstructorID", "RowVersion" }; if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var departmentToUpdate = await db.Departments.FindAsync(id); if (departmentToUpdate == null) { Department deletedDepartment = new Department(); TryUpdateModel(deletedDepartment, fieldsToBind); ModelState.AddModelError(string.Empty, "Unable to save changes. The department was deleted by another user."); ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", deletedDepartment.InstructorID); return View(deletedDepartment); } if (TryUpdateModel(departmentToUpdate, fieldsToBind)) { try { db.Entry(departmentToUpdate).OriginalValues["RowVersion"] = rowVersion; 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, "Unable to save changes. The department was deleted by another user."); } else { var databaseValues = (Department)databaseEntry.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.InstructorID).FullName); ModelState.AddModelError(string.Empty, "The record you attempted to edit " + "was modified by another user after you got the original value. The " + "edit operation was canceled and the current values in the database " + "have been displayed. If you still want to edit this record, click " + "the Save button again. Otherwise click the Back to List hyperlink."); departmentToUpdate.RowVersion = databaseValues.RowVersion; } } catch (RetryLimitExceededException /* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator."); } } ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", departmentToUpdate.InstructorID); return View(departmentToUpdate); }
在Views\Department\Edit.cshtml添加一个隐藏域存储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)
4.测试开放式并发处理:
在English department的Edit右键Open in new tab,然后点击English department的Edit链接:
在第一个浏览器标签修改,并保存:
在浏览器的第二个标签修改并保存:
再次点击保存:
5.更新Delete页面:
修改DepartmentController.cs的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.GetValueOrDefault()) { return RedirectToAction("Index"); } return HttpNotFound(); } if (concurrencyError.GetValueOrDefault()) { ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete " + "was modified by another user after you got the original values. " + "The delete operation was canceled and the current values in the " + "database have been displayed. If you still want to delete this " + "record, click the Delete button again. Otherwise " + "click the Back to List hyperlink."; } return View(department); } [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 /* dex */) { //Log the error (uncomment dex variable name after DataException and add a line here to write a log. ModelState.AddModelError(string.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator."); return View(department); } }
替换之前的代码,POST只接收ID:
public async Task<ActionResult> DeleteConfirmed(int id)
替换之后的代码,POST参数变为模型绑定的Department实体实例。这样除了访问主键外,还访问RowVersion属性:
public async Task<ActionResult> Delete(Department department)
修改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>
运行,在English department的Delete右键Open in new tab,然后点击English department的Edit链接.
在浏览器的第一个标签修改并保存:
在浏览器的第二个标签页,点击Delete:
再次点击Delete,将会删除该department,然后导航到Index页面。
处理各种并发场景的其他方法,请查看:Optimistic Concurrency Patterns和Working with Property Values。