EF学习笔记(十) 处理并发

总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理)html

上一篇:EF学习笔记(九):异步处理和存储过程sql

本篇原文连接:Handling Concurrency数据库

Concurrency Conflicts 并发冲突

发生并发冲突很简单,一个用户点开一条数据进行编辑,另一个用户同时也点开这条数据进行编辑,那么若是不处理并发的话,谁后提交修改保存的,谁的数据就会被记录,而前一个就被覆盖了;数组

若是在一些特定的应用中,这种并发冲突能够被接受的话,那么就不用花力气去特地处理并发;毕竟处理并发确定会对性能有所影响。服务器

Pessimistic Concurrency (Locking) 保守并发处理(锁)

若是应用须要预防在并发过程当中数据丢失,那么一种方式就是采用数据库锁;这种方式称为保守并发处理。并发

这种就是原有的处理方式,要修改数据前,先给数据库表或者行加上锁,而后在这个事务处理完以前,不会释放这个锁,等处理完了再释放这个锁。mvc

但这种方式应该是对一些特殊数据登记才会使用的,好比取流水号,多个用户都在取流水号,用一个表来登记当前流水号,那么取流水号过程确定要锁住表,否则同时两个用户取到同样的流水号就出异常了。app

并且有的数据库都没有提供这种处理机制。EF并无提供这种方式的处理,因此本篇就不会讲这种处理方式。异步

Optimistic Concurrency 开放式并发处理

替代保守并发处理的方式就是开放式并发处理,开放式并发处理运行并发冲突发生,可是由用户选择适当的方式来继续;(是继续保存数据仍是取消)async

好比在出现如下状况:John打开网页编辑一个Department,修改预算为0, 而在点保存以前,Jone也打开网页编辑这个Department,把开始日期作了调整,而后John先点了保存,Jone以后点了保存;

在这种状况下,有如下几种选择:

一、跟踪用户具体修改了哪一个属性,只对属性进行更新;当时也会出现,两个用户同时修改一个属性的问题;EF是否实现这种,须要看本身怎么写更新部分的代码;在Web应用中,这种方式不是很合适,须要保持大量状态数据,维护大量状态数据会影响程序性能,由于状态数据要么须要服务器资源,要么须要包含在页面自己(隐藏字段)或Cookie中;

二、若是不作任何并发处理,那么后保存的就直接覆盖前一个保存的数据,叫作: Client Wins or Last in Wins

三、最后一种就是,在后一我的点保存的时候,提示相应错误,告知其当前数据的状态,由其确认是否继续进行数据更新,这叫作:Store Wins(数据存储值优先于客户端提交的值),此方法确保没有在没有通知用户正在发生的更改的状况下覆盖任何更改。

Detecting Concurrency Conflicts 检测并发冲突

要想经过解决EF抛出的OptimisticConcurrencyException来处理并发冲突,必须先知道何时会抛出这个异常,EF必须可以检测到冲突。所以必须对数据模型进行适当的调整。

有两种选择:

一、在数据库表中增长一列用来记录何时这行记录被更新的,而后就能够配置EF的Update或者Delete命令中的Where部分把这列加上;

通常这个跟踪记录列的类型为 rowversion ,通常是一个连续增加的值。在Update或者Delete命令中的Where部分包含上该列的本来值;

若是原有记录被其余人更新,那么这个值就会变化,那么Update或者Delete命令就会找不到本来数据行;这个时候,EF就会认为出现了并发冲突。

二、经过配置EF,在全部的Update或者Delete命令中的Where部分把全部数据列都包含上;和第1种方式同样,若是其中有一列数据被其余人改变了,那么Update或者Delete命令就不会找到本来数据行,这个时候,EF就会认为出现了并发冲突。

这个方式惟一问题就是where后面要拖很长很长的尾巴,并且之前版本中,若是where后面太长会引起性能问题,因此这个方式不被推荐,后面也不会再讲。

 若是肯定要采用这个方案,则必须为每个非主键的Properites都加上ConcurrencyCheck属性定义,这个会让EF的update的WHERE加上全部的列;

Add an Optimistic Concurrency Property to the Department Entity

给Modles/Department 加上一个跟踪属性: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或者Delete的时候必定要加在Where语句里;

叫作Timestamp的缘由是SQL Server之前的版本使用timestamp 数据类型,后来用SQL rowversion取代了 timestamp 。

在.NET里 rowversion 类型为byte数组。

固然,若是喜欢用fluent API,你能够用IsConcurrencyToken方法来定义一个跟踪列:

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

记得变动属性后,要更新数据库,在PMC中进行数据库更新:

Add-Migration RowVersion
Update-Database

 修改Department 控制器

先增长一个声明:

using System.Data.Entity.Infrastructure;

而后把控制器里4个事件里的SelectList里的 LastName 改成 FullName ,这样下拉选择框里就看到的是全名;显示全名比仅仅显示Last Name要友好一些。

下面就是对Edit作大的调整:

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

能够看到,修改主要分为如下几个部分:
一、先经过ID查询一下数据库,若是不存在了,则直接提示错误,已经被其余用户删除了;

二、经过 db.Entry(departmentToUpdate).OriginalValues["RowVersion"] = rowVersion; 这个语句把原版本号给赋值进来;

三、EF在执行SaveChange的时候自动生成的Update语句会在where后面加上版本号的部分,若是语句执行结果没有影响到任何数据行,则说明出现了并发冲突;EF会自动抛出DbUpdateConcurrencyException异常,在这个异常里进行处理显示已被更新过的数据,好比告知用户那个属性字段被其余用户变动了,变动后的值是多少;

    var clientValues = (Department)entry.Entity;    //取的是客户端传进来的值
            var databaseEntry = entry.GetDatabaseValues();  //取的是数据库里现有的值 ,若是取来又是null,则表示已被其余用户删除

这里有人会以为,不是已经在前面处理过被删除的状况,这里又加上出现null的状况处理,是否是多余,应该是考虑其余异步操做的问题,就是在第1次异步查询到最后SaveChange之间也可能被删除。。。(我的以为第1次异步查询有点多余。。也许是为了性能考虑吧)

最后就是写一堆提示信息给用户,告诉用户哪一个值已经给其余用户更新了,是否还继续确认本次操做等等。

对于Edit的视图也须要更新一下,加上版本号这个隐藏字段:

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

最后测试一下效果:

打开2个网页,同时编辑一个Department:

第一个网页先改预算为 0 ,而后点保存;

第2个网页改日期为新的日期,而后点保存,就出现如下状况:

这个时候若是继续点Save ,则会用最后一次数据更新到数据库:

突然又有个想法,若是在第2次点Save以前,又有人更新了这个数据呢?会怎么样?

打开2个网页,分别都编辑一个Department ;

而后第1个网页把预算变动为 0 ;点保存;

第2个网页把时间调整下,点保存,这时候提示错误,不点Save ;

在第1个网页里,再编辑该Department ,把预算变动为 1 ,点保存;

回到第2个网页,点Save , 这时 EF会自动再次提示错误

 

下面对Delete 处理进行调整,要求同样,就是删除的时候要检查是否是原数据,有没有被其余用户变动过,若是变动过,则提示用户,并等待用户是否确认继续删除;

把Delete Get请求修改一下,适应两种状况,一种就是有错误的状况:

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

把Delete Post请求修改下,在删除过程当中,处理并发冲突异常:

[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的视图,把错误信息显示给用户,而且在视图里加上DepartmentID和当前数据版本号的隐藏字段:

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

最后看看效果:

打开2个网页进入Department Index页面,第1个页面点击一个Department的Edit ,第2个页面点击该 Department的Delete;

而后第一个页面把预算改成100,点击Save.

第2个页面点击Delete 确认删除,会提示错误:

相关文章
相关标签/搜索