[渣譯文] 使用 MVC 5 的 EF6 Code First 入門 系列:爲ASP.NET MVC應用程序處理併發

這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這裏是第十篇:爲ASP.NET MVC應用程序處理併發html

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

譯文版權全部,謝絕全文轉載——但您能夠在您的網站上添加到該教程的連接。web

在以前的教程中,您已經學習瞭如何更新數據。在本節教程中將展現當多個用戶在同一時間更新同一實體時如何處理衝突。sql

你將修改web頁面來處理Department實體,使得它們可以處理併發錯誤。下面的截圖顯示了索引和刪除頁面,以及一些併發衝突的錯誤消息。數據庫

 

併發衝突

當一個用戶顯示實體的數據並對其進行編輯,而後另外一個用戶在第一個用戶的更改寫入到數據庫以前更新同一實體的數據,將發生併發衝突。若是您不啓用這種衝突的檢測,最後一次更新數據庫的用戶將覆蓋其餘用戶對數據庫所作的更改。在大部分應用程序中,這種風險是能夠接收的:若是僅有幾個用戶或不多更新,或者數據更新覆蓋的問題真的不是很重要,實現併發衝突的開銷可能會大於它帶來的益處。在這種狀況下,您不須要配置應用程序以處理併發衝突。編程

悲觀併發(鎖定)

若是您的應用程序須要防止併發帶來的意外數據丟失,要作到這一點的一個方法是使用數據庫鎖。即所謂的悲觀併發。例如,您從數據庫讀取行以前,先請求一個只讀或更新的訪問鎖。若是你鎖定了某行的更新訪問,沒有其餘用戶能夠給該行加鎖,不管是隻讀或是更新。由於他們獲得的數據只是變動過程當中的一個副本。若是你鎖定了某行的只讀訪問,其餘人也能夠將其鎖定爲只讀訪問,但不能進行更新。數組

管理鎖也有缺點。它會使編程更復雜。而且它須要數據庫的管理資源——大量的,以及它可能致使性能的問題好比應用程序的用戶數量增長。出於這些緣由,並非全部的數據庫管理系統都支持悲觀併發。實體框架內置了悲觀併發的支持,單本教程中不會討論如何實現它。瀏覽器

樂觀併發

悲觀併發的替代方案就是樂觀併發。樂觀併發意味着運行併發衝突發生,而後對發生的變化作出適當的反應。例如,路人甲在系編輯頁面,更改天然科學的預算從50更改成50000。服務器

在路人甲保存該更改以前,路人乙也一樣打開了該頁面,並更改起始日期字段到2012-12-12。併發

路人甲首先點擊保存,他在索引頁面上看到了他所作的修改,以後路人乙也點擊了保存。下一步會發生什麼取決於你如何處理併發衝突,下面列出了一些選擇:

  • 你能夠跟蹤用戶已修改的屬性並僅更新數據庫中的相應列。在示例中,沒有數據會丟失,由於兩個不一樣的屬性分別由兩個不一樣的用戶更新。路人丙此時瀏覽頁面會同時看到甲和乙所分別作出的變化——2012年的起始日期和0元的預算。
    這種更新的方法能夠減小衝突,但仍可能會致使數據丟失——若是對同一屬性進行更改的話。是否採用這種方式來讓實體框架工做取決於您如何實現您的更新代碼。在實際的web應用程序中這每每不是最佳作法。由於它會要求保持大量的狀態以便跟蹤實體的全部原始屬性和新值。維護大量的狀態會影響應用程序性能。由於這須要更多的服務器資源。
  • 您可讓乙的更改覆蓋甲的更改,在丙瀏覽頁面時,他會看到2012年的起始日期和還原的50元預算。這被稱爲客戶端通吃或最後一名通吃。(來自客戶端的值優先於先保存的值,覆蓋所有數據)。下面的截圖演示了這種狀況:



  • 您也能夠阻止乙的更改保存到數據庫。一般狀況下會顯示一條錯誤信息,顯示被覆蓋的數據之間有何不一樣來容許用戶從新提交更改——若是用戶想要這樣作的話。這被稱爲存儲通吃。(已經保存的值優先於客戶端提交的值)你會在本教程中實現該方案,以確保在提示用戶以前不會覆蓋其它用戶的更改。

檢測併發衝突

您能夠經過實體框架引起的OptimisticConcurrencyException異常處理來解決衝突。爲了知道什麼時候何地會引起這些異常,實體框架必須可以檢測到衝突。所以,你必須對數據庫和數據模型進行適當的配置,包括如下內容:

  • 在數據表中,包含用於跟蹤修改的列。而後,您能夠配置實體框架在更新或刪除的時候包含該列來進行檢測。
    跟蹤列的數據類型一般是rowversion。行版本的值是一個每次在更新時都會遞增的順序編號。在更新或刪除命令中,Where字句將包含跟蹤列的原始值。若是有另外一個用戶更改了正在更新的行,行版本中的值會和原來的不一致。所以更新和刪除語句沒法找到要更新的行。當在更新或刪除時沒有行被更新時,實體框架將認定該命令爲併發衝突。
  • 配置實體框架能夠在更新和刪除命令的Where子句中包含數據表每一個列的原始值。
    和第一個方式相似,若是數據行被首次讀取後發生了更改,Where字句不會找到要更新的行,實體框將解釋爲併發衝突。對於有多列的數據庫表,這種方法可能會致使很是龐大的Where字句,並要求你保持大量的狀態。如前所述,保持大量狀態可能會影響應用程序的性能。所以該方法通常並不推薦,本教程中也不會使用。
    若是您想要執行這種方法來實現併發,你必須將ConcurrencyCheck特性添加到實體全部的非主鍵屬性上。這種變化使實體框架能夠將全部標記的列包含到更新語句的Where子句中。

在本教程的剩餘部分,你會添加行版本用來跟蹤Department實體的屬性。

將樂觀併發的所需屬性添加到Department實體

在Models\Department.cs中,添加一個名爲RowCersion的跟蹤屬性:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    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 = "起始日期")]
        public DateTime StartDate { get; set; }

        [Display(Name = "系主任")]
        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特性指定該列將會包含在發送到數據庫的更新或刪除命令的Where子句中。該屬性被稱爲時間戳,由於以前版本的SQL Server使用SQL Timestamp數據類型。行版本的.Net類型是一個字節數組。

若是您更喜歡使用fluent API,您可使用IsConcurrencyToken方法來指定跟蹤屬性,以下面的示例:

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

 

如今您已經更改了數據庫模型,因此您須要再作一次遷移。在軟件包管理器控制檯中,輸入如下命令:

Add-Migration RowVersion 
Update-Database

 

修改Department控制器

在DepartmentController.cs中,添加using語句:

using System.Data.Entity.Infrastructure;

 

將文件中全部的"LastName"更改成"FullName"以便下拉列表使用教師的全名,而不是姓。

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

 

使用下面的代碼替換HttpPost的Edit方法:

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Edit([Bind(Include = "DepartmentID,Name,Budget,StartDate,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, "沒法保存更改,系已經被其餘用戶刪除。");
                }
                else
                {
                    var databaseValues = (Department)databaseEntry.ToObject();
                    if (databaseValues.Name != clientValues.Name)
                        ModelState.AddModelError("Name", "當前值: "
                            + databaseValues.Name);
                    if (databaseValues.Budget != clientValues.Budget)
                        ModelState.AddModelError("Budget", "當前值: "
                            + String.Format("{0:c}", databaseValues.Budget));
                    if (databaseValues.StartDate != clientValues.StartDate)
                        ModelState.AddModelError("StartDate", "當前值: "
                            + String.Format("{0:d}", databaseValues.StartDate));
                    if (databaseValues.InstructorID != clientValues.InstructorID)
                        ModelState.AddModelError("InstructorID", "當前值: "
                            + db.Instructors.Find(databaseValues.InstructorID).FullName);
                    ModelState.AddModelError(string.Empty, "當前記錄已經被其餘人更改。若是你仍然想要保存這些數據,"
                    + "從新點擊保存按鈕或者點擊返回列表撤銷本次操做。");
                    department.RowVersion = databaseValues.RowVersion;
                }
            }
            catch (RetryLimitExceededException)
            {
                ModelState.AddModelError(string.Empty, "沒法保存更改,請重試或聯繫管理員。");
            }
            ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", department.InstructorID);
            return View(department);
        }

 

視圖在隱藏字段中存儲原始的RowVersion值。 當模型綁定器建立系的實例,對象將有原始的RowVersion屬性值及其餘屬性的新值,好比在編輯頁面上輸入的用戶。而後實體框架建立一個更新命令,命令將在Where子句中包括RowVersion值來進行查詢。

若是沒有任何行被更新(沒有找到匹配原始RowVersion值的行),實體框架將引起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, "沒法保存更改,系已經被其餘用戶刪除。");
}
else
{
    var databaseValues = (Department)databaseEntry.ToObject();                 

 

下一步,代碼將添加每一列數據庫和用戶輸入不一樣值的自定義錯誤消息:

if (databaseValues.Name != clientValues.Name)
      ModelState.AddModelError("Name", "當前值: " + databaseValues.Name);

 

一個較長的錯誤消息向用戶解釋發生了什麼事情:

ModelState.AddModelError(string.Empty, "當前記錄已經被其餘人更改。若是你仍然想要保存這些數據,"
+ "從新點擊保存按鈕或者點擊返回列表撤銷本次操做。");                

 

最後,代碼將Department對象的RowVersion值設置爲從數據庫檢索到的新值。新的值在從新顯示編輯頁面時被存儲在隱藏字段。下一次用戶單擊保存時,從新顯示的編輯頁面會繼續捕獲併發錯誤。

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

 

測試樂觀併發處理

運行應用程序,單擊系選項卡並複製一個選項卡,重複打開兩個系頁面。

同時在兩個窗口中打開同一系的編輯頁面,編輯其中的一個頁面並保存。

你會看到值已經被保存到數據庫中。

修改第二個窗口中的字段並保存。

你會看到併發錯誤的消息:

再次單擊保存,你在第二個瀏覽器中數據庫值會覆蓋掉第一個窗口中的保存到數據庫中。

更新刪除頁

對於刪除頁面,實體框架使用相似的方式來檢測併發衝突。當HttpGet的Delete方法顯示確認視圖時,視圖的隱藏字段中包括了原始RowVersion值。當用戶確認刪除時,該值在HttpPost的Delete方法中就夠被傳遞並調用。當實體框架建立SQL Delete命令時,Where子句中將包括原始的RowVersion值。若是命令執行後沒有行受到影響,就會引起併發異常。HttpGet的Delete方法被調用,標誌位將被設置爲true以從新顯示確認頁並顯示錯誤。但同時要考慮若是有另外一個用戶正好也刪除了該行,一樣會致使一個0行受影響的結果。在這種狀況下,咱們將顯示一個不一樣的錯誤消息。

在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 = "你想要刪除的記錄"
                        + "已經被另外一個用戶刪除了,點擊列表超連接返回。";
                }
                else
                {
                    ViewBag.ConcurrencyErrorMessage = "你想要刪除的記錄"
                        + "被另外一個用戶修改了原始值,若是您仍然想要刪除該條記錄"
                        + "再次點擊刪除按鈕,或者點擊列表超連接返回。";
                }
            }
            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)
            {
                ModelState.AddModelError(string.Empty, "沒法刪除,請重試或聯繫管理員。");
                return View(department);
            }
        }

 

在您還沒有修改的腳手架代碼中,該方法接收一個記錄ID

        public async Task<ActionResult> DeleteConfirmed(int id)

您更改了此參數,使用模型綁定器來建立一個Department實體,這使您能夠訪問到RowVersion屬性值。

        public async Task<ActionResult> Delete(Department department)

 

同時您修改了方法名稱從DeleteConfirmed到Delete。腳手架代碼爲HttpPost的Delete方法使用了Delete的名稱,由於這樣可以給HttpPost方法一個惟一的簽名。(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>
<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>

 

使用FullName替換了LastName:

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

 

最後,它在Html.BeginForm語句以後添加了隱藏字段:

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

 

運行應用程序,打開系索引頁面,右鍵點擊天然科學的刪除超連接,選擇在新窗口中打開。而後在第一個窗口上點擊編輯,修改預算並保存。

更改已經保存到數據庫。

點擊第二個窗口中的刪除按鈕,會看到一個併發錯誤信息。

若是此時再次點擊刪除,實體將被刪除,你會被重定向到索引頁面。

 

總結

在本節中咱們介紹瞭如何處理併發衝突。關於更多處理併發衝突的信息,請參閱MSDN上的和。下一節中咱們將介紹如何實現Instructor和Student實體的表-每一個層次繼承。

做者信息

 

  Tom Dykstra - Tom Dykstra是微軟Web平臺及工具團隊的高級程序員,做家。

相關文章
相關標籤/搜索