[翻译][MVC 5 + EF 6] 10:处理并发

时间:2021-08-18 02:57:56

原文: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:

[翻译][MVC 5 + EF 6] 10:处理并发

  在John点击保存之前,Jane把English的department的Start Date从9/1/2007修改为8/8/2013:

[翻译][MVC 5 + EF 6] 10:处理并发

  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.测试开放式并发处理

[翻译][MVC 5 + EF 6] 10:处理并发

  在English department的Edit右键Open in new tab,然后点击English department的Edit链接:

[翻译][MVC 5 + EF 6] 10:处理并发

  在第一个浏览器标签修改,并保存:

[翻译][MVC 5 + EF 6] 10:处理并发

[翻译][MVC 5 + EF 6] 10:处理并发

  在浏览器的第二个标签修改并保存:

[翻译][MVC 5 + EF 6] 10:处理并发

[翻译][MVC 5 + EF 6] 10:处理并发

  再次点击保存:

[翻译][MVC 5 + EF 6] 10:处理并发

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链接.

  在浏览器的第一个标签修改并保存:

[翻译][MVC 5 + EF 6] 10:处理并发

[翻译][MVC 5 + EF 6] 10:处理并发

  在浏览器的第二个标签页,点击Delete:

[翻译][MVC 5 + EF 6] 10:处理并发

[翻译][MVC 5 + EF 6] 10:处理并发

  再次点击Delete,将会删除该department,然后导航到Index页面。

  处理各种并发场景的其他方法,请查看:Optimistic Concurrency PatternsWorking with Property Values

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

  1. &lbrack;翻译&rsqb;&lbrack;MVC 5 &plus; EF 6&rsqb; 12&lbrack;完结&rsqb;:高级场景

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

  2. &lbrack;翻译&rsqb;&lbrack;MVC 5 &plus; EF 6&rsqb; 7:加载相关数据

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

  3. &lbrack;翻译&rsqb;&lbrack;MVC 5 &plus; EF 6&rsqb; 6:创建更复杂的数据模型

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

  4. &lbrack;翻译&rsqb;&lbrack;MVC 5 &plus; EF 6&rsqb; 5:Code First数据库迁移与程序部署

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

  5. &lbrack;翻译&rsqb;&lbrack;MVC 5 &plus; EF 6&rsqb; 2:基础的增删改查&lpar;CRUD&rpar;

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

  6. &lbrack;翻译&rsqb;&lbrack;MVC 5 &plus; EF 6&rsqb; 1:创建数据模型

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

  7. &lbrack;翻译&rsqb;&lbrack;MVC 5 &plus; EF 6&rsqb; 11:实现继承

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

  8. &lbrack;翻译&rsqb;&lbrack;MVC 5 &plus; EF 6&rsqb; 9:异步和存储过程

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

  9. &lbrack;翻译&rsqb;&lbrack;MVC 5 &plus; EF 6&rsqb; 8:更新相关数据

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

随机推荐

  1. 如何利用rem在移动端不同设备上让字体自适应大小

    本人也是一个刚刚接触前端的小虾米,对于移动端这一块更是一抹眼的黑,前端时间接手开始一个移动端的项目,在网上查询了一下rem的作用,百度搜索下来全是介绍rem的作用原理的(rem是根据根元素计算的),然 ...

  2. Google分布式构建软件之二:构建系统如何工作

    分布式软件构建第二部分:构建系统如何工作 注:本文英文原文在google开发者工具组的博客上[需要FQ],以下是我的翻译,欢迎转载,但请尊重作者版权,注名原文地址. 上篇文章中提到了在Google,所 ...

  3. AEAI CRM客户关系管理升级说明

    本次发版的AEAI CRM_v1.5.1版本为AEAI CRM_v1.5.0版本的升级版本,该产品现已开源并上传至开源社区http://www.oschina.net/p/aeaicrm. 1 升级说 ...

  4. 【转】iOS实时卡顿监控

    转自http://www.tanhao.me/code/151113.html/ 在移动设备上开发软件,性能一直是我们最为关心的话题之一,我们作为程序员除了需要努力提高代码质量之外,及时发现和监控软件 ...

  5. Windows下AndroidStudio 中使用Git&lpar;AndroidStudio项目于GitHub关联&rpar;

    前提条件 : 1. 安装 Git 客户端 下载链接 2. 有 GitHub 账号 (假设你已经有了一些git基础, 如果还一点都不会, 请去找其他加成学习) AndroidStudio项目发布到Git ...

  6. vue1&period;0和vue2&period;0的区别&lpar;二&rpar;

    这篇我们继续之前的vue1.0和vue2.0的区别(一)继续说 四.循环 学过vue的同学应该知道vue1.0是不能添加重复数据的,否则它会报错,想让它重复添加也不是不可以,不过需要定义别的东西 而v ...

  7. Java-IO之CharArrayReader

    CharArrayReader是字符数组输入流,CharArrayReader用于读取字符数组,继承于Reader操作的数据是以字符为单位. (1)CharArrayReader实际上是通过字符数组去 ...

  8. Go中链路层套接字的实践

    1. 介绍 2. 服务端 3. 协议头部 4. 客户端 5. 总结 1. 介绍 接上次的博客,按照约定的划分,还有一层链路层socket.这一层就可以自定义链路层的协议头部(header)了,下面是目 ...

  9. 如何在JSP中获得Cookie对象

    Cookie cookies[]=request.getCookies(); //读出用户硬盘上的Cookie,并将所有的Cookie放到一个cookie对象数组里面 Cookie sCookie=n ...

  10. Luogu 1583 - 魔法照片 - &lbrack;简单排序题&rsqb;

    题目链接:https://www.luogu.org/problemnew/show/P1583 题目描述一共有n(n≤20000)个人(以1--n编号)向佳佳要照片,而佳佳只能把照片给其中的k个人. ...