EntityFramework_MVC4中EF5 新手入门教程之七 ---7.通过 Entity Framework 处理并发

在以前的两个教程你对关联数据进行了操作。本教程展示如何处理并发性。您将创建工作与各Department实体的 web 页和页,编辑和删除Department实体将处理并发错误。下面的插图显示索引和删除的页面,包括一些如果发生并发冲突,则显示的消息。

Department_Index_page_before_edits

Department_Edit_page_2_after_clicking_Save

并发冲突

当一个用户要编辑它,显示实体数据,然后另一个用户更新相同的实体数据第一个用户的更改写入到数据库之前,将发生并发冲突。如果您不启用此类冲突检测,最后谁更新数据库覆盖其他用户的更改。在许多应用中,这种风险是可以接受: 如果有几个用户或一些更新,或者如果不是真的很重要,如果覆盖了一些变化,并发性编程的费用可能超过其益处。在这种情况下,您不需要配置应用程序以处理并发冲突。

保守式并发 (锁定)

如果您的应用程序确实需要防止数据意外丢失在并发性的场景中,做到这一点的一种方法是使用数据库锁。这就被所谓保守式并发例如,从数据库中读取的行之前,你请求锁定为只读或更新访问权限。如果您锁定行更新访问权限,允许没有其他用户锁定该行要么为只读或更新访问权限,因为他们会得到正在更改的数据的一个副本。如果您锁定的只读访问权限的行,别人也可以锁定它为只读访问权限而不是更新。

管理锁定也有缺点。它可以是复杂的程序。它需要大量的数据库管理资源,它可能导致性能问题的应用程序的用户数增加了 (也就是说,它并不很好地扩展)。基于这些原因,并不是所有的数据库管理系统支持保守式并发。实体框架提供了,没有内置的支持,本教程不会告诉你如何实现它。

开放式并发

保守式并发的替代方案是开放式并发开放式并发意味着允许并发冲突发生,然后适当地反应,如果他们这样做。例如,John运行部门编辑页面,改变为英语系的预算金额从 $ 350000.00改为 $0.00。

Changing_English_dept_budget_to_100000

John单击保存之前,Jane运行相同的页面,并更改开始日期字段从 2007/9/1 改为 2013/8/8。

Changing_English_dept_start_date_to_1999

John第一次单击保存,看到他的变化,当浏览器返回到索引页上,然后Jane再单击保存下一步会发生什么取决于你如何处理并发冲突。一些选项包括以下内容:

  • 你可以跟踪用户已修改的属性和更新仅在数据库中相应的列。在示例场景中,没有的数据将会丢失,因为不同的属性由两个用户更新。在下一次有人浏览English department时,他们就会看到John和Jane的修改 —一开始日期 2013/8/8 和预算零美元。

    这种更新方法可以减少冲突,可能会导致数据丢失,但它不能避免 如果有两个人更改到同一个实体的属性 数据丢失。 Entity Framework是否用这种方式取决于您如何实现您更新代码。它往往是不实际的Web应用程序,因为它需要维护大量的状态,以及新值保持一个实体的所有原始属性值的轨道。维护大量的状态会影响应用程序性能,因为它需要消耗服务器资源,或者必须包括在 web 页 (例如,在隐藏字段)。

  • 您可以让Jane的更改覆盖John的更改。在下一次有人浏览English department时,他们会看到 2013/8/8 和还原的 $ 350,000.00 值。这就被所谓Client Wins  Last in Wins的场景。(客户端的值将优先于在数据存储区中的是什么。)因为在这一节,导言中指出,如果你不做任何编码的并发处理,这会自动发生。

  • 你可以阻止Jane的更改对数据库的更新。通常情况下,如果她仍然想要保存,会显示一条错误消息,显示她的数据的当前状态和允许她重新打开页面修改。这就被所谓一个 Store Wins的场景。(数据存储区值将优先于提交的客户端的值)。在本教程中,您将实现 Store Wins方案。此方法确保没有更改将被覆盖没有用户,注意到了发生了什么事。

检测并发冲突

可以通过实体框架将引发的OptimisticConcurrencyException异常处理来解决冲突。为了知道什么时候会引发这些异常,实体框架必须能够检测到冲突。因此,你适当地必须配置数据库和数据模型。启用冲突检测的一些选项包括以下内容:

  • 在数据库表中,包含可用于记录确定何时更改行的跟踪列。你可以配置实体框架UpdateDelete命令中包含WhereSQL 语句中列的。

    跟踪列的数据类型通常是rowversion.rowversion值是一个已更新的行每次递增的顺序编号。UpdateDelete的命令中, Where子句包括跟踪列 (原始行版本) 的原始值。如果由另一个用户更改了正在更新的行,rowversion列中的值是不同于原始值,因此UpdateDelete语句无法找到要更新的Where子句的行。当实体框架发现没有行被更新过的UpdateDelete命令 (那就是,当受影响的行数为零) 时,它将这解释为并发冲突。

  • 配置实体框架可以在UpdateDelete命令的Where子句中的表中包含每个列的原始值。

    在第一个选项,如果在首次读取时发现有任何的改变, Where子句不返回行更新,而实体框架解释作为一个并发冲突。对于有多个列的数据库表,此方法可能会导致非常大的Where子句,并可以要求你保持大量的状态。因为它消耗服务器资源,或者必须包括在 web 页本身,如前文所述,保持大量的状态可以影响应用程序性能可能。因此一般不建议使用此方法,并在本教程中不使用此方法。

    如果你想要实现这种并发性的方法,你有来标记您想要跟踪并发性,通过将ConcurrencyCheck属性添加到他们的实体中的所有非主键属性。这种变化使实体框架可以在UPDATE语句的WHERESQL 子句中包括的所有列。

在本教程的其余部分会添加到Department实体跟踪属性的rowversion、 创建一个控制器和视图,和测试,以验证一切工作正常。

将开放式并发属性添加到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)]
    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属性指定此列的UpdateDelete命令发送到数据库的Where子句中。该属性称为Timestamp,因为以前版本的 SQL Server 使用 SQLTimestamp数据类型之前 SQLrowversion替换它。.Net 类型为  rowversion是一个字节数组。如果你喜欢使用 fluent API,你可以使用IsConcurrencyToken方法来指定跟踪的属性,如下面的示例所示:

modelBuilder.Entity<Department>()
    .Property(p => p.RowVersion).IsConcurrencyToken();

通过添加一个属性更改的数据库模型,所以你需要做另一次迁移。在程序包管理器控制台 (PMC) 中,输入以下命令:

Add-Migration RowVersion 
Update-Database

创建一个部控制器

创建Department控制器和视图的相同的方式,你的其他控制器,使用以下设置:

Add_Controller_dialog_box_for_Department_controller

Controllers\DepartmentController.cs,添加using语句:

using System.Data.Entity.Infrastructure;

更改"LastName"为"FullName"任何一个此文件 (四个点) 以便部管理员下拉列表将包含讲师的完整名称,而不是只是最后的名字。

ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName");

HttpPost Edit方法的现有代码替换为以下代码:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
   [Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, InstructorID")]
    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 clientValues = (Department)entry.Entity;
      var databaseValues = (Department)entry.GetDatabaseValues().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.");
      department.RowVersion = databaseValues.RowVersion;
   }
   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 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);
}

视图将隐藏字段中存储的原始RowVersion值。当模型联编程序创建department实例时,该对象也将有原始RowVersion属性值和其他属性的新值,这是作为由用户编辑页上输入。然后当实体框架创建一个 SQLUPDATE命令,命令将包括WHERE子句的行看起来具有原始RowVersion 值。

如果没有任何行受到UPDATE命令 (即没有行有原始 RowVersion值),实体框架将引发DbUpdateConcurrencyException异常,和catch块中的代码从异常对象中获取受影响的Department实体。此实体已从数据库中读取的值和由用户输入的新值:

var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseValues = (Department)entry.GetDatabaseValues().ToObject();

接下来,该代码将添加为具有数据库值不同于用户在编辑页上输入的每一列的自定义错误消息:

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对象的RowVersion值设置为从数据库中检索新值。这个新的 RowVersion值将存储在隐藏字段中,当重新显示编辑页,并在用户单击保存,在下一次只发生以来的编辑页面重新显示的并发错误将被捕获。

Views\Department\Edit.cshtml,添加一个隐藏的字段来保存紧接该隐藏的字段的DepartmentID属性的RowVersion属性值:

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Department</legend>

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

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>

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>

测试并发处理

运行该站点并单击部门:

Department_Index_page_before_edits

右键单击编辑超链接为Kim Abercrombie和选择在新选项卡中打开然后单击编辑超链接为 Kim Abercrombie。这两个窗口显示相同的信息。

Department_Edit_page_before_changes

更改第一个浏览器窗口中的字段并单击保存.

Department_Edit_page_1_after_change

浏览器显示更改后的值与索引页。

Departments_Index_page_after_first_budget_edit

更改第二个浏览器窗口中的任何字段并单击保存.

Department_Edit_page_2_after_change

单击第二次的浏览器窗口中的保存您看到一条错误消息:

Department_Edit_page_2_after_clicking_Save

再次单击保存你在第二个浏览器中输入的值是与您在第一次浏览器中更改的数据的原始值一起保存。Index页出现时,你看到的已保存的值。

Department_Index_page_with_change_from_second_browser

更新删除页

对于删除页面,实体框架检测到并发冲突而引起的其他编辑新闻部以类似的方式。HttpGet Delete方法显示确认视图时,视图包括原始 RowVersion值的隐藏字段中。该值是然后提供给用户确认删除时,将调用HttpPost Delete方法。当实体框架创建 SQLDELETE命令时,它包括一个WHERE子句与原始 RowVersion值。如果在零行命令结果的影响 (即行更名后显示删除确认页),并发异常,和HttpGet Delete方法称为一个错误标志设置为true以重新显示确认页,并显示错误消息。它也是可能的零行受到影响,因为该行已被删除由另一个用户,所以在这种情况下显示不同的错误消息。

DepartmentController.cs,用下面的代码替换HttpGet Delete方法:

public ActionResult Delete(int id, bool? concurrencyError)
{
    Department department = db.Departments.Find(id);

    if (concurrencyError.GetValueOrDefault())
    {
        if (department == null)
        {
            ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
                + "was deleted by another user after you got the original values. "
                + "Click the Back to List hyperlink.";
        }
        else
        {
            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);
}

该方法接受一个可选的参数,该值指示是否在页面被重新显示后并发错误。如果此标志为true,错误消息是发送到使用ViewBag属性的视图。

HttpPost Delete方法 (称为DeleteConfirmed) 中的代码替换为以下代码:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(Department department)
{
    try
    {
        db.Entry(department).State = EntityState.Deleted;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    catch (DbUpdateConcurrencyException)
    {
        return RedirectToAction("Delete", new { concurrencyError=true } );
    }
    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);
    }
}

在您刚更换搭建代码中,这种方法接受只有一个记录 ID:

        public ActionResult DeleteConfirmed(int id)

你改变了此参数,对Department实体实例创建的模型联编程序。这使您可以访问到除了记录关键的 RowVersion属性值。

        public ActionResult Delete(Department department)

Delete,也有从DeleteConfirmed 改变操作方法的名称。搭建的代码命名为DeleteConfirmed ,给出了HttpPost方法独特的签名HttpPost Delete方法。(CLR 需要有不同的方法参数的重载的方法)。现在,签名是独一无二的你可以坚持使用 MVC 公约并使用HttpPost相同的名称,HttpGet删除方法。

如果捕获到并发错误,代码重新显示删除确认页,并提供一个标志,指示它应该显示并发错误消息。

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.DisplayNameFor(model => model.Name)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Name)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Budget)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Budget)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.StartDate)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.StartDate)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Administrator.FullName)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Administrator.FullName)
    </div>
</fieldset>
@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
   @Html.HiddenFor(model => model.DepartmentID)
    @Html.HiddenFor(model => model.RowVersion)
    <p>
        <input type="submit" value="Delete" /> |
        @Html.ActionLink("Back to List", "Index")
    </p>
}

此代码将添加h2h3标题之间的错误消息:

<p class="error">@ViewBag.ConcurrencyErrorMessage</p>

它将LastName替换FullNameAdministrator字段:

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

最后,它添加隐藏的字段的DepartmentID RowVersion的属性Html.BeginForm语句之后:

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

运行部门索引页。右键单击英语系的删除超链接,然后选择在新窗口中打开第一个窗口点击编辑超链接为英语系。

在第一个窗口中,更改其中一个值,并单击保存 :

Department_Edit_page_after_change_before_delete

索引页反映此更改。

Departments_Index_page_after_budget_edit_before_delete

在第二个窗口中,单击删除.

Department_Delete_confirmation_page_before_concurrency_error

你看到并发错误消息,并与当前数据库刷新部值。

Department_Delete_confirmation_page_with_concurrency_error

如果再次单击删除,您正在被重定向到索引页面,显示该署已被删除。

摘要

这样就完成了处理并发冲突的简介。关于其他的方法来处理各种并发性的场景的信息,请参阅在实体框架团队博客上的乐观并发模式使用属性值下一个教程演示如何实现每个层次结构一个表继承的InstructorStudent的实体。

 

 

这篇翻译了很久。。。。有很多概念性的东西。不知翻译的是否清楚?附上原文地址

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

posted @ 2015-02-09 16:02  178mz  阅读(1823)  评论(2编辑  收藏  举报