原文: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。接下来会发生什么取决于我们如何处理并发冲突。下面是一些选择:

  • 我们跟踪哪些属性被用户修改,并且只更新修改的列。在示例场景中,不会造成数据丢失,因为两个用户修改的是不同的属性。这种方法可以减少丢失数据的冲突的数量,但是当对相同的属性作出修改时,它不能避免数据丢失。EF是否采用这种方法取决于我们如何实现更新代码。这在web程序中通常是不实际的,因为为了跟踪实体所有属性的原始值以及新值,它要求我们保持大量的状态。保持大量的状态会影响程序的性能,因为它需要服务器资源或必须包含在web页面本身(比如隐藏域)或者cookie中。
  • 让Jane的更改覆盖John的更改。这被称作Client Wins或者Last in Wins场景(所有从客户端获取的值优先于数据存储的值)。如果我们不编码做并发处理,这种情况将会自动发生。
  • 阻止Jane的变化更新到数据库中。通常情况下,我们显示一条错误信息,告诉她数据现在的状态,如果他依然想要修改允许她重试修改。这被称作Store Wins场景(数据存储的值优先于从客户端获取的值)。本教程将实现Store Wins场景。这种方法确保在用户没有意识到发生了什么时,没有修改会被覆盖。

1.3.检测并发冲突:

  我们可以通过处理EF抛出的OptimisticConcurrencyException异常来解决冲突。为了知道什么时候抛出这些异常,必须启用EF检测冲突。因此,我们必须适当地配置数据库和数据模型。启用冲突检测的方法如下:

  • 在数据表中包含一个跟踪列,被用来判定该行什么时候被修改。我们可以在SQL的Update和Delete命令的Where子句中配置EF包含该列。跟踪列的类型通常是时间戳(rowversion)。时间戳的值是一个连续的数字,在每次更新行时增加。在Update或者Delete命令中,Where子句包含跟踪列的原始值(原来的行版本)。如果该行已经被其他用户修改,时间戳列的值将会与原始值不同,因此Update或者Delete语句将无法找到要更新的行,因为Where子句中包含的是原始值。当EF发现Update或者Delete命令没有更新列(也就是说,当受影响的行为0),它就认为发生了并发冲突。
  • 配置EF,在Update和Delete命令的Where子句中包含每列的原始值。

  如果选择配置EF,如果该行被第一次读取时做了任何改变,Where子句将不会返回一行被更新,EF将认为发生了并发冲突。因为数据表中有许多列,这种做法将导致庞大的Where子句,并且要求我们保持大量的状态。如前所述,保持大量的状态会出现性能问题。因此这种做法通常是不推荐的,因此本教程也不使用此种方法。

  如果我们要采用这种方法处理并发,我们必须为所有想要跟踪并发的非主键属性添加ConcurrencyCheck属性。这种改变将启用EF在Update的Where子句中包含所有的列。

  本教程将采用添加时间戳来跟踪Department实体的属性,创建一个控制器和视图,并且添加测试以确保一切工作正常。

2.为Department实体添加开放式并发属性

  修改Models\Department.cs,添加名为RowVersion的跟踪列:

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)]
[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 PatternsWorking with Property Values

[翻译][MVC 5 + EF 6] 10:处理并发的更多相关文章

  1. [翻译][MVC 5 + EF 6] 12[完结]:高级场景

    原文:Advanced Entity Framework 6 Scenarios for an MVC 5 Web Application 1.执行原生SQL查询: EF Code First API ...

  2. [翻译][MVC 5 + EF 6] 7:加载相关数据

    原文:Reading Related Data with the Entity Framework in an ASP.NET MVC Application 1.延迟(Lazy)加载.预先(Eage ...

  3. [翻译][MVC 5 + EF 6] 6:创建更复杂的数据模型

    原文:Creating a More Complex Data Model for an ASP.NET MVC Application 前面的教程中,我们使用的是由三个实体组成的简单的数据模型.在本 ...

  4. [翻译][MVC 5 + EF 6] 5:Code First数据库迁移与程序部署

    原文:Code First Migrations and Deployment with the Entity Framework in an ASP.NET MVC Application 1.启用 ...

  5. [翻译][MVC 5 + EF 6] 2:基础的增删改查(CRUD)

    原文:Implementing Basic CRUD Functionality with the Entity Framework in ASP.NET MVC Application 1.修改Vi ...

  6. [翻译][MVC 5 + EF 6] 1:创建数据模型

    原文:Getting Started with Entity Framework 6 Code First using MVC 5 1.新建MVC项目: 2.修改Views\Shared\_Layou ...

  7. [翻译][MVC 5 + EF 6] 11:实现继承

    原文:Implementing Inheritance with the Entity Framework 6 in an ASP.NET MVC 5 Application 1.选择继承映射到数据库 ...

  8. [翻译][MVC 5 + EF 6] 9:异步和存储过程

    原文:Async and Stored Procedures with the Entity Framework in an ASP.NET MVC Application 1.为什么使用异步代码: ...

  9. [翻译][MVC 5 + EF 6] 8:更新相关数据

    原文:Updating Related Data with the Entity Framework in an ASP.NET MVC Application 1.定制Course的Create和E ...

随机推荐

  1. 基于EF创建数据库迁移

    通过创建的实体类和DbContext类利用EF的Code First数据库迁移创建数据库. 下面看代码. 一.先创建实体类 我先添加一个BaseEntity,里面就一个属性 [Key] public ...

  2. Redis实战之征服 Redis + Jedis + Spring (三)

    一开始以为Spring下操作哈希表,列表,真就是那么土.恍惚间发现“stringRedisTemplate.opsForList()”的强大,抓紧时间恶补下. 通过spring-data-redis完 ...

  3. 【36】绝不重新定义继承而来的non-virtual方法

    1.绝不重新定义继承而来的non-virtual方法,为什么? 首先想想,non-virtual方法是干什么的?也就是说,它的使用场景.父类的non-virtual方法,其实就是告诉子类,继承实现,子 ...

  4. iOS开发——高级技术&摇一摇功能的实现

    摇一摇功能的实现 在AppStore中多样化功能越来越多的被使用了,所以今天就开始介绍一些iOS开发的比较实用,但是我们接触的比较少的功能,我们先从摇一摇功能开始 在 UIResponder中存在这么 ...

  5. [Effective C++ --009]确定对象被使用前已先被初始化

    在确保对象在使用前已先被初始化这一条款的编码实践中,作者为我们总结了三条经验,它们分别是: ------------------------------------------------------ ...

  6. PHP之open_ssl

    http://www.wapm.cn/phpdoc/zh/openssl.installation.html http://liuxufei.com/weblog/jishu/376.html dem ...

  7. nginx rewrite 参数和例子

    http://www.cnblogs.com/analyzer/articles/1377684.html ] 本位转自:http://blog.c1gstudio.com/archives/434 ...

  8. CreateToolhelp32Snapshot

    CreateToolhelp32Snapshot CreateToolhelp32Snapshot函数为指定的进程.进程使用的堆[HEAP].模块[MODULE].线程[THREAD])建立一个快照[ ...

  9. [COCOS2DX]COCOS命令新建项目+编译安卓项目并成功运行

    全程搭建过程参考网址: http://blog.csdn.net/lengxue789/article/details/38116475 http://blog.csdn.net/cbbbc/arti ...

  10. JQuery+EasyUI弹窗代码

    来源:http://www.cnblogs.com/taven/p/3330125.html <head>需要引用的文件: <link href="../JS/EasyUi ...