MVC5 Entity Framework学习之处理并发

之前你已经学习了如何更新数据,那么在本节你将学习如何在当多个用户在同一时间更新同一实体时处理冲突。

修改与Department实体相关的那些页面以便它们能够i处理并发错误。下面的截图是Index 和Delete页面,以及当出现并发冲突时的错误消息。

并发冲突

当一个用户对实体的数据进行编辑,然后另一个用户在前一个用户将更改写入到数据库之前更新同一实体的数据时将发生并发冲突。如果你没有启用冲突检测,那么最后一次对数据库的更新将会覆盖其他用户对数据库所做的更改。在大部分应用程序中,这种风险是可以接受的:如果只有少量的用户,或者很少的更新,或者被覆盖的数据是不太重要的,实现并发冲突可能是得不偿失的。在这种情况下,你不需要配置应用程序以处理并发冲突。

悲观并发(锁定)

如果你的应用程序需要防止由于并发而导致数据意外丢失,你可以使用数据库锁,即所谓的悲观并发。例如,当你从数据库中读取一条记录时,你可以将其锁定为只读或更新状态。如果你将某条记录锁定为更新状态,那么其他用户将无法对该记录再次加锁,无论是读取还是更新操作。如果你将某条记录锁定为只读状态,其他人也可以将其锁定为只读状态,但不能进行更新操作。

管理锁也有缺点,它会导致编程更复杂,它需要大量的数据库管理资源,并且它可能会在用户数量增加时导致性能问题。基于以上种种,并不是所有的数据库管理系统都支持悲观并发。Entity Framework并没有提供内置支持,且本节中不会讨论如何实现它。

乐观并发

悲观并发的替代方案之一就是乐观并发。乐观并发意味着允许发生并发冲突,然后做出适当的反应。例如,John打开Departments Edit页面,将English department的Budget从$350,000.00 修改为$0.00。

在John点击Save之前,一个叫Jane也打开了此页面,并将Start Date修改为 2014/09/01

John首先点击了Save,Index页面显示了被修改的数据,之后Jane也点击了Save,接下来会发生什么取决于你如何处理并发冲突,你可以通过使用下面的方法来处理你的应用:

  • 你可以跟踪用户所修改的属性并只更新数据库中相应的列。在上面的示例中,数据不会丢失,因为两个用户分别更新了两个不同的属性。接下来如果有人查看 English department,他们会看到John和Jane所做的所有修改--2014/09/01的起始日期和$0.00的预算。

    这种更新方式可以减少冲突,但是如果对同一个实体的同一个属性同时进行修改的话可能会导致数据丢失, 是否让Entity Framework 使用这种方式取决于你如何实现更新操作。但在web应用程序中这通常是不太实际的,因为你需要维护大量的状态以便跟踪实体的所有原始属性和修改的值。维护大量的状态会影响应用程序的性能,因为这需要大量的服务器资源或者这些状态必须包含在web页面本身中或者cookie中。

  • 你可以让Jane的更改覆盖John的更改,接下来有人查看English department时会看到2014/09/01的起始日期和$350,000.00的预算,这被称为客户端优先( Client Wins)或后进有效(Last in Wins)策略。(客户端的值会覆盖已保存的数据)。如上所述,如果你没有采取任何措施来处理并发冲突,程序默认使用该方式。
  • 你也可以阻止Jane对数据库进行更新,通常情况下你应该显示一条当前数据状态的错误信息,如果她仍然希望更新这些数据,你应该允许Jane所做的这些操作,这被称为存储优先(Store Wins)策略。接下来会在本节中使用该方式,该方式可以确保在提示用户接下来会发生的事情之前不允许覆盖其他用户所做的修改。

检测并发冲突

你可以通过处理Entity Framework抛出的OptimisticConcurrencyException异常来解决冲突。为了知道何时会抛出这些异常,Entity Framework必须能够检测冲突。因此,你必须对数据库和数据模型进行适当的配置,以下是可以启用冲突检测的方法:

  • 在数据库表中,包含一个跟踪列用于确定该列何时被修改。接下来配置Entity Framework在 SQL Update 或 Delete 命令的Where子句中包含该列。

    跟踪列的数据类型通常是rowversion,rowversion的值是一个在该行每次被更新时都会递增的顺序编号。在Update 或 Delete 命令中,Where子句将包含跟踪列的原始值,如果一个正在更新的行已被另一个用户更改,rowversion列的值会和原来的不一致,因此Update 或 Delete语句由于Where子句而无法找到要更新的行。当Entity Framework发现 Update 或Delete命令没有更新任何行时会将其认定为并发冲突。

  • 配置Entity Framework在Update 或 Delete 命令的Where子句中包含数据库表中每一列的原始值。

    就像第一种方式,如果数据行首次被读取并被修改,Where子句不会返回要更新的行,Entity Framework会将其认定为并发冲突。对于数据库中具有多列的表来说,这种方法可能会产生庞大的Where子句,并要求你维护大量的状态。就像之前提醒过的,维护大量状态可能会影响应用性能。因此该方法一般不推荐使用,在示例中也不会使用。

    如果你确实希望使用该方法来处理并发,你必须要通过添加ConcurrencyCheck属性来标记实体的所有非主键属性。这样可以让Entity Framework在Update语句的Where子句中包含所有的列。

接下来你将会在Department实体中添加一个rowversion跟踪属性,并创建一个控制器和视图,最后验证他们。

向Department实体中添加乐观并发属性

打开Models\Department.cs,添加一个名为RowCersion的跟踪属性

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或Delete命令的Where子句中。该属性被称为Timestamp,因为SQL Server之前的版本使用了SQL timestamp数据类型,之后使用了SQL rowversion来替换它。rowversion的.Net类型是一个字节数组。

如果你更喜欢使用fluent API,你可以使用IsConcurrencyToken方法来指定跟踪属性,如下所示:

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

通过添加属性你已经更改了数据库模型,所以你需要再做一次迁移。打开Package Manager Console (PMC),输入下列命令:

Add-Migration RowVersion
Update-Database

修改Department控制器

打开DepartmentController.cs,添加命名空间:

using System.Data.Entity.Infrastructure;

打开DepartmentController.cs,将所有出现的LastName修改为FullName以便department administrator下列列表包含instructor 的full name而不是last name:

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

修改 HttpPost Edit方法:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(
   [Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, InstructorID")]
       Department department)
{
    try
    {
        if (ModelState.IsValid)
        {
            db.Entry(department).State = EntityState.Modified;
            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.");
            department.RowVersion = databaseValues.RowVersion;
        }
    }
    catch (RetryLimitExceededException /* dex */)
    {
        //Log the error (uncomment dex variable name 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, "ID", "FullName", department.InstructorID);
    return View(department);
}

视图将原始RowVersion值存储至隐藏字段中,模型绑定器创建department实例时,该对象将拥有原始RowVersion属性值和其他属性的新值,比如用户在Edit页面中输入的数据, 然后Entity Framework会生成一个SQL Update命令,该命令包含有一个查找具有RowVersion值的行的Where子句。

如果Update操作没有更新任何行,Entity Framework会抛出DbUpdateConcurrencyException异常,并且catch块中的代码会从异常对象中获取受影响的Department实体。

var entry = ex.Entries.Single();

在该对象的Entity 属性中拥有用户输入的新值,你也可以调用GetDatabaseValues方法从数据库中读取该值。

var clientValues = (Department)entry.Entity;

var databaseEntry = entry.GetDatabaseValues();

如果有人从数据库中删除了该行,那么GetDataBaseValue方法将返回null,否则,你必须将返回的对象转换为Department类以访问Department中的属性。

if (databaseEntry == null)
{
    ModelState.AddModelError(string.Empty,
        "Unable to save changes. The department was deleted by another user.");
}
else
{
    var databaseValues = (Department)databaseEntry.ToObject();

接下来,如果在Edit页面中用户输入的数据与数据库中的数据不一致,上面的代码为这些数据不一致的列添加了自定义错误信息:

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值设置为从数据库检索到的新值,当重新呈现Edit页面时RowVersion的新值被存储在隐藏字段中,下一次用户单击Save时,仅在重新显示Edit页面时捕获发生的并发错误。

打开Views\Department\Edit.cshtml,在DepartmentID属性后添加一个隐藏字段用来保存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)

测试乐观并发处理

运行项目,单击Departments选项卡

打开两个English department Edit页面

将第一个Edit页面中的Budget修改为0,单击Save

Index页面显示了修改后的数据

修改第二个Edit页面中的Start Date

点击Save,可以看到错误信息

再次点击Save,将会覆盖在第一个Edit页面修改的数据

更新Delete页面

对于Delete页面,Entity Framework使用类似与上面的编辑department 时的方式来检测并发冲突。当HttpGet Delete方法显示确认视图时,该视图的隐藏字段中包含了原始的RowVersion值。当用户确认删除时,该值会在调用HttpPost Delete方法时传递给该方法。当Entity Framework创建SQL Delete命令时,该命令的Where子句中将包括原始的RowVersion值。如果该命令没有删除任何行,程序就会抛出并发异常,HttpGet Delete方法会被调用,同时一个错误标志位被设置为true,以便重新重新显示确认页面并显示错误信息。Delete命令没有删除任何行也可能是因为有另一个用户正好也删除了该行,在这种情况下,我们应该显示一个不同的错误信息。

打开DepartmentController.cs,修改HttpGet 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 == true)
        {
            return RedirectToAction("Index");
        }
        return HttpNotFound();
    }

    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 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);
    }
}

由框架自动生成的Delete方法仅接收一个记录ID参数

public async Task<ActionResult> DeleteConfirmed(int id)

修改后的方法接收一个由模型绑定器创建的Department 实体参数,这样可以访问到RowVersion属性

public async Task<ActionResult> Delete(Department department)

你已经将方法名称从DeleteConfirmed修改为Delete,框架代码将HttpPost Delete方法命名为DeleteConfirmed以给予其一个唯一的签名。(CLR需要重载方法具有不同的参数)现在签名是唯一的,你可以遵从MVC的约定,将HttpPost和HttpGet Delete方法使用相同的方法名。

如果捕获到并发错误,该代码将重新显示Delete确认页并提供一个标志位来指明将显示并发错误信息。

打开Views\Department\Delete.cshtml,为DepartmentID 和RowVersion属性添加错误信息字段和隐藏字段,如下所示

@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>

上面的代码在h2和h3标题之间添加了错误信息

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

将Administrator字段的LastName修改为FullName

<dt>
  Administrator
</dt>
<dd>
  @Html.DisplayFor(model => model.Administrator.FullName)
</dd>

在Html.BeginForm语句之后为DepartmentID 和RowVersion属性添加隐藏字段

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

运行项目,点击Department选项卡,为English department 打开一个Edit页面和一个Delete页面

在Edit页面中,修改Budget的值,点击Save

Index页面显示了修改后的值

在Delete页面中点击Delete

可以看到并发错误信息,并显示了数据库中已经被修改的值

如果你再次点击Delete,你会被重定向Index页面,并且该Department已经被删除。

原文:Handling Concurrency with the Entity Framework 6 in an ASP.NET MVC
5 Application

欢迎转载,请注明文章出处:http://blog.csdn.net/johnsonblog/article/details/39298201

还大家一个健康的网络环境,从你我做起

THE END

时间: 2024-11-02 09:47:09

MVC5 Entity Framework学习之处理并发的相关文章

MVC5 Entity Framework学习之实现主要的CRUD功能

在上一篇文章中,我们使用Entity Framework 和SQL Server LocalDB创建了一个MVC应用程序,并使用它来存储和显示数据.在这篇文章中,你将对由 MVC框架自己主动创建的CRUD(create, read, update, delete)代码进行改动. 注意:通常我们在控制器和数据訪问层之间创建一个抽象层来实现仓储模式.为了将注意力聚焦在怎样使用实体框架上.这里暂没有使用仓储模式. 在本篇文章中,要创建的web页面: watermark/2/text/aHR0cDovL

MVC5 Entity Framework学习

MVC5 Entity Framework学习(1):创建Entity Framework数据模型 MVC5 Entity Framework学习(2):实现基本的CRUD功能 MVC5 Entity Framework学习(3):添加排序.筛选和分页功能 MVC5 Entity Framework学习(4):弹性连接和命令拦截 MVC5 Entity Framework学习(5):Code First迁移和部署 MVC5 Entity Framework学习(6):创建复杂的数据模型 MVC5

MVC5 Entity Framework学习之异步和存储过程

在之前的文章中,你已经学习了如何使用同步编程模型来读取和更新数据,在本节中你将学习如何实现异步编程模型.异步可以使应用程序执行更有效率,因为它可以更有效的使用服务器资源. 同样在本节中你还将学习如何针对实体的insert, update, 和delete操作使用存储过程. 最后将应用程序部署到 Windows Azure. 下面是完成后的页面 为什么要使用异步代码 一个web服务器的可用线程是有限的,在高负载情况下,所有的可用线程可能都在被使用.当出现这种情况时,服务器将无法处理新的请求,直到有

MVC5 Entity Framework学习之更新相关数据

在上篇文章中学习了如何在页面中显示相关数据,本节中将学习如何对相关数据进行更新.对于大多数实体关系,可以通过更新外键或导航属性来更新数据,对于多对多关系,Entity Framework不会直接公开连接表,所以你需要通过相应的导航属性来添加和移除实体. 先看完成后的效果图 为Courses自定义Create 和Edit 页面 当一个新的course实体被创建时,该实体必须关联到一个已存在的department.要做到这一点,生成的框架代码应该要包括控制器方法和用于选择department的下列列

MVC5 Entity Framework学习之读取相关数据

前一篇文章中完成了School 数据模型,接下来你将学习如何读取和显示相关的数据--这里指Entity Framework加载至导航属性中的数据. 下图是完成后的效果图 延迟.预先和显示加载相关数据 Entity Framework可以通过多种方法向实体的导航属性中加载数据 延迟加载(Lazy loading) 当实体第一次被读取时,相关数据并不会被检索.但是,当你第一次访问导航属性时,该导航属性所需的数据会自动加载.这是向数据库发送多个查询语句的结果--一次是读取实体本身,接着是每次与被检索的

MVC5 Entity Framework学习之实现继承

之前你已经学习了如何处理并发异常,在本节中你将学习如何实现继承. 在面向对象的编程中,你可以使用继承来重用代码.接下来你将修改Instructor和Student类,让它们派生自Person基类,该基类包含instructor和student共有的属性如LastName.你不需要添加或修改任何WEB页面,但是你需要修改某些代码,这些修改会自动反映在数据库中. 映射继承到数据库的选项 School 数据模型中的Instructor和Student类有几个相同的属性: 假设你希望通过共享Instru

MVC5 Entity Framework学习之弹性连接和命令拦截

到目前为止,应用程序一直在本地IIS Express上运行.如果你想让别人通过互联网访问你的应用程序,你必须将它部署到WEB服务器同时将数据库部署到数据库服务器 本篇文章中将教你如何使用在将你的应用程序部署到云环境时的Entity Framework 6的非常有价值的两个特性:弹性连接(瞬时错误的自动重试)和命令拦截(捕获所有发送到数据库的SQL查询语句并记录至日志中). 1.启用弹性连接 当你将应用程序部署到Windows Azure时,相应的数据库部也应被部署到Windows Azure S

MVC5 Entity Framework学习之Entity Framework高级功能

在之前的文章中,你已经学习了如何实现每个层次结构一个表继承.本节中你将学习使用Entity Framework Code First来开发ASP.NET web应用程序时可以利用的高级功能. 在本节中你将重用之前已经创建的页面,接下来你需要新建一个页面并使用原始SQL来批量更新数据库中所有Course的学分. 在Department Edit页面中添加新的验证逻辑并使用非跟踪查询. 执行原始SQL查询 Entity FrameworkCode First API包含有可以让你直接向数据库发送SQ

MVC5 Entity Framework学习之实现基本的CRUD功能

在上一篇文章中,我们使用Entity Framework 和SQL Server LocalDB创建了一个MVC应用程序,并使用它来存储和显示数据.在这篇文章中,你将对由 MVC框架自动创建的CRUD(create, read, update, delete)代码进行修改. 注意:通常我们在控制器和数据访问层之间创建一个抽象层来实现仓储模式,为了将注意力聚焦在如何使用实体框架上,这里暂没有使用仓储模式. 在本篇文章中,要创建的web页面: 1.创建一个Details页面 由框架代码生成的Stud