Contoso 大學 - 7 – 處理併發

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

全文目錄: Contoso 大學 - 使用 EF Code First 建立 MVC 應用

在上一次的教程中咱們處理了關聯數據問題。這個教程演示如何處理併發問題。你將使用 Department 實體建立一個頁面,這個頁面在支持編輯和刪除的同時,還能夠處理併發錯誤。下面的截圖演示了 Index 頁面和 Delete 頁面,包括在出現併發衝突的時候提示的一些信息。

 

 

7-1 併發衝突

 

併發衝突出如今這樣的時候,一個用戶正在顯示並編輯一個實體,可是在這個用戶將修改保存到數據庫以前,另外的一個用戶卻更新了一樣的實體。若是你沒有經過 EF 檢測相似的衝突,最後一個更新數據的用戶將會覆蓋其餘用戶的修改。在一些程序中,這樣的風險是能夠接受的,若是隻有不多的用戶,或者不多的更新,甚至對數據的覆蓋不是真的很關鍵,或者解決併發的代價超過了支持併發所帶來的優點。在這種狀況下,你就不須要讓你的程序支持併發衝突的處理。javascript

 

7-1-1 悲觀併發 ( 鎖定 )

 

若是你的應用須要在併發環境下防止偶然的數據丟失,一種方式是經過數據庫的鎖來實現。這種方式被稱爲悲觀併發。例如,在從數據庫中讀取一行數據以前,能夠申請一個只讀鎖,或者一個更新訪問鎖。若是你對數據行使用了更新訪問鎖,就沒有其餘的用戶能夠獲取不論是隻讀鎖仍是更新訪問鎖,由於他們可能獲取正在被修改中的數據。若是你使用只讀鎖來鎖定一行,其餘用戶也可使用只讀訪問,可是不能進行更新。html

 

管理鎖有一些缺點,對程序來講可能很複雜。它須要重要的數據庫管理資源,對於大量用戶的時候可能致使性能問題 ( 擴展性很差 ),因爲這些緣由,不是全部的數據庫管理系統都支持悲觀鎖。EF 對悲觀鎖沒有提供內建的支持,這個教程也不會演示如何實現它。java

 

7-1-2 樂觀併發

除了悲觀併發以外的另外一方案是樂觀併發。樂觀併發意味着容許併發衝突發生,若是出現了就作出適當的反應。例如,John 執行 Department 的編輯頁面,將 English 系的 Budget $350,000.00 修改成 $100,000.00 ( John 管理與 English 有競爭的系,但願將一些資金轉移到他本身的系使用 )數據庫

 

John 點擊保存 Save 以前,Jane 運行一樣的頁面,將開始時間 Start Date 字段從 9/1/2007 修改成 1/1/1999 ( Jane 管理歷史系,但願它的歷史更加悠久 )瀏覽器

 

 

John 先點擊保存 Save,而後在回到 Index 頁面的時候看到本身的修改。而後 Jane 點擊保存 Save。下一步發生什麼取決於如何處理併發衝突。可能的狀況以下:服務器

  •  你能夠追蹤用戶修改和更新了哪些數據庫中的列。在這個例子的場景下,不會丟失數據,由於兩個用戶更新了不一樣的屬性。下一次其餘人在瀏覽英語系的時候,他們會發現 John Jane 所作的全部修改:開始時間成爲 1/1/1999,預算成爲 $100,000.00

 這種方法能夠減小可能形成數據丟失的衝突次數,可是若是用戶修改同一個實體的相同屬性的話,會丟失數據, EF 具體依賴於你如何實現你的更新代碼。這種方式不適合 Web 應用程序,由於須要你維護大量的狀態,以便追蹤全部新值的原始狀態。維護大量的狀態會影響到程序的性能,由於既須要服務器的資源,又須要將狀態保存在頁面中 ( 例如,使用隱藏域 )併發

  •  你能夠容許 Jane 的修改覆蓋 John 的修改。下一次用戶瀏覽英語系的時候,將會看到 1/1/1999 和恢復的 $350,000.00 值。這被稱爲 Client Wins 或者 Last in Wins 場景 ( 客戶端的值優先於保存的值 )。像在這節開始介紹的,若是你沒有使用任何代碼處理併發,這將會自動發生。
  • 你能夠阻止 Jane 的修改更新到數據庫中。一般狀況下,咱們但願顯式一個錯誤信息。展現數據當前的狀態,若是她仍然但願作出這樣修改的話,容許她重作修改。這被稱爲 Store Wins 場景。( 保存的值優先於客戶提交的值 ) 在這個教程中,你將要實現 Store Wins 場景。這種方法在提示用戶發生什麼以前,不會覆蓋其餘用戶的修改。

7-1-3 檢測併發衝突

 

你能夠經過處理 EF 拋出的 OptimisticConcurrencyException 異常來處理衝突。爲了知道何時 EF 拋出了這種異常,EF 必須可以檢測衝突。所以,你必須合理配置數據庫和數據模型。啓用衝突檢測的一些選項以下:mvc

  • 在數據庫的表中,包含用於追蹤修改的列,在行被修改的時候能夠用來進行檢測。而後配置 EF 在更新 Update 或者刪除 Delete Where 子句中包含檢測列。用於追蹤的列的數據類型一般是 timestamp,可是其中並不真的包含實際的日期或者時間值。相反,值是在行每次更新的時候的一個遞增值( 所以,在最近的 SQL Server 中,一樣的類型被稱爲行版本 rowversion ) 。在更新 Update 或者 Delete 命令中,Where 子句中包含跟蹤列的原始值。若是行被其餘用戶更新了,那麼,此時跟蹤列中的值就會與原始值不一樣,因爲 Where 子句的做用,Update 或者 Delete 語句就不會取得須要更新的行。當 EF 發現沒有行被 Update 或者 Delete 命令更新的時候 ( 就是說,影響的行數爲 0 ),就理解爲發生了併發衝突。
  •     配置 EF Update 或者 Delete 語句的 Where 中包含全部的原始列。如同第一個方式,若是在數據行被讀取以後,行發生了任何修改,Where 將不能取得須要更新的行,這樣 EF 就理解爲發生了併發衝突。這種方式像使用跟蹤列同樣有效。可是,若是數據庫中的表有不少列,就會致使巨大的 Where 子句,你也必須維護大量的狀態。如前所述,維護大量的狀態會影響程序的性能,由於既須要消耗服務器資源,也須要在頁面中包含狀態。所以,不建議使用這種方式,在這個教程中也不使用這種方法。

在本教程剩下的部分,你須要在 Department 實體上增長一個追蹤列,建立控制器和視圖,而後檢查一切是否工做正常。app

 

注意:若是你沒有使用追蹤列來實現併發,你就必須經過使用 ConcurrencyCheck 特性標記全部的非主屬性用在併發跟蹤中。這將會使 EF 將全部的列包含在 Update 語句的 Where 子句中。asp.net

 

7-2  Department 實體增長跟蹤屬性

Models\Departments.cs 文件中,增長跟蹤屬性。

[Timestamp]
public Byte[] Timestamp { get; set; }

 

Timestamp 特性指定隨後的列將會被包含在 Update 或者 Delete 語句的 Where 子句中。

 

 

 

7-3 建立 Department 控制器

 

如同建立其餘的控制器同樣,建立 Department 控制器和視圖,使用以下的設置。

 

Controllers\DepartmentController.cs 中,增長一個 using 語句。

 

using System.Data.Entity.Infrastructure;

 

將文件中全部的 「LastName」 修改成 「FullName」 ( 共有 4 ),使得系控制器中的下拉列表使用教師的全名而不是名字。

 

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

 

 

複製代碼
[HttpPost]
public ActionResult Edit(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 databaseValues = (Department)entry.GetDatabaseValues().ToObject();
        var clientValues = (Department)entry.Entity;
        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.Timestamp = databaseValues.Timestamp;
    }
    catch (DataException)
    {
        //Log the error (add a variable name after Exception)
        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);
}
複製代碼

 

 

視圖經過頁面中的隱藏域保存原始的時間戳。當編輯頁面提交到服務器的時候,經過模型綁定建立 Department 實例的時候,實例將會擁有原始的 Timestamp 屬性值,其餘的屬性獲取新值。而後,當 EF 建立 Update 命令時,命令中將包含查詢包含原始 Timestamp 值的 Where 子句。

 

在執行 Update 語句以後,若是沒有行被更新,EF 將會拋出 DbUpdateConcurrencyException 異常,代碼中的 catch 塊從異常對象中獲取受影響的 Department 實體對象,實體中既有從數據庫中讀取的值,也有用戶新輸入的值。

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

 

而後,代碼爲用戶在編輯頁面上每個輸入的值與數據庫中的值不一樣的列添加自定義的錯誤信息。

 

 

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 Timestamp 屬性值設置爲數據庫中新獲取的值。新的 Timestamp 值被保存在從新顯示頁面的隱藏域中,下一次用戶點擊保存的時候,當前顯示的編輯頁面值會被從新獲取,這樣就能夠處理新的併發錯誤。

 

Views\Department\Edit.cshtml 中,增長一個隱藏域來保存 Timestamp 屬性值,緊跟在 DepartmentID 屬性以後。

 

@Html.HiddenFor(model => model.Timestamp)

 

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>
複製代碼

 

7-4 測試樂觀併發處理

 

運行程序,點擊 Departments.

 

 

點擊 Edit 超級連接,而後再打開一個新的瀏覽器窗口,窗口中使用相同的地址顯示相同的信息。

 

 

在第一個瀏覽器的窗口中修改一個字段的內容,而後點擊 Save

 

 

瀏覽器回到 Index 頁面顯示修改以後的值。

 

 

在第二個瀏覽器窗口中將一樣的字段修改成不一樣的值,

 

 

在第二個瀏覽器窗口中,點擊 Save,將會看到以下錯誤信息。

 

 

再次點擊 Save。在第二個瀏覽器窗口中輸入的值被保存到數據庫中,在 Index 頁面顯示的時候出如今頁面上。

 

 

7-5 增長刪除頁面

 

對於刪除頁面,EF 使用相似的方式檢測併發衝突。當 HttpGet Delete 方法顯示確認頁面的時候,視圖在隱藏域中包含原始的 Timestamp 值,當用戶確認刪除的時候,這個值被傳遞給 HttpPost Delete 方法,當 EF 建立 Delete 命令的時候,在 Where 子句中包含使用原始 Timestamp 值的條件,若是命令影響了 0 ( 意味着在顯示刪除確認頁面以後被修改了 ),併發異常被拋出,經過傳遞錯誤標誌爲 true HttpGet Delete 方法被調用,帶有錯誤提示信息的刪除確認頁面被顯示出來。

 

DepartmentController.cs 中,使用以下代碼替換 HttpGet Delete 方法。

 

複製代碼
public ActionResult Delete(int id, bool? concurrencyError)
{
    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.";
    }

    Department department = db.Departments.Find(id);
    return View(department);
}
複製代碼

 

方法接收一個可選的表示是不是在併發衝突以後從新顯示頁面的參數,若是這個標誌爲 true,錯誤信息經過 ViewBag 傳遞到視圖中。

 

使用下面的代碼替換 HttpPost Delete 方法中的代碼 ( 方法名爲 DeleteConfirmed )

 

複製代碼
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(Department department)
{
    try
    {
        db.Entry(department).State = EntityState.Deleted;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    catch (DbUpdateConcurrencyException)
    {
        return RedirectToAction("Delete",
            new System.Web.Routing.RouteValueDictionary { { "concurrencyError", true } });
    }
    catch (DataException)
    {
        //Log the error (add a variable name after Exception)
        ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
        return View(department);
    }
}
複製代碼

 

你剛剛替換的腳手架代碼方法僅僅接收一個記錄的 Id

 public ActionResult DeleteConfirmed(int id)

 

將這個參數替換爲經過模型綁定建立的 Department 實體實例。這使得能夠訪問額外的 Timestamp 屬性。

 

public ActionResult DeleteConfirmed(Department department)

 

若是發生了併發衝突,代碼將會傳遞表示應該顯示錯誤的標誌給確認頁面,而後從新顯示確認頁面。

 

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

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

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

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

 

 

代碼中在 h2 h3 之間增長了錯誤信息。

 

 

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

 

 

Administrator 區域將 LastName 替換爲 FullName

 

 

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

 

 

最後,增長了用於 DepartmentId Timestamp 屬性的隱藏域,在 Html.BeginForm 語句以後。

 

 

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

 

 

運行 Departments Index 頁面,使用一樣的 URL 打開第二個瀏覽器窗口。

 

在第一個窗口中,在某個繫上點擊 Edit ,而後修改一個值,先不要點擊 Save

 

 

在第二個窗口中,在一樣的繫上,選擇 Delete ,刪除確認窗口出現了。

 

 

在第一個窗口中,點擊 Save,在 Index 頁面中確認修改信息。

 

 

如今,在第二個瀏覽器窗口中點擊 Delete,你將會看到併發錯誤信息,其中 Department的名稱已經使用當前數據庫中的值刷新了。

 

 

 

若是再次點擊 Delete,你將會被重定向到 Index 頁面,在顯示中 Department 已經被刪除了。

 

這裏完整地介紹了處理併發衝突。對於處理併發衝突的其餘場景,能夠在 EF 團隊的博客上查閱Optimistic Concurrency PatternsWorking with Property Values。下一次教程將會演示針對教師 Instructor 和學生 Student 實體的表層次的繼承。

相關文章
相關標籤/搜索