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

原文: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(50, MinimumLength = 3)]
    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

时间: 2024-10-14 00:46:44

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

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

原文:Reading Related Data with the Entity Framework in an ASP.NET MVC Application 1.延迟(Lazy)加载.预先(Eager)加载.显式(Explicit)加载: EF加载相关数据到实体导航属性有以下几种方式: 延迟加载:当实体第一次读取时,相关数据没有加载.当第一次试图访问导航属性时,所需的导航数据自动加载.这导致多条查询语句被发送到数据库:一条查询实体本身,一条查询实体相关数据.DbContext类默认启用延迟加载

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

原文:Creating a More Complex Data Model for an ASP.NET MVC Application 前面的教程中,我们使用的是由三个实体组成的简单的数据模型.在本教程中,我们将添加更多的实体和关系,并通过指定格式.验证和数据库映射规则来自定义数据模型.有两种方式来定义数据模型:一种是给实体类添加属性,另一种是在数据库上下文类添加代码. 当我们完成后,实体类将完成下图所示的数据模型: 1.通过使用属性来自定义数据模型: 1.1.DateType属性: 修改Mo

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

原文:Code First Migrations and Deployment with the Entity Framework in an ASP.NET MVC Application 1.启用Code First迁移: 当我们开发一个新的程序时,数据模型经常会发生改变,每次模型发生改变时,就会变得与数据库不同步.我们之前配置EF在每次数据模型发生改变时自动删除然后重建数据库.当我们增加.删除或者改变实体类或者改变DbContext类时,在程序下次运行时将会自动删除已经存在的数据库,并且创

[翻译][MVC 5 + EF 6] 1:准备工作

原文:Getting Started with Entity Framework 6 Code First using MVC 5 1.新建MVC项目: 2.修改Views\Shared\_Layout.cshtml: <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=d

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

原文:Implementing Basic CRUD Functionality with the Entity Framework in ASP.NET MVC Application 1.修改Views\Student\Details.cshtml: @model ContosoUniversity.Models.Student @{ ViewBag.Title = "Details"; } <h2>Details</h2> <div> <

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

原文:Implementing Inheritance with the Entity Framework 6 in an ASP.NET MVC 5 Application 1.选择继承映射到数据库表: 在School数据模型里面,Instructor和Student类有几个属性是相同的: 假设我们想要消除Instructor和Student实体属性的冗余代码.或者我们想要编写一个可以格式化name的服务,而不用考虑这个name是来自一个instructor还是student.我们可以创建一个

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

原文:Async and Stored Procedures with the Entity Framework in an ASP.NET MVC Application 1.为什么使用异步代码: 一个服务器可用的线程数量是有限的,在高负载的情况下所有的可用线程都可能在使用.在这种情况下,在线程被释放之前服务器不能处理新的请求.在同步代码中,很多线程被占用但是实际上没有做任何操作,因为它们在等待I/O完成.在异步代码中,当一个进程在等待I/O完成的过程中,它的线程将会被释放用于处理其他请求.这

[翻译][MVC 5 + EF 6] 4:弹性连接和命令拦截(Command Interception)

原文:Connection Resiliency and Command Interception with the Entity Framework in an ASP.NET MVC Application [注:本节教程可以选择性学习] 本节教程将学习EF6的两个重要特性,这两个特性在我们将程序部署在云环境时特别有用: 弹性连接(connection resiliency):遇到瞬时的连接错误时自动重试连接. 命令拦截(command interception):捕获所有发送到数据库的查询

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

原文:Updating Related Data with the Entity Framework in an ASP.NET MVC Application 1.定制Course的Create和Edit页面: 修改CourseController.cs的Create和Edit方法: public ActionResult Create() { PopulateDepartmentsDropDownList(); return View(); } [HttpPost] [ValidateAnt