英文渣水平,大伙湊合着看吧……
這是微軟官方SignalR 2.0教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這里是第二篇:實現基本的CRUD功能
原文:Implementing Basic CRUD Functionality with the Entity Framework in ASP.NET MVC Application
譯文版權所有,謝絕全文轉載——但你可以在你的網站上添加到該教程的鏈接。
在之前的教程中,我們使用實體框架及SQL Server LocalDB創建了一個用來存儲和顯示數據的MVC應用程序。在本教程中,你將審閱並定義 MVC腳手架在控制器和視圖中自動為您創建的CRUD(創建、讀取、更新、刪除)代碼。
注意:通常我們實現倉儲模式,即在你的控制器和數據存取層之間創建一個抽象層來存取數據。為了保持教程的簡潔並將注意力聚焦在如何使用實體框架上,我們在本教程中沒有使用倉儲模式。關於更多的信息,請參閱ASP.NET Data Access Content Map。
在本教程中,你將建立以下Web頁面:



創建一個詳細頁面
在學生索引頁面中,腳手架代碼將Enrollments屬性排除在外,因為該屬性是一個集合。在詳細頁面中,我們將在HTML表格中顯示集合中的內容。
在Student控制器中,Details視圖中的動作方法使用Find方法來檢索單個學生實體。
1 public ActionResult Details(int? id) 2 { 3 if (id == null) 4 { 5 return new HttpStatusCodeResult(HttpStatusCode.BadRequest); 6 } 7 Student student = db.Students.Find(id); 8 if (student == null) 9 { 10 return HttpNotFound(); 11 } 12 return View(student); 13 }
索引頁上詳細信息超鏈接的路由數據中的鍵值以id參數傳遞給方法用於檢索。
- 打開Details視圖,每個字段都使用DisplayFor幫助器來呈現數據,如下面的代碼所示:
<dt> @Html.DisplayNameFor(model => model.LastName) </dt> <dd> @Html.DisplayFor(model => model.LastName) </dd>
- 在EnrollmentData字段后,</dl>標簽之前,將下列高亮代碼添加到顯示列表中:
<dd> @Html.DisplayFor(model => model.EnrollmentDate) </dd> <dt> @Html.DisplayNameFor(model => model.Enrollments) </dt> <dd> <table class="table"> <tr> <th>Course Title</th> <th>Grade</th> </tr> @foreach (var item in Model.Enrollments) { <tr> <td> @Html.DisplayFor(modelItem => item.Course.Title) </td> <td> @Html.DisplayFor(modelItem => item.Grade) </td> </tr> } </table> </dd> </dl> </div>
如果代碼縮進有問題,你可以在粘貼過代碼后按下Ctrl-K-D來糾正它。
此段代碼遍歷Enrollments導航屬性中的實體,在遍歷到的每個Enrollment實體中,顯示出課程標題和成績。課程標題從Enrollments實體下的Course導航屬性中的Course實體中獲取。所有這些數據是在需要時自動從數據庫檢索到的。(換句話說,在此處您正在使用延遲加載。你沒有指定Courses導航屬性是需要預先加載的,所以在同一次查詢中,只有學生的數據從數據庫中查詢並讀取。相反,當您第一次嘗試訪問Enrollments導航屬性時,一個新查詢發送到數據庫以檢索數據。您可以在這里閱讀更多關於延遲加載和預先加載的相關信息。) - 運行項目,點擊學生選項卡並點擊Alexander的詳情連接。(如果你按下Ctrl+F5時,Details.cshtml文件時打開的,你會收到一個HTTP 400錯誤頁面。因為VS會認為你想要查看Details頁面而直接打開該頁面。由於不是從連接點擊進入該頁面,所以頁面無法獲得所需的參數而發生錯誤。在這種情況下,你需要從URL移除Student/Details然后重試。或者關閉瀏覽器,右鍵點擊項目,點擊視圖,然后點擊在瀏覽器中查看。)
你可以看到你選擇學生的課程及成績。
更新創建頁面
- 在Student控制器中,使用以下的代碼添加一個try-catch代碼塊並從Bind特性中刪除ID屬性來替換腳手架生成的HttpPost創建方法。
1 [HttpPost] 2 [ValidateAntiForgeryToken] 3 public ActionResult Create([Bind(Include="LastName,FirstMidName,EnrollmentDate")] Student student) 4 { 5 try 6 { 7 if (ModelState.IsValid) 8 { 9 db.Students.Add(student); 10 db.SaveChanges(); 11 return RedirectToAction("Index"); 12 } 13 } 14 catch (DataException) 15 { 16 ModelState.AddModelError("", "保存數據時出現錯誤。請重試,如果問題依舊存在請聯系系統管理員。"); 17 } 18 return View(student); 19 }
這段代碼將ASP.NET MVC模型綁定器創建的Student實體添加到學生實體集合並保存到數據庫中。(模型綁定器是能夠使你更輕松地處理表單提交數據的ASP.NET MVC功能;模型綁定器將提交的表單值轉換為CLR值並將它們傳遞給動作方法中的參數。在本例中,模型綁定器使用了Form表單集合中的值來實例化了一個學生實體。)
因為ID是主鍵值,在插入新紀錄時,SQL Server會自動設置該值,所以我們將ID從Bind特性中刪除來禁止用戶設置該值。
安全注意事項:ValidateAntiForgeryToken屬性有助於防止跨站請求偽造攻擊,它需要在視圖中相應地設置Html.AntiForgeryToken()語句,您將在后面看到。
Bind特性用於防止“過多發布”攻擊。舉例來說,假設Student實體中包含一個Secert字段,你不想讓此屬性由Web頁面來進行更新,所以你沒有在頁面上放置Secert的相應輸入框。但黑客可以通過工具強行附加Secert字段即相應值到表單中並發送給服務器端。在沒有使用Bind的默認情況下,模型綁定器會自動遍歷提交過來的所有表單值並嘗試更新到實體中,所以Secert也會得到更新——使用黑客強行附加的值。
安全的做法是使用Bind特性的Include參數,可以讓你指定那些字段是由模型綁定器來進行更新的,也可以相反地使用Exclude來排除你不想讓模型綁定器來進行更新的屬性。我們推薦使用Include的理由是,如果對實體添加了新的屬性,Exclude是不會自動更新的,新屬性會默認被模型綁定器進行更新。
另一種替代方法是使用ViewModel。ViewModel中僅包含你想要綁定的屬性。在模型綁定器完成對ViewModel的更新后,將ViewModel中的屬性復制到實體的實例已完成更新。
try-catch塊是除了Bind特性外您對腳手架代碼所做的唯一更改。如果在保存時有一個源於DataException的異常被引發,一個通用的錯誤消息被顯示出來。由於DataException錯誤有時會由外部的應用程序引發,而不是程序編寫的錯誤,所以建議用戶進行再次嘗試。此外,雖然該實例中沒有實現,在生產環境下,所有的應用程序錯誤都應該被記錄下來。
Create.cshtml的代碼類似Details.cshtml的,除了DisplayFor被EditorFor和ValidationMessageFor幫助器替代了。下面是相關的代碼:<div class="form-group"> @Html.LabelFor(model => model.LastName, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.LastName) @Html.ValidationMessageFor(model => model.LastName) </div> </div>
Create.cshtml還包含@Html.AntiForgeryToken()方法和控制器中的ValidateAntiForgeryToken特性,已防止跨站請求偽造攻擊。
Create.cshtml無需任何更改。 - 通過選擇學生選項卡,單擊新創建運行該頁面。
- 輸入姓名和無效的日期,然后單擊Create查看錯誤消息。

這是默認情況下的服務器段驗證。在以后的教程中您會看到如何添加特性並生成客戶端驗證。以下突出顯示的代碼顯示了創建方法中的模型驗證檢查。
if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); }
- 將日期更改為一個有效的值,單擊創建,然后參閱索引頁面中顯示的新學生。

更新編輯HttpPost頁面
在Student控制器中,HttpGet Edit方法(沒有HttpPost特性的那一個)使用Find方法來檢索所選擇的Student實體,正如你在Details方法中看到的一樣。您不需要更新此方法。
使用以下代碼以添加一個try-catch塊來替換HttpPost Edit方法:
1 [HttpPost] 2 [ValidateAntiForgeryToken] 3 public ActionResult Edit([Bind(Include="ID,LastName,FirstMidName,EnrollmentDate")] Student student) 4 { 5 try 6 { 7 if (ModelState.IsValid) 8 { 9 db.Entry(student).State = EntityState.Modified; 10 db.SaveChanges(); 11 return RedirectToAction("Index"); 12 } 13 } 14 catch (DataException) 15 { 16 ModelState.AddModelError("","無法保存變更,請重試,如果問題依舊,請聯系管理員。") 17 } 18 return View(student); 19 }
這段代碼類似於你在HttpPost Create方法中看到的那樣,但不是將由模型綁定器創建的提示添加到實體集,這段代碼設置實體上的標志位,表明它已經被更改。當調用SaveChanges方法時,Modified標志使實體框架來創建SQL語句並執行以更新數據庫。數據庫中該行的所有列都將被更新,包括哪些用戶沒有改變的,並發沖突被忽略。
實體狀態和附加和調用SaveChanges方法
數據庫上下文會跟蹤內存中的實體是否與數據庫中的行保持同步。並根據同步的信息來確定調用SaveChanges方法時會發生什么。例如,讓你傳遞一個新實體給Add方法,該實體的狀態設置為Added。然后您調用SaveChanges方法時,數據庫上下文會生成一個SQL Insert命令以插入數據。
一個實體可能處於以下狀態之一:
- Added。該實體尚未在數據庫中。SaveChanges方法將發出一個Insert語句。
- Unchanged。SaveChanges對該實體什么都不需要做。當你從數據庫讀出一個實體時,該實體就為這一狀態。
- Modified。某些或所有實體的屬性值已都被更改。SaveChanges將發出一個Update語句。
- Deleted。該實體已經被標志為刪除。SaveChanges將發出一個Delete語句。
- Detached。該實體沒有被跟蹤的數據庫上下文。
在桌面應用程序中,狀態變化通常是自動設置的。在桌面型的應用程序中,你看到一個實體並更改它的一些屬性值,將導致它的實體狀態自動更改為Modified。然后你調用SaveChanges,實體框架生成一個SQL Update來更新你進行了變更的屬性。
Web應用程序的斷開連接性質不允許這種連續序列。數據庫上下文在讀取到實體並將其呈現在頁面上,之后便被銷毀。當HttpPost Edit動作方法被調用時,一個新請求被處理,你將獲取一個新的數據庫上下文的實例。所以你必須手動設置實體狀態為Modified,然后你調用SaveChanges,實體框架更新數據庫中的所有的數據行,因為上下文沒有辦法知道那個屬性是你進行了變更的。
如果你想在SQL Update語句只更新用戶實際更改的字段,你可以以某種方式保存原來的值(比如隱藏字段),這樣在調用HttpPost Edit方法時就可以使用它們。然后,你可以使用原值來創建一個Student實體,調用原始版本的Attach方法更新實體的值到新值,然后調用SaveChanges。更多信息請參見MSDN上的Entity states and SaveChanges 和 Local Data。
如同你在Create.cshtml中見到的一樣,Edit.cshtml中的HTML和Razor代碼無需更改。
通過選擇學生選項卡,單擊一個學生的編輯超鏈接運行該頁面。

改變一些數據並單擊保存,你可以在索引頁面中看到你所做出的更改。

更新刪除頁面
在學生控制器中,HttpGet Delete方法的模板代碼使用Find方法檢索所選的Student實體,正如你在Details和Edit方法中看到的那樣。然而,調用SaveChanges失敗時的錯誤信息需要修正,你需要向該方法和視圖中添加一些功能。
類似你之前看到的更新和創建操作,刪除操作需要兩個動作方法。Get請求用來顯示一個視圖,讓用戶有機會批准或取消刪除操作。如果用戶批准,POST請求被創建,HttpPost Delete方法被調用,然后該方法將實際執行刪除操作。
您將添加一個try-catch塊到HttpPost Delete方法來處理數據庫更新時可能發生的任何錯誤。如果出現了錯誤,則HttpPost Delete方法調用HttpGet Delete方法,向其傳遞一個參數表明發生了錯誤。HttpGet Delete方法重新顯示帶錯誤消息的提示頁面,給用戶一個機會,取消或重試。
- 使用下面的代碼更新HttpGet Delete方法:
1 public ActionResult Delete(int? id,bool? saveChangesError = false) 2 { 3 if (id == null) 4 { 5 return new HttpStatusCodeResult(HttpStatusCode.BadRequest); 6 } 7 if (saveChangesError.GetValueOrDefault()) 8 { 9 ViewBag.ErrorMessage = "刪除錯誤,請重試。如果錯誤依舊,請聯系管理員。"; 10 } 11 Student student = db.Students.Find(id); 12 if (student == null) 13 { 14 return HttpNotFound(); 15 } 16 return View(student); 17 }
此代碼接受一個可選擇參數,指示該方法是否是由保存更改后出現了故障的的方法調用的。在HttpGet Delete方法不是由之前出現了錯誤的方法被調用的,該參數為false。當HttpPost Delete出現了錯誤,參數為true並且錯誤信息被傳遞給視圖。
- 使用下面的代碼替換HttpPost Delete動作方法(名稱為DeleteConfirmed的那個)用來執行刪除操作並捕獲任何數據庫更新錯誤。
1 [HttpPost] 2 [ValidateAntiForgeryToken] 3 public ActionResult Delete(int id) 4 { 5 try 6 { 7 Student student = db.Students.Find(id); 8 db.Students.Remove(student); 9 db.SaveChanges(); 10 } 11 catch(DataException) 12 { 13 return RedirectToAction("Delete", new { id = id, saveChangesError = true }); 14 } 15 return RedirectToAction("Index"); 16 }
這段代碼從數據庫中檢索要刪除的實體,然后調用Remove方法將實體的狀態設置為Deleted。當調用SaveChanges命令時,數據庫上下文將生成SQL Delete命令將實體從數據庫中刪除。此外,我們將動作方法從DeleteConfirmed改為Delete。腳手架代碼將DeleteConfirmed方法命名為HttpPost Delete動作以設置一個唯一的簽名(CLR需要有不同的方法參數來重載方法)。現在,方法簽名是唯一的,基於MVC的約定在可以讓你通過使用HttpPost和HttpGet特性來使用相同名稱的刪除方法。
如果在一個高容量應用程序中改善性能是優先事項,你可以避免不必須要的SQL查詢,使用下面的代碼替換Find和Remove方法:
Student studentToDelete = new Student() { ID = id }; db.Entry(studentToDelete).State = EntityState.Deleted;
這段代碼使用唯一的一個主鍵值實例化了一個學生實體,然后將實體狀態設置為Deleted。這便是實體框架刪除一個實體所需要的全部信息。
要注意HttpGet Delete方法不會執行數據刪除。在一個Get請求響應中執行刪除動作(或者創建、修改等對數據進行變更的動作)將帶來安全風險。有關風險的詳細信息請參見ASP.NET MVC Tip #46 — Don't use Delete Links because they create Security Holes。 - 在Delete.cshtml中,在H2和H3標簽之間添加錯誤信息,如下面的代碼:
<h2>Delete</h2> <p class="error">@ViewBag.ErrorMessage</p> <h3>Are you sure you want to delete this?</h3>
運行程序,點擊學生選項卡,點擊某個學生的刪除連接:

- 點擊刪除按鈕,你會看到在索引頁面中指定的學生已經被刪除。(稍后的教程中我們將介紹並發處理)
確保數據庫連接不是一直打開
要確保數據庫連接正確的關閉並釋放所占用的資源,當你使用完數據庫上下問候,需要將其銷毀。這就是為什么腳手架代碼在Student控制器類的最后部分提供了一個Dispose方法,如下面的代碼:
protected override void Dispose(bool disposing) { if (disposing) { db.Dispose(); } base.Dispose(disposing); }
控制器基類已經實現了IDisposeable接口,所以這段代碼只是簡單的重寫了Dispose(bool)方法以顯式地銷毀上下文實例。
處理事務
默認情況下,實體框架隱式的實現事務處理。當你對多個表或行進行了更改后調用SaveChanges,實體框架會自動確保你的所有更改全部成功保存到數據庫或全部保存失敗。如果某些更新完成,之后發生了一個錯誤,那之前完成的更新將自動全部回滾。當你需要對事務的更多的控制權時——比如您想要在一次事務中包含在實體框架之外的操作——參見MSDN上的Working with Transactions。
總結
您現在擁有一套針對Student實體完成的CRUD操作。你使用了MVC幫助器來生成數據字段的UI元素,關於幫助器的更多信息,參見 Rendering a Form Using HTML Helpers。
在下一節教程中我們會給索引頁添加排序和分頁等更多的功能。
作者信息
Tom Dykstra - Tom Dykstra是微軟Web平台及工具團隊的高級程序員,作家。
