并发冲突出如今这样的时候,一个用户正在显示并编辑一个实体,可是在这个用户将修改保存到数据库以前,另外的一个用户却更新了一样的实体。若是你没有经过 EF 检测相似的冲突,最后一个更新数据的用户将会覆盖其余用户的修改。在一些程序中,这样的风险是能够接受的,若是只有不多的用户,或者不多的更新,甚至对数据的覆盖不是真的很关键,或者解决并发的代价超过了支持并发所带来的优点。在这种状况下,你就不须要让你的程序支持并发冲突的处理。javascript
若是你的应用须要在并发环境下防止偶然的数据丢失,一种方式是经过数据库的锁来实现。这种方式被称为悲观并发。例如,在从数据库中读取一行数据以前,能够申请一个只读锁,或者一个更新访问锁。若是你对数据行使用了更新访问锁,就没有其余的用户能够获取不论是只读锁仍是更新访问锁,由于他们可能获取正在被修改中的数据。若是你使用只读锁来锁定一行,其余用户也可使用只读访问,可是不能进行更新。html
管理锁有一些缺点,对程序来讲可能很复杂。它须要重要的数据库管理资源,对于大量用户的时候可能致使性能问题 ( 扩展性很差 ),因为这些缘由,不是全部的数据库管理系统都支持悲观锁。EF 对悲观锁没有提供内建的支持,这个教程也不会演示如何实现它。java
除了悲观并发以外的另外一方案是乐观并发。乐观并发意味着容许并发冲突发生,若是出现了就作出适当的反应。例如,John 执行 Department 的编辑页面,将 English 系的 Budget 从 $350,000.00 修改成 $100,000.00 ( John 管理与 English 有竞争的系,但愿将一些资金转移到他本身的系使用 )。数据库
在 John 点击保存 Save 以前,Jane 运行一样的页面,将开始时间 Start Date 字段从 9/1/2007 修改成 1/1/1999 ( Jane 管理历史系,但愿它的历史更加悠久 )浏览器
John 先点击保存 Save,而后在回到 Index 页面的时候看到本身的修改。而后 Jane 点击保存 Save。下一步发生什么取决于如何处理并发冲突。可能的状况以下:服务器
这种方法能够减小可能形成数据丢失的冲突次数,可是若是用户修改同一个实体的相同属性的话,会丢失数据, EF 具体依赖于你如何实现你的更新代码。这种方式不适合 Web 应用程序,由于须要你维护大量的状态,以便追踪全部新值的原始状态。维护大量的状态会影响到程序的性能,由于既须要服务器的资源,又须要将状态保存在页面中 ( 例如,使用隐藏域 )。并发
你能够经过处理 EF 抛出的 OptimisticConcurrencyException 异常来处理冲突。为了知道何时 EF 抛出了这种异常,EF 必须可以检测冲突。所以,你必须合理配置数据库和数据模型。启用冲突检测的一些选项以下:mvc
在本教程剩下的部分,你须要在 Department 实体上增长一个追踪列,建立控制器和视图,而后检查一切是否工做正常。app
注意:若是你没有使用追踪列来实现并发,你就必须经过使用 ConcurrencyCheck 特性标记全部的非主属性用在并发跟踪中。这将会使 EF 将全部的列包含在 Update 语句的 Where 子句中。asp.net
在 Models\Departments.cs 文件中,增长跟踪属性。
[Timestamp] public Byte[] Timestamp { get; set; }
Timestamp 特性指定随后的列将会被包含在 Update 或者 Delete 语句的 Where 子句中。
如同建立其余的控制器同样,建立 Department 控制器和视图,使用以下的设置。
在 Controllers\DepartmentController.cs 中,增长一个 using 语句。
using System.Data.Entity.Infrastructure;
将文件中全部的 “LastName” 修改成 “FullName” ( 共有 4 处 ),使得系控制器中的下拉列表使用教师的全名而不是名字。
将 HttpPost Edit 方法使用下面的代码替换掉。
[HttpPost] public ActionResult Edit(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 databaseValues = (Department)entry.GetDatabaseValues().ToObject(); var clientValues = (Department)entry.Entity; 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."); department.Timestamp = databaseValues.Timestamp; } catch (DataException) { //Log the error (add a variable name after Exception) ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator."); } ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName", department.InstructorID); return View(department); }
视图经过页面中的隐藏域保存原始的时间戳。当编辑页面提交到服务器的时候,经过模型绑定建立 Department 实例的时候,实例将会拥有原始的 Timestamp 属性值,其余的属性获取新值。而后,当 EF 建立 Update 命令时,命令中将包含查询包含原始 Timestamp 值的 Where 子句。
在执行 Update 语句以后,若是没有行被更新,EF 将会抛出 DbUpdateConcurrencyException 异常,代码中的 catch 块从异常对象中获取受影响的 Department 实体对象,实体中既有从数据库中读取的值,也有用户新输入的值。
var entry = ex.Entries.Single(); var databaseValues = (Department)entry.GetDatabaseValues().ToObject(); var clientValues = (Department)entry.Entity;
而后,代码为用户在编辑页面上每个输入的值与数据库中的值不一样的列添加自定义的错误信息。
if (databaseValues.Name != currentValues.Name) ModelState.AddModelError("Name", "Current value: " + databaseValues.Name); // ...
长的错误信息解释了发生的情况以及如何解决的方式。
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.");
最后,代码将 Department 的 Timestamp 属性值设置为数据库中新获取的值。新的 Timestamp 值被保存在从新显示页面的隐藏域中,下一次用户点击保存的时候,当前显示的编辑页面值会被从新获取,这样就能够处理新的并发错误。
在 Views\Department\Edit.cshtml 中,增长一个隐藏域来保存 Timestamp 属性值,紧跟在 DepartmentID 属性以后。
@Html.HiddenFor(model => model.Timestamp)
在 Views\Department\Index.cshtml 中,使用下面的代码替换原有的代码,将连接移到左边,更新页面标题和列标题,在 Administrator 列中,使用 FullName 代替 LastName
@model IEnumerable<ContosoUniversity.Models.Department> @{ ViewBag.Title = "Departments"; } <h2>Departments</h2> <p> @Html.ActionLink("Create New", "Create") </p> <table> <tr> <th></th> <th>Name</th> <th>Budget</th> <th>Start Date</th> <th>Administrator</th> </tr> @foreach (var item in Model) { <tr> <td> @Html.ActionLink("Edit", "Edit", new { id=item.DepartmentID }) | @Html.ActionLink("Details", "Details", new { id=item.DepartmentID }) | @Html.ActionLink("Delete", "Delete", new { id=item.DepartmentID }) </td> <td> @Html.DisplayFor(modelItem => item.Name) </td> <td> @Html.DisplayFor(modelItem => item.Budget) </td> <td> @Html.DisplayFor(modelItem => item.StartDate) </td> <td> @Html.DisplayFor(modelItem => item.Administrator.FullName) </td> </tr> } </table>
运行程序,点击 Departments.
点击 Edit 超级连接,而后再打开一个新的浏览器窗口,窗口中使用相同的地址显示相同的信息。
在第一个浏览器的窗口中修改一个字段的内容,而后点击 Save。
浏览器回到 Index 页面显示修改以后的值。
在第二个浏览器窗口中将一样的字段修改成不一样的值,
在第二个浏览器窗口中,点击 Save,将会看到以下错误信息。
再次点击 Save。在第二个浏览器窗口中输入的值被保存到数据库中,在 Index 页面显示的时候出如今页面上。
对于删除页面,EF 使用相似的方式检测并发冲突。当 HttpGet Delete 方法显示确认页面的时候,视图在隐藏域中包含原始的 Timestamp 值,当用户确认删除的时候,这个值被传递给 HttpPost Delete 方法,当 EF 建立 Delete 命令的时候,在 Where 子句中包含使用原始 Timestamp 值的条件,若是命令影响了 0 行 ( 意味着在显示删除确认页面以后被修改了 ),并发异常被抛出,经过传递错误标志为 true ,HttpGet Delete 方法被调用,带有错误提示信息的删除确认页面被显示出来。
在 DepartmentController.cs 中,使用以下代码替换 HttpGet Delete 方法。
public ActionResult Delete(int id, bool? concurrencyError) { 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."; } Department department = db.Departments.Find(id); return View(department); }
方法接收一个可选的表示是不是在并发冲突以后从新显示页面的参数,若是这个标志为 true,错误信息经过 ViewBag 传递到视图中。
使用下面的代码替换 HttpPost Delete 方法中的代码 ( 方法名为 DeleteConfirmed )
[HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(Department department) { try { db.Entry(department).State = EntityState.Deleted; db.SaveChanges(); return RedirectToAction("Index"); } catch (DbUpdateConcurrencyException) { return RedirectToAction("Delete", new System.Web.Routing.RouteValueDictionary { { "concurrencyError", true } }); } catch (DataException) { //Log the error (add a variable name after Exception) ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator."); return View(department); } }
你刚刚替换的脚手架代码方法仅仅接收一个记录的 Id
public ActionResult DeleteConfirmed(int id)
将这个参数替换为经过模型绑定建立的 Department 实体实例。这使得能够访问额外的 Timestamp 属性。
public ActionResult DeleteConfirmed(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> <fieldset> <legend>Department</legend> <div class="display-label"> @Html.LabelFor(model => model.Name) </div> <div class="display-field"> @Html.DisplayFor(model => model.Name) </div> <div class="display-label"> @Html.LabelFor(model => model.Budget) </div> <div class="display-field"> @Html.DisplayFor(model => model.Budget) </div> <div class="display-label"> @Html.LabelFor(model => model.StartDate) </div> <div class="display-field"> @Html.DisplayFor(model => model.StartDate) </div> <div class="display-label"> @Html.LabelFor(model => model.InstructorID) </div> <div class="display-field"> @Html.DisplayFor(model => model.Administrator.FullName) </div> </fieldset> @using (Html.BeginForm()) { @Html.HiddenFor(model => model.DepartmentID) @Html.HiddenFor(model => model.Timestamp) <p> <input type="submit" value="Delete" /> | @Html.ActionLink("Back to List", "Index") </p> }
代码中在 h2 和 h3 之间增长了错误信息。
<p class="error">@ViewBag.ConcurrencyErrorMessage</p>
在 Administrator 区域将 LastName 替换为 FullName。
<div class="display-label"> @Html.LabelFor(model => model.InstructorID) </div> <div class="display-field"> @Html.DisplayFor(model => model.Administrator.FullName) </div>
最后,增长了用于 DepartmentId 和 Timestamp 属性的隐藏域,在 Html.BeginForm 语句以后。
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.Timestamp)
运行 Departments 的 Index 页面,使用一样的 URL 打开第二个浏览器窗口。
在第一个窗口中,在某个系上点击 Edit ,而后修改一个值,先不要点击 Save。
在第二个窗口中,在一样的系上,选择 Delete ,删除确认窗口出现了。
在第一个窗口中,点击 Save,在 Index 页面中确认修改信息。
如今,在第二个浏览器窗口中点击 Delete,你将会看到并发错误信息,其中 Department的名称已经使用当前数据库中的值刷新了。
若是再次点击 Delete,你将会被重定向到 Index 页面,在显示中 Department 已经被删除了。
这里完整地介绍了处理并发冲突。对于处理并发冲突的其余场景,能够在 EF 团队的博客上查阅Optimistic Concurrency Patterns和Working with Property Values。下一次教程将会演示针对教师 Instructor 和学生 Student 实体的表层次的继承。