您的位置:首页 > 编程语言 > ASP

【EntityFramework系列教程七,翻译】为ASP.NET MVC中EntityFramework添加并发冲突处理机制

2012-04-27 17:12 971 查看
上两章中你已经学会如何处理相关数据,本章将介绍如何处理并发冲突。为此你将创建若干页面处理Department信息,编辑删除Department信息的页面将采用并发冲突机制。以下一些截图展示了在Index和Delete页面中的状况,以及当检测到并发冲突的时候所显现的一些错误信息。





【并发冲突】

“并发冲突”就是指一个用户准备编辑一条实体记录的,同时另外一个用户在该用户之前已更新了同样的消息。倘若你不设置相关检测并发冲突的机制,那么永远是最后一个客户的变更信息覆盖先前一切更改。在许多情形下此风险是可以接受的(比如用户数量极少,更新也极少,或是即便一些数据被覆盖了也并不足以致命……),因此此时使用并发冲突检测机制显得得不偿失,也就没有必要为程序处理冲突检测。

【“悲观”(锁定式)冲突检测】

如你的程序需要避免在并发冲突之时数据丢失,办法之一就是采用“数据库锁定”。这个被成为“(悲观)锁定式冲突检测”,当你从数据库中读取一条记录的时候,你要不为“只读”发出请求,要不为“更新”发出请求。假设你为“更新”锁住一行记录的时候,其它用户——无论再想发出“只读”式锁定或是“仅更新”时锁定都无济于事,因为他们只能获取一个正在被修改的数据副本;如果你为“只读”发出请求,那么其它用户也只能为“只读”发出请求而不是为“更新”。

管理锁定机制有一些缺陷:首先它使得程序复杂化——它需要数据库管理资源的数字签名,并且当用户数量骤增时(因为无法很好地判断究竟多少用户)可能导致程序性能下降等问题……考虑这些因素,并不是所有的数据库管理系统都支持此方式,对于EntityFramework而言,内置也不支持此方式检测处理冲突,因此本章不讨论如何实现该模式。

【乐观(开放式)冲突检测】

“悲观冲突检测”的替代品就是“乐观(开放式)冲突处理检测机制”——它意味着当下允许发生冲突,并且适当对其进行处理。现在假设John运行Departments的编辑页面,把”英语系“的财政预算从“$350,000.00”改到“$100,000.00”(John打算给予一个具备竞争性的系更多预算,因此他想从自己的系中抽出些钱给它们)。



但是在保存(点击Save)前,Jane同样运行了该页面,并且把“Start Date”(起始日期)从“9/1/2007”改成了“1/1/1999”(因为Jane负责历史系,因此她想提高一些该系的资历)。

John首先点击了Save,当页面回到Index之后他看到了数据发生的变化;随后Jane也点击了Save,那么下一步就取决于你如何处理该并发冲突了——以下是一些选择:

1)你可以跟踪哪些字段被修改了,在数据库中只更新对应的部分……在这个示例中没有数据丢失,因为两个人分别更新了两个不同的字段,下一次如果有第三者查看英语系的情况,那么他(她)会发现John和Jane同时修改后的变化:起始日期变成了“1/1/1999”,预算则是$100,000.00。

这个方法可以有效减少导致数据丢失的并发冲突,但是当多个针对同一个属性字段进行修改的时导致的数据丢失,此方法无能为力;至于EntityFramework是否用此方式工作完全取决于你的代码,在传统的Web程序中此方法并不使用,因为这需要你保存大量的数据记录状态,跟踪“新值”和“旧值”,维护如此大数量的记录势必导致程序性能下降等问题——因为它既需占用要服务器资源,同时也一定包含在页面自身中(比如需要通过“隐藏域”存储维护数据状态)。

2)你可以让Jane做出的改变覆盖John的,那么下一次当第三者浏览这个英语系时,他们就会看到1/1/1999以及$350,000.00,这被称为“Client Wins(客户端胜)”或是“Last Wins Senario(谁最后谁胜)”的策略(因为客户端的数据优先级高于数据库中实际存储的数据,因此如果你代码中不加以处理,那么默认情况下也就是这种情况)。

3)你可以阻止Jane对数据库的更新——典型情况下你可以向Jane展示一个错误信息,告知其当前数据的状态,并允许她继续做更改(如果她坚持要那样做的话)。这个被成为“Store Wins(存储胜)”策略。此时,数据库中存储的数据优先级高于客户端数据,你也将在本章节中实现该策略,该策略保证在任何一个客户没有被告知发生任何事时,数据不会发生覆盖重写。

【检测并发冲突】

你可以通过处理EntityFramework抛出的“”达到此目的。为了知道何时抛出这些异常,EntityFramework必须能够检测冲突,因此你也就必须要合理地配置数据库以及数据模型。以下包含了一些启用冲突检测机制的选项:

1)在数据库表中,包含一个“跟踪”字段以此决定何时改行发生了改变。随后你可以把该字段包含在SQL的Update或者Delete命令中的Where条件中。跟踪字段最典型的是timestamp,可是它实际上并不包含任何“日期”或者“时间”;取而代之的是每当改行发生变化时,此字段中的“序列码”就会自增(在最近的SQL Server版本中又被成为rowversion)。在Update或是Delete命令中,Where条件中包含了该字段的原始数据。如果该行被某人所改变,那么该字段的数值势必就与原先的数值不同,Update或Delete方法也就无法找到对应的行进行操作,当EntityFramework发现使用了Update或者Delete但是没有任何效果的时候(也就是受影响的行数为0),它会以“并发冲突”抛出异常而中断。

2)对EntityFramework做一些配置,使得其在Update或Delete语句中的Where条件里包含每一个列的原来数值。

第一个方法,自原始记录从数据库中被读取后,一旦对改行所做的任何改变导致Where将无法更新改行;这将导致EntityFramework以“并发冲突”检测中断程序。此方法作为使用跟踪字段非常有效。不过,对于有着大量列的表而言这样的方法将产生一个庞大的Where条件语句,同时需要维护大量的状态等;正如上述所提及到的——维护大量状态可导致程序性能下降,因为这些状态不是保存在服务端就是保存在web页面中。因此这个方法通常而言不推荐,在本章节中也不是使用此方法。

在本章节其余部分你将对Department增加了一个跟踪字段,创建一个控制器并测试其是否正常工作。

注意:如果你在没有“跟踪字段”的情况下实现了冲突检测机制,那么你不得不把非主键的所有字段标识为“冲突跟踪字段”(通过添加“ConcurrencyCheck”属性)。此改变将导致EntityFramework在Update或者是Delete语句的Where条件中包含所有的列字段。

【为Department实体添加一个跟踪属性】

在“Models\Department.cs”中添加一个如下的跟踪属性:

[Timestamp]
public Byte[] Timestamp { get; set; }


Timestamp属性指定了该字段将被包含在Update或Delete语句中的Where条件中并发往数据库。

【创建一个Department控制器】

使用向导,如先前一样地创建此控制器和视图:



在“Controllers\DepartmentController.cs”,添加一个“using”块:

using System.Data.Entity.Infrastructure;


把该文件中所有的LastName(总共出现四次)变更为"FullName",这样系管理员的下拉列表选项将包含教师的全名,而不只是“姓”。

用以下代码替换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);
}


视图中在一个隐藏域中存储了原始的timestamp数值,当模型绑定机制创建了Department实体类时,该对象自然就拥有原始的timestamp属性值以及其它新赋予的数值(通过Edit页面输入的)。当EntityFramework创建了一个更新命令的时候,首先会去检查拥有该原始timestamp值的行。如果更新之后受影响的行数是0,那说明检测到并发冲突(于是抛出“
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在页面重新加载之后也将被存储在隐藏域中。当下一次用户点击Save的时候,自动Edit页重新加载显示以后的并发性冲突才会被捕获。

在“Views\Department\Edit.cshtml”中,为TimeStamp添加一个隐藏域,它紧挨着DepartmentId隐藏域之后:

@Html.HiddenFor(model => model.Timestamp)


在“Views\Department\Index.cshtml”中,用以下代码替换现有代码把每一行的超链接移至左边,同时在Administrator列中改变页面的标题和列标题,以便显示全名而非“姓”。

@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页之后你可以看到该数据的变化:



【增加一个删除页】

对于删除页而言,EntityFramework用同样类似的手法检测到了并发冲突。当HttpGet方式的Delete方法显示了确认视图之后,该视图应包含一个存储TimeStamp的隐藏域。对于当用户确认并且调用HttpPost的Delete方法时,隐藏域值在那时是可用的;当EntityFramework自生成了删除语句时,它会在WHERE中自动包含原始的TimeStamp值,那么如果执行后的结果是“0行受到影响”,这意味着在删除确认页面出现之后数据行已经发生了改变),并发冲突异常将被捕获并且抛出,HttpGet方式的Delete方法参数中的错误标志位也被设置为true用以展示错误消息。

在“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属性发送到对应View中去。

用以下代码替换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字段中用FullName替换了LastName:

<div class="display-label">
@Html.LabelFor(model => model.InstructorID)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Administrator.FullName)
</div>


最后,在Html.BeginForm之后为DepartmentId和TimeStamp增加了对应的两个隐藏域:

@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.Timestamp)


运行Departments索引页,另外再打开一个浏览器窗口,重复此步骤。

在第一个窗口中,点击Edit并更改某个属性的数值,不要点击Save:



第二个窗口中点击同一个deparment中的Delete按钮,Delete确认框弹出:



在第一个窗口里点击更新,索引页复现表明数据得到更新。



现在在第二个窗口里点击Delete,你就将看到由于冲突所带来的错误信息,并且Department中的数值将被实际数据库中所有的数据重新刷新并加载:



如果你再次点击了Delete按钮,那么你将重新回到整个项目的起始索引页,从而看到该项确实被删除了。

至此我们已经结束了有关并发冲突的介绍和处理方法,有关于此更多的信息请参考 Optimistic Concurrency PatternsWorking with Property Values。下一节我们将介绍如何为Instructor和Student实现“树形表结构继承”。

关于其它EntityFramework资源您可以本系列最后一篇末尾处找到。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐