Contoso 大学 - 7 – 处理并发

原文地址: http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/handling-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application

全文目录: Contoso 大学 - 使用 EF Code First 建立 MVC 应用

在上一次的教程中咱们处理了关联数据问题。这个教程演示如何处理并发问题。你将使用 Department 实体建立一个页面,这个页面在支持编辑和删除的同时,还能够处理并发错误。下面的截图演示了 Index 页面和 Delete 页面,包括在出现并发冲突的时候提示的一些信息。

 

 

7-1 并发冲突

 

并发冲突出如今这样的时候,一个用户正在显示并编辑一个实体,可是在这个用户将修改保存到数据库以前,另外的一个用户却更新了一样的实体。若是你没有经过 EF 检测相似的冲突,最后一个更新数据的用户将会覆盖其余用户的修改。在一些程序中,这样的风险是能够接受的,若是只有不多的用户,或者不多的更新,甚至对数据的覆盖不是真的很关键,或者解决并发的代价超过了支持并发所带来的优点。在这种状况下,你就不须要让你的程序支持并发冲突的处理。javascript

 

7-1-1 悲观并发 ( 锁定 )

 

若是你的应用须要在并发环境下防止偶然的数据丢失,一种方式是经过数据库的锁来实现。这种方式被称为悲观并发。例如,在从数据库中读取一行数据以前,能够申请一个只读锁,或者一个更新访问锁。若是你对数据行使用了更新访问锁,就没有其余的用户能够获取不论是只读锁仍是更新访问锁,由于他们可能获取正在被修改中的数据。若是你使用只读锁来锁定一行,其余用户也可使用只读访问,可是不能进行更新。html

 

管理锁有一些缺点,对程序来讲可能很复杂。它须要重要的数据库管理资源,对于大量用户的时候可能致使性能问题 ( 扩展性很差 ),因为这些缘由,不是全部的数据库管理系统都支持悲观锁。EF 对悲观锁没有提供内建的支持,这个教程也不会演示如何实现它。java

 

7-1-2 乐观并发

除了悲观并发以外的另外一方案是乐观并发。乐观并发意味着容许并发冲突发生,若是出现了就作出适当的反应。例如,John 执行 Department 的编辑页面,将 English 系的 Budget $350,000.00 修改成 $100,000.00 ( John 管理与 English 有竞争的系,但愿将一些资金转移到他本身的系使用 )数据库

 

John 点击保存 Save 以前,Jane 运行一样的页面,将开始时间 Start Date 字段从 9/1/2007 修改成 1/1/1999 ( Jane 管理历史系,但愿它的历史更加悠久 )浏览器

 

 

John 先点击保存 Save,而后在回到 Index 页面的时候看到本身的修改。而后 Jane 点击保存 Save。下一步发生什么取决于如何处理并发冲突。可能的状况以下:服务器

  •  你能够追踪用户修改和更新了哪些数据库中的列。在这个例子的场景下,不会丢失数据,由于两个用户更新了不一样的属性。下一次其余人在浏览英语系的时候,他们会发现 John Jane 所作的全部修改:开始时间成为 1/1/1999,预算成为 $100,000.00

 这种方法能够减小可能形成数据丢失的冲突次数,可是若是用户修改同一个实体的相同属性的话,会丢失数据, EF 具体依赖于你如何实现你的更新代码。这种方式不适合 Web 应用程序,由于须要你维护大量的状态,以便追踪全部新值的原始状态。维护大量的状态会影响到程序的性能,由于既须要服务器的资源,又须要将状态保存在页面中 ( 例如,使用隐藏域 )并发

  •  你能够容许 Jane 的修改覆盖 John 的修改。下一次用户浏览英语系的时候,将会看到 1/1/1999 和恢复的 $350,000.00 值。这被称为 Client Wins 或者 Last in Wins 场景 ( 客户端的值优先于保存的值 )。像在这节开始介绍的,若是你没有使用任何代码处理并发,这将会自动发生。
  • 你能够阻止 Jane 的修改更新到数据库中。一般状况下,咱们但愿显式一个错误信息。展现数据当前的状态,若是她仍然但愿作出这样修改的话,容许她重作修改。这被称为 Store Wins 场景。( 保存的值优先于客户提交的值 ) 在这个教程中,你将要实现 Store Wins 场景。这种方法在提示用户发生什么以前,不会覆盖其余用户的修改。

7-1-3 检测并发冲突

 

你能够经过处理 EF 抛出的 OptimisticConcurrencyException 异常来处理冲突。为了知道何时 EF 抛出了这种异常,EF 必须可以检测冲突。所以,你必须合理配置数据库和数据模型。启用冲突检测的一些选项以下:mvc

  • 在数据库的表中,包含用于追踪修改的列,在行被修改的时候能够用来进行检测。而后配置 EF 在更新 Update 或者删除 Delete Where 子句中包含检测列。用于追踪的列的数据类型一般是 timestamp,可是其中并不真的包含实际的日期或者时间值。相反,值是在行每次更新的时候的一个递增值( 所以,在最近的 SQL Server 中,一样的类型被称为行版本 rowversion ) 。在更新 Update 或者 Delete 命令中,Where 子句中包含跟踪列的原始值。若是行被其余用户更新了,那么,此时跟踪列中的值就会与原始值不一样,因为 Where 子句的做用,Update 或者 Delete 语句就不会取得须要更新的行。当 EF 发现没有行被 Update 或者 Delete 命令更新的时候 ( 就是说,影响的行数为 0 ),就理解为发生了并发冲突。
  •     配置 EF Update 或者 Delete 语句的 Where 中包含全部的原始列。如同第一个方式,若是在数据行被读取以后,行发生了任何修改,Where 将不能取得须要更新的行,这样 EF 就理解为发生了并发冲突。这种方式像使用跟踪列同样有效。可是,若是数据库中的表有不少列,就会致使巨大的 Where 子句,你也必须维护大量的状态。如前所述,维护大量的状态会影响程序的性能,由于既须要消耗服务器资源,也须要在页面中包含状态。所以,不建议使用这种方式,在这个教程中也不使用这种方法。

在本教程剩下的部分,你须要在 Department 实体上增长一个追踪列,建立控制器和视图,而后检查一切是否工做正常。app

 

注意:若是你没有使用追踪列来实现并发,你就必须经过使用 ConcurrencyCheck 特性标记全部的非主属性用在并发跟踪中。这将会使 EF 将全部的列包含在 Update 语句的 Where 子句中。asp.net

 

7-2  Department 实体增长跟踪属性

Models\Departments.cs 文件中,增长跟踪属性。

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

 

Timestamp 特性指定随后的列将会被包含在 Update 或者 Delete 语句的 Where 子句中。

 

 

 

7-3 建立 Department 控制器

 

如同建立其余的控制器同样,建立 Department 控制器和视图,使用以下的设置。

 

Controllers\DepartmentController.cs 中,增长一个 using 语句。

 

using System.Data.Entity.Infrastructure;

 

将文件中全部的 “LastName” 修改成 “FullName” ( 共有 4 ),使得系控制器中的下拉列表使用教师的全名而不是名字。

 

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);
}
复制代码

 

 

视图经过页面中的隐藏域保存原始的时间戳。当编辑页面提交到服务器的时候,经过模型绑定建立 Department 实例的时候,实例将会拥有原始的 Timestamp 属性值,其余的属性获取新值。而后,当 EF 建立 Update 命令时,命令中将包含查询包含原始 Timestamp 值的 Where 子句。

 

在执行 Update 语句以后,若是没有行被更新,EF 将会抛出 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 值被保存在从新显示页面的隐藏域中,下一次用户点击保存的时候,当前显示的编辑页面值会被从新获取,这样就能够处理新的并发错误。

 

Views\Department\Edit.cshtml 中,增长一个隐藏域来保存 Timestamp 属性值,紧跟在 DepartmentID 属性以后。

 

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

 

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>
复制代码

 

7-4 测试乐观并发处理

 

运行程序,点击 Departments.

 

 

点击 Edit 超级连接,而后再打开一个新的浏览器窗口,窗口中使用相同的地址显示相同的信息。

 

 

在第一个浏览器的窗口中修改一个字段的内容,而后点击 Save

 

 

浏览器回到 Index 页面显示修改以后的值。

 

 

在第二个浏览器窗口中将一样的字段修改成不一样的值,

 

 

在第二个浏览器窗口中,点击 Save,将会看到以下错误信息。

 

 

再次点击 Save。在第二个浏览器窗口中输入的值被保存到数据库中,在 Index 页面显示的时候出如今页面上。

 

 

7-5 增长删除页面

 

对于删除页面,EF 使用相似的方式检测并发冲突。当 HttpGet Delete 方法显示确认页面的时候,视图在隐藏域中包含原始的 Timestamp 值,当用户确认删除的时候,这个值被传递给 HttpPost Delete 方法,当 EF 建立 Delete 命令的时候,在 Where 子句中包含使用原始 Timestamp 值的条件,若是命令影响了 0 ( 意味着在显示删除确认页面以后被修改了 ),并发异常被抛出,经过传递错误标志为 true HttpGet Delete 方法被调用,带有错误提示信息的删除确认页面被显示出来。

 

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 传递到视图中。

 

使用下面的代码替换 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 区域将 LastName 替换为 FullName

 

 

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

 

 

最后,增长了用于 DepartmentId Timestamp 属性的隐藏域,在 Html.BeginForm 语句以后。

 

 

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

 

 

运行 Departments Index 页面,使用一样的 URL 打开第二个浏览器窗口。

 

在第一个窗口中,在某个系上点击 Edit ,而后修改一个值,先不要点击 Save

 

 

在第二个窗口中,在一样的系上,选择 Delete ,删除确认窗口出现了。

 

 

在第一个窗口中,点击 Save,在 Index 页面中确认修改信息。

 

 

如今,在第二个浏览器窗口中点击 Delete,你将会看到并发错误信息,其中 Department的名称已经使用当前数据库中的值刷新了。

 

 

 

若是再次点击 Delete,你将会被重定向到 Index 页面,在显示中 Department 已经被删除了。

 

这里完整地介绍了处理并发冲突。对于处理并发冲突的其余场景,能够在 EF 团队的博客上查阅Optimistic Concurrency PatternsWorking with Property Values。下一次教程将会演示针对教师 Instructor 和学生 Student 实体的表层次的继承。

相关文章
相关标签/搜索