原文 Contoso 大學 - 7 – 處理併發html
By Tom Dykstra, Tom Dykstra is a Senior Programming Writer on Microsoft's Web Platform & Tools Content Team.
原文地址: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 頁面,包括在出現併發衝突的時候提示的一些信息。web
併發衝突出如今這樣的時候,一個用戶正在顯示並編輯一個實體,可是在這個用戶將修改保存到數據庫以前,另外的一個用戶卻更新了一樣的實體。若是你沒有經過 EF 檢 測相似的衝突,最後一個更新數據的用戶將會覆蓋其餘用戶的修改。在一些程序中,這樣的風險是能夠接受的,若是隻有不多的用戶,或者不多的更新,甚至對數據 的覆蓋不是真的很關鍵,或者解決併發的代價超過了支持併發所帶來的優點。在這種狀況下,你就不須要讓你的程序支持併發衝突的處理。數據庫
若是你的應用須要在併發環境下防止偶然的數 據丟失,一種方式是經過數據庫的鎖來實現。這種方式被稱爲悲觀併發。例如,在從數據庫中讀取一行數據以前,能夠申請一個只讀鎖,或者一個更新訪問鎖。若是 你對數據行使用了更新訪問鎖,就沒有其餘的用戶能夠獲取不論是隻讀鎖仍是更新訪問鎖,由於他們可能獲取正在被修改中的數據。若是你使用只讀鎖來鎖定一行, 其餘用戶也可使用只讀訪問,可是不能進行更新。瀏覽器
管理鎖有一些缺點,對程序來講可能很複雜。它須要重要的數據庫管理資源,對於大量用戶的時候可能致使性能問題 ( 擴展性很差 ),因爲這些緣由,不是全部的數據庫管理系統都支持悲觀鎖。EF 對悲觀鎖沒有提供內建的支持,這個教程也不會演示如何實現它。服務器
除了悲觀併發以外的另外一方案是樂觀併發。樂觀併發意味着容許併發衝突發生,若是出現了就作出適當的反應。例如,John 執行 Department 的編輯頁面,將 English 系的 Budget 從 $350,000.00 修改成 $100,000.00 ( John 管理與 English 有競爭的系,但願將一些資金轉移到他本身的系使用 )。併發
在 John 點擊保存 Save 以前,Jane 運行一樣的頁面,將開始時間 Start Date 字段從 9/1/2007 修改成 1/1/1999 ( Jane 管理歷史系,但願它的歷史更加悠久 )mvc
John 先點擊保存 Save,而後在回到 Index 頁面的時候看到本身的修改。而後 Jane 點擊保存 Save。下一步發生什麼取決於如何處理併發衝突。可能的狀況以下:app
這種方法能夠減小可能形成數據丟失的衝突次數,可是若是用戶修改同一個實體的相同屬性的話,會丟失數據, EF 具體依賴於你如何實現你的更新代碼。這種方式不適合 Web 應用程序,由於須要你維護大量的狀態,以便追蹤全部新值的原始狀態。維護大量的狀態會影響到程序的性能,由於既須要服務器的資源,又須要將狀態保存在頁面中 ( 例如,使用隱藏域 )。asp.net
你能夠經過處理 EF 拋出的 OptimisticConcurrencyException 異常來處理衝突。爲了知道何時 EF 拋出了這種異常,EF 必須可以檢測衝突。所以,你必須合理配置數據庫和數據模型。啓用衝突檢測的一些選項以下:post
在本教程剩下的部分,你須要在 Department 實體上增長一個追蹤列,建立控制器和視圖,而後檢查一切是否工做正常。
注意:若是你沒有使用追蹤列來實現併發,你就必須經過使用 ConcurrencyCheck 特性標記全部的非主屬性用在併發跟蹤中。這將會使 EF 將全部的列包含在 Update 語句的 Where 子句中。
在 Models\Departments.cs 文件中,增長跟蹤屬性。
[Timestamp] public Byte[] Timestamp { get; set; }
Timestamp 特性指定隨後的列將會被包含在 Update 或者 Delete 語句的 Where 子句中。
如同建立其餘的控制器同樣,建立 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>
運行程序,點擊 Departments.
點擊 Edit 超級連接,而後再打開一個新的瀏覽器窗口,窗口中使用相同的地址顯示相同的信息。
在第一個瀏覽器的窗口中修改一個字段的內容,而後點擊 Save。
瀏覽器回到 Index 頁面顯示修改以後的值。
在第二個瀏覽器窗口中將一樣的字段修改成不一樣的值,
在第二個瀏覽器窗口中,點擊 Save,將會看到以下錯誤信息。
再次點擊 Save。在第二個瀏覽器窗口中輸入的值被保存到數據庫中,在 Index 頁面顯示的時候出如今頁面上。
對於刪除頁面,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 Patterns和Working with Property Values。下一次教程將會演示針對教師 Instructor 和學生 Student 實體的表層次的繼承。