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 確認刪除,會提示錯誤:

相關文章
相關標籤/搜索