演练5-6:Contoso大学校园管理系统6
在上一次的教程中,我们处理了关联数据问题。这个教程演示如何处理并发问题。你将使用Department实体创建一个页面,这个页面在支持编辑和删除的同时,还可以处理并发错误。下面的截图演示了Index页面和Delete页面,包括在出现并发冲突的时候提示的一些信息。
一、并发冲突
并发冲突出现在这样的时候,一个用户正在显示并编辑一个实体,但是在这个用户将修改保存到数据库之前,另外的一个用户却更新了同样的实体。如果你没有通过EF检测类似的冲突,最后一个更新数据的用户将会覆盖其他用户的修改。例如,一个大客户有不同的部门下订单。部门1打电话给员工1下订单。几个小时后,部门1又打电话给员工1修改订单。同时,部门2打电话给员工2修改同一个订单,添加一些产品。两个员工同时检索该订单,员工1修改了一些details,添加了几个并且保存订单。员工2移除一个detail,1分钟后保存订单。这两个员工不会意识到存在问题,最终将错误的货物交付给客户。
在一些程序中,这样的风险是可以接受的,如果只有很少的用户,或者很少的更新,甚至对数据的覆盖不是真的很关键,或者解决并发的代价超过了支持并发所带来的收益。在这种情况下,你就不需要让你的程序支持并发冲突的处理。
1.悲观并发(锁定)
如果你的应用需要在并发环境下防止偶然的数据丢失,一种方式是通过数据库的锁来实现。这种方式被称为悲观并发。例如,在从数据库中读取一行数据之前,可以申请一个只读锁,或者一个更新访问锁。如果你对数据行使用了更新访问锁,就没有其他的用户可以获取不管是只读锁还是更新访问锁,因为他们可能获取正在被修改中的数据。如果你使用只读锁来锁定一行,其他用户也可以使用只读锁,但是不能使用更新锁。
管理锁有一些缺点,对程序来说可能很复杂。它需要重要的数据库管理资源,对于大量用户的时候可能导致性能问题(扩展性不好),由于这些原因,不是所有的数据库管理系统都支持悲观锁。EF对悲观锁没有提供内建的支持,这个教程也不会演示如何实现它。
2.乐观并发
除了悲观并发之外的另一方案是乐观并发。乐观并发意味着允许并发冲突发生,如果出现了就做出适当的反应。例如,John执行Department的编辑页面,将English系的Budget从$350,000.00修改为$0.00。
在John点击保存Save之前,Jane运行同样的页面,将开始时间Start Date字段从9/1/2007修改为8/8/2013。
John先点击保存Save,然后再回到Index页面的时候看到自己的修改。然后Jane点击保存Save。下一步发生什么取决于如何处理并发冲突。可能的情况如下:
- 你可以追踪用户修改和更新了哪些数据库中的列。在这个例子的场景下,不会丢失数据,因为两个用户更新了不同的属性。下一次其他人在浏览英语系的时候,他们会发现 John 和 Jane 所做的所有修改:开始时间成为 8/8/2013,预算成为 $0。
这种方法可以减少可能造成数据丢失的冲突次数,但是如果用户修改同一个实体的相同属性的话,会丢失数据, EF 具体依赖于你如何实现你的更新代码。这种方式不适合 Web 应用程序,因为需要你维护大量的状态,以便追踪所有新值的原始状态。维护大量的状态会影响到程序的性能,因为既需要服务器的资源,又需要将状态保存在页面中 ( 例如,使用隐藏域 )。
- 你可以允许 Jane 的修改覆盖 John 的修改。下一次用户浏览英语系的时候,将会看到 8/8/2013和恢复的 $350,000.00值。这被称为 Client Wins 或者 Last in Wins 场景 ( 客户端的值优先于保存的值 )。像在这节开始介绍的,如果你没有使用任何代码处理并发,这将会自动发生。
- 你可以阻止 Jane 的修改更新到数据库中。通常情况下,我们希望显式一个错误信息。展示数据当前的状态,如果她仍然希望做出这样修改的话,允许她重做修改。这被称为 Store Wins 场景。( 保存的值优先于客户提交的值 ) 在这个教程中,你将要实现 Store Wins 场景。这种方法在提示用户发生什么之前,不会覆盖其他用户的修改。
3.检测并发冲突
你可以通过处理 EF 抛出的 OptimisticConcurrencyException 异常来处理冲突。为了知道什么时候 EF 抛出了这种异常,EF 必须能够检测冲突。因此,你必须合理配置数据库和数据模型。启用冲突检测的一些选项如下:
- 在数据库的表中,包含用于追踪修改的列,在行被修改的时候可以用来进行检测。然后配置 EF 在更新 Update 或者删除 Delete 的 Where 子句中包含检测列。用于追踪的列的数据类型通常是rowVersion,其中并不真的包含实际的日期或者时间值,它是一个顺序的数字,值是在行每次更新的时候的递增加1。如果行被其他用户更新了,那么,此时跟踪列中的值就会与原始值不同,由于 Where 子句的作用,Update 或者 Delete 语句就不会取得需要更新的行。当 EF 发现没有行被 Update 或者 Delete 命令更新的时候 ( 就是说,影响的行数为 0 ),就理解为发生了并发冲突。
- 配置 EF 在 Update 或者 Delete 语句的 Where 中包含所有的原始列。如同第一个方式,如果在数据行被读取之后,行发生了任何修改,Where 将不能取得需要更新的行,这样 EF 就理解为发生了并发冲突。这种方式像使用跟踪列一样有效。但是,如果数据库中的表有很多列,就会导致巨大的 Where 子句,你也必须维护大量的状态。如前所述,维护大量的状态会影响程序的性能,因为既需要消耗服务器资源,也需要在页面中包含状态。因此,不建议使用这种方式,在这个教程中也不使用这种方法。
如果你没有使用追踪列来实现并发,你就必须通过使用 ConcurrencyCheck 特性标记所有的非主属性用在并发跟踪中。这将会使 EF 将所有的列包含在 Update 语句的 Where 子句中。
在本教程剩下的部分,你需要在 Department 实体上增加一个追踪列,创建控制器和视图,然后检查一切是否工作正常。
二、对 Department 实体增加跟踪属性
在 Models\Departments.cs 文件中,增加跟踪属性。
public class Department
{
public int DepartmentID { get; set; } [StringLength(, MinimumLength = )]
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 子句中,提交到数据库。
对模型进行数据迁移。
三、创建 Department 控制器
如同创建其他的控制器一样,创建 Department 控制器和视图,使用如下的设置。
在 Controllers\DepartmentController.cs 中,增加一个 using 语句。
using System.Data.Entity.Infrastructure;
将文件中所有的 “LastName” 修改为 “FullName” ( 共有 4 处 ),使得系控制器中的下拉列表使用教师的全名而不是名字。
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.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.RowVersion = databaseValues.RowVersion;
}
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 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);
}
视图通过页面中的隐藏域保存原始的RowVersion值。当编辑页面提交到服务器的时候,通过模型绑定创建 Department 实例的时候,实例将会拥有原始的 RowVersion 属性值,其他的属性获取新值。然后,当 EF 创建 Update 命令时,命令中将包含查询包含原始RowVersion值的 Where 子句。
在执行 Update 语句之后,如果没有行被更新,EF 将会抛出 DbUpdateConcurrencyException 异常,代码中的 catch 块从异常对象中获取受影响的 Department 实体对象,实体中既有从数据库中读取的值,也有用户新输入的值。
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);
// ...
长的错误信息解释了发生的状况以及如何解决的方式。
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 的RowVersion属性值设置为数据库中新获取的值。新的RowVersion值被保存在重新显示页面的隐藏域中,下一次用户点击保存的时候,当前显示的编辑页面值会被重新获取,这样就可以处理新的并发错误。
在 Views\Department\Edit.cshtml 中,增加一个隐藏域来保存 RowVersion 属性值,紧跟在 DepartmentID 属性之后。
@model ContosoUniversity.Models.Department @{
ViewBag.Title = "Edit";
} <h2>Edit</h2> @using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.ValidationSummary(true) <fieldset>
<legend>Department</legend> @Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion) <div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
在 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后,将会看到如下错误信息。
再次点击 Save。在第二个浏览器窗口中输入的值被保存到数据库中,在 Index 页面显示保存过的值。
五、增加删除页面
对于删除页面,EF 使用类似的方式检测并发冲突。当 HttpGet Delete 方法显示确认页面的时候,视图在隐藏域中包含原始的 RowVersion 值,当用户确认删除的时候,这个值被传递给 HttpPost Delete 方法,当 EF 创建 Delete 命令的时候,在 Where 子句中包含使用原始 RowVersion值的条件,如果命令影响了 0 行 ( 意味着在显示删除确认页面之后被修改了 ),并发异常被抛出,通过传递错误标志为 true ,HttpGet Delete 方法被调用,带有错误提示信息的删除确认页面被显示出来。
在 DepartmentController.cs 中,使用如下代码替换 HttpGet Delete 方法。
public ActionResult Delete(int id, bool? concurrencyError)
{
Department department = db.Departments.Find(id); if (concurrencyError.GetValueOrDefault())
{
if (department == null)
{
ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was deleted by another user after you got the original values. "
+ "Click the Back to List hyperlink.";
}
else
{
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);
}
方法接收一个可选的表示是否是在并发冲突之后重新显示页面的参数,如果这个标志为 true,错误信息通过 ViewBag 传递到视图中。
使用下面的代码替换 HttpPost Delete 方法中的代码 ( 方法名为 DeleteConfirmed )
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(Department department)
{
try
{
db.Entry(department).State = EntityState.Deleted;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToAction("Delete", new { concurrencyError=true } );
}
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);
}
}
你刚刚替换的脚手架代码方法仅仅接收一个记录的 Id
public ActionResult DeleteConfirmed(int id)
将这个参数替换为通过模型绑定创建的 Department 实体实例。这使得可以访问额外的 RowVersion 属性。
public 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>
<fieldset>
<legend>Department</legend> <div class="display-label">
@Html.DisplayNameFor(model => model.Name)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div> <div class="display-label">
@Html.DisplayNameFor(model => model.Budget)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Budget)
</div> <div class="display-label">
@Html.DisplayNameFor(model => model.StartDate)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.StartDate)
</div> <div class="display-label">
@Html.DisplayNameFor(model => model.Administrator.FullName)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Administrator.FullName)
</div>
</fieldset>
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)
<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 和 RowVersion 属性的隐藏域,在 Html.BeginForm 语句之后。
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)
运行 Departments 的 Index 页面,使用同样的 URL 打开第二个浏览器窗口。然后在第一个窗口中打开点击Englis后面的超链接Edit。在第一个窗口中改变值,点击Save。
Index页面确认修改的信息。
现在,在第二个浏览器窗口中点击 Delete。
你将会看到并发错误信息,其中 Department的名称已经使用当前数据库中的值刷新了。
如果再次点击 Delete,你将会被重定向到 Index 页面,在其中 Department 已经被删除了。
六、总结
这里完整地介绍了处理并发冲突。对于处理并发冲突的其他场景,可以在 EF 团队的博客上查阅Optimistic Concurrency Patterns和Working with Property Values。下一次教程将会演示针对教师 Instructor 和学生 Student 实体的表层次的继承。
演练5-6:Contoso大学校园管理系统6的更多相关文章
- 演练5-3:Contoso大学校园管理系统3
在前面的教程中,我们使用了一个简单的数据模型,包括三个数据实体.在这个教程汇中,我们将添加更多的实体和关系,按照特定的格式和验证规则等自定义数据模型. Contoso大学校园管理系统的数据模型如下. ...
- 演练5-5:Contoso大学校园管理系统5
Contoso University示例网站演示如何使用Entity Framework 5创建ASP.NET MVC 4应用程序. Entity Framework有三种处理数据的方式: Data ...
- 演练5-4:Contoso大学校园管理系统4
在之前的教程中,我们已经完成了学校的数据模型.现在我们将读取和显示相关数据,请理解EF加载导航属性的方式. 一.Lazy.Eager.Explicit数据加载 使用EF为实体中的导航属性加载相关数据, ...
- 演练5-7:Contoso大学校园管理系统(实现继承)
***操作视频下载:1 *** 在上一次教程中,你已经能够处理并发异常.这个教程将会展示如何在数据模型中实现继承. 在面向对象的程序设计中,你可以通过继承来清除冗余的代码.在这个教程中,你将要 ...
- 演练5-8:Contoso大学校园管理系统(实现存储池和工作单元模式)
在上一次的教程中,你已经使用继承来消除在 Student 和 Instructor 实体之间的重复代码.在这个教程中,你将要看到使用存储池和工作单元模式进行增.删.改.查的一些方法.像前面的教程一样, ...
- 演练5-1:Contoso大学校园管理1
**演练目的:掌握复杂模型的应用程序开发. Contoso大学校园管理系统功能包括学生.课程.教师的管理. 一.创建MVC Web应用程序 显示效果如下图,操作步骤略. 二.创建数据模型 1.创建学生 ...
- 演练5-2:Contoso大学校园管理2
一.添加列标题排序功能 我们将增加Student/Index页面的功能,为列标题添加超链接,用户可以点击列标题对那一列进行排序. 1.修改Index方法 public ActionResult Ind ...
- Contoso 大学 - 使用 EF Code First 创建 MVC 应用,实例演练
Contoso 大学 Web 示例应用演示了如何使用 EF 技术创建 ASP.NET MVC 应用.示例中的 Contoso 大学是虚构的.应用包括了类似学生注册.课程创建以及教师分配等功能. 这个系 ...
- Contoso 大学 - 7 – 处理并发
原文 Contoso 大学 - 7 – 处理并发 By Tom Dykstra, Tom Dykstra is a Senior Programming Writer on Microsoft's W ...
随机推荐
- Objective-c中@interface、@implementation、@protocal
以下 void print(); }; class AC{ }; 这时候,AI和AC是独立存在,AC不会因为没有和AI建立关系而编译错误,将AC做以下修改后,AI才和AC建立了关系,AC必须实现AI中 ...
- openstack之网络基础
L1:物理层L2:数据链路层,基于mac地址的通信,通过交换机连接:对等传输,即交换机上的一个主机发一个包,连接在该交换机上的所有机器都能收到:L3:网络层,基于ip地址,路由器设备,连接不同网段,进 ...
- 使用LINQ的几个小技巧
这里总结了这些技巧.介绍如何使用LINQ来: 初始化数组 在一个循环中遍历多个数组 生成随机序列 生成字符串 转换序列或集合 把值转换为长度为1的序列 遍历序列的所有子集 如果你在LINQ方面有心得也 ...
- authorization 元素(ASP.NET 设置架构)
authorization 元素(ASP.NET 设置架构) 其他版本 1(共 1)对本文的评价是有帮助 - 评价此主题 [本文档仅供预览,在以后的发行版中可能会发生更改.包含的空白主题用作占位符.] ...
- 关于64位Windows7系统下INF的安装问题
原文 http://bbs.csdn.net/topics/360262492 我的电脑 ——>属性 ——>设备管理器 ——>操作 ——>添加过时硬件 但是,64位系统上报“指 ...
- Windows Phone 8初学者开发—第14部分:在运行时绑定到真实的数据
原文 Windows Phone 8初学者开发—第14部分:在运行时绑定到真实的数据 第14部分:在运行时绑定到真实的数据 原文地址: http://channel9.msdn.com/Series/ ...
- Android百度地图定位
在谈到百度地图.如今,每个人都知道这个时候应该可以了吧.而更多的字不拉.直接朝话题. 访问百度地图api您必须应用key,应用在这里key不用说,有官方的文件说明如何应用上述key. 在这里,百度地图 ...
- iOS之UITableViewCell左右滑动效果
首先在 UITableViewCell.h 中声明一个代理 @protocol UITableViewCellSlideDelegate <UITableViewDelegate> @op ...
- 抽象工厂模式和autofac的使用总结
抽象工厂模式和依赖注入的使用目的都是降低对象直接依赖耦合关系,应该说依赖注入是抽象工厂模式的一种升华,功能更强大. 说到抽象工厂的模式,一般都要先解释下简单工厂,简单工厂就是将对象的实例化抽取出来形成 ...
- BZOJ 1734: [Usaco2005 feb]Aggressive cows 愤怒的牛( 二分答案 )
最小最大...又是经典的二分答案做法.. -------------------------------------------------------------------------- #inc ...