英文渣水平,大夥湊合着看吧……html
這是微軟官方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表格中顯示集合中的內容。mvc
在Student控制器中,Details視圖中的動做方法使用Find方法來檢索單個學生實體。app
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參數傳遞給方法用於檢索。框架
<dt> @Html.DisplayNameFor(model => model.LastName) </dt> <dd> @Html.DisplayFor(model => model.LastName) </dd>
<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導航屬性時,一個新查詢發送到數據庫以檢索數據。您能夠在這裏閱讀更多關於延遲加載和預先加載的相關信息。)
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無需任何更改。
if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); }
在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方法從新顯示帶錯誤消息的提示頁面,給用戶一個機會,取消或重試。
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而且錯誤信息被傳遞給視圖。
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。
<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平臺及工具團隊的高級程序員,做家。