教學課程:在 ASP.NET MVC 中使用實體框架實作 CRUD 功能
在上一教學課程中,您建立了一個使用實體框架 (EF) 6 和 SQL Server LocalDB 儲存和顯示資料的 MVC 應用程式。 在本教學課程中,您將查看並自訂 MVC 支架在控制器和檢視中自動為您建立的建立、讀取、更新、刪除 (CRUD) 程式碼。
注意
實作存放庫模式,以在您的控制器及資料存取層之間建立抽象層是一種非常常見的做法。 為了使這些教學課程簡單並專注於教導如何使用 EF 6 本身,它們不使用儲存庫。 有關如何實現儲存庫的信息,請參閱 ASP.NET 資料存取內容映射。
以下是您建立的網頁的範例:
在本教學課程中,您已:
- 建立詳細資訊頁面
- 更新 [建立] 頁面
- 更新 HttpPost Edit 方法
- 更新 [刪除] 頁面
- 關閉資料庫連線
- 處理交易
必要條件
建立詳細資訊頁面
學生Index
頁面的鷹架程式碼忽略了 Enrollments
屬性,因為該屬性包含一個集合。 在 Details
頁面中,您將在 HTML 表格中顯示集合的內容。
在 Controllers\StudentController.cs 中,檢視的操作方法使用 Details
FindStudent
方法來檢索單一實體。
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
鍵值作為id
參數傳遞給方法,來自索引頁面上詳細資訊超連結中的路由資料。
提示:路線資料
路由資料是模型綁定器在路由表中指定的 URL 段中找到的資料。 例如,預設路由指定 controller
、action
和 id
段:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
在以下 URL 中,預設路由對應為 Instructor
、controller
和 Index
1 action
id
;這些是路線資料值。
http://localhost:1230/Instructor/Index/1?courseID=2021
?courseID=2021
是查詢字串值。 如果您將 id
作為查詢字串值傳遞,模型綁定器也會運作:
http://localhost:1230/Instructor/Index?id=1&CourseID=2021
URL 由 Razor 檢視中的 ActionLink
語句建立。 在下面的程式碼中,id
參數與預設路由匹配,因此 id
被添加到路由資料中。
@Html.ActionLink("Select", "Index", new { id = item.PersonID })
在以下程式碼中,courseID
與預設路由中的參數不匹配,因此將其新增為查詢字串。
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
建立詳細資訊頁面
打開 Views\Student\Details.cshtml。
每個欄位都使用
DisplayFor
助手顯示,如下列範例所示:<dt> @Html.DisplayNameFor(model => model.LastName) </dt> <dd> @Html.DisplayFor(model => model.LastName) </dd>
在
EnrollmentDate
欄位後面、結束</dl>
標記之前,新增突出顯示的程式碼以顯示註冊列表,如下例所示:<dt> @Html.DisplayNameFor(model => model.EnrollmentDate) </dt> <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> <p> @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) | @Html.ActionLink("Back to List", "Index") </p>
如果貼上程式碼後程式碼縮排錯誤,請按 Ctrl K+、Ctrl D+ 進行格式化。
此程式碼會以迴圈逐一巡覽
Enrollments
導覽屬性中的實體。 對於屬性中的每個Enrollment
實體,它顯示課程標題和成績。 課程標題是從儲存在Course
實體的Course
導航屬性中的Enrollments
實體檢索的。 所有這些資料都會在需要時自動從資料庫中檢索。 換句話說,您在這裡使用的是延遲載入。 您沒有為Courses
導航屬性指定預先加載,因此不會在獲取學生的相同查詢中檢索註冊情況。 相反,當您第一次嘗試存取Enrollments
導航屬性時,系統會向資料庫發送新查詢以檢索資料。 您可以在本系列後面的閱讀相關資料教學課程中了解有關延遲加載和預先加載的更多資訊。透過啟動程式 (Ctrl+F5)、選擇「學生」索引標籤,然後點擊 Alexander Carson 的「詳細資料」鏈接,開啟詳細資料頁面。 (如果在 Details.cshtml 檔案開啟時按 Ctrl+F5,則會收到 HTTP 400 錯誤。這是因為 Visual Studio> 嘗試執行「詳細資料」頁面,但無法從指定要顯示的學生的連結存取該頁面。如果發生這種情況,請從URL 中刪除「Student/Details」並重試,或關閉瀏覽器,右鍵點擊該項目,然後按一下「瀏覽器中的 ViewView」。)
您將看到所選學生的課程和成績清單。
關閉瀏覽器。
更新 [建立] 頁面
在 Controllers\StudentController.cs 中,將HttpPostAttribute
Create
操作方法替換為以下程式碼。 此程式碼會新增一個try-catch
區塊並從支架方法的 BindAttribute 屬性中刪除ID
:[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "LastName, FirstMidName, EnrollmentDate")]Student student) { try { if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); } } catch (DataException /* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator."); } return View(student); }
此程式碼將 ASP.NET MVC 模型綁定器建立的
Student
實體新增至Students
實體集,然後將變更儲存到資料庫。 模型綁定器是指 ASP.NET MVC 功能,它可讓您更輕鬆地處理表單提交的資料;模型綁定器將發布的表單值轉換為 CLR 類型,並將它們傳遞給參數中的操作方法。 在這種情況下,模型綁定器使用Form
集合中的屬性值為您執行個體化Student
實體。您從 Bind 屬性中刪除了它,因為它是插入行時 SQL Server 將自動設定的主鍵值。
ID
ID
使用者輸入不會設定ID
值。安全警告 - 此屬性有助於防止跨網站請求偽造攻擊。
ValidateAntiForgeryToken
它需要檢視中的相應Html.AntiForgeryToken()
語句,稍後您將看到。Bind
屬性是防止建立場景中過度發布的一種方法。 例如,假設Student
實體包含您不希望此網頁設定的Secret
屬性。public class Student { public int ID { get; set; } public string LastName { get; set; } public string FirstMidName { get; set; } public DateTime EnrollmentDate { get; set; } public string Secret { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } }
即使網頁上沒有
Secret
欄位,駭客也可以使用 fiddler 等工具或編寫一些 JavaScript 來發布Secret
表單值。 如果沒有 BindAttribute 屬性限制模型綁定程序在建立執行個體時使用的欄位,模型綁定程序將選取該表單值並使用它來建立實體執行個體。Student
Secret
Student
則無論駭客在Secret
表單欄位中指定了什麼值,該值都會更新到您的資料庫中。 下圖顯示了 fiddler 工具將Secret
欄位 (值為「OverPost」) 新增至已發佈的表單值。"OverPost" 的值會成功新增到插入資料列的
Secret
屬性中,即使您沒有要讓網頁設定該屬性。最好使用帶有
Bind
屬性的Include
參數來明確列出欄位。 也可以使用Exclude
參數來阻止要排除的欄位。 更安全的Include
原因是,當您向實體新增屬性時,新欄位不會自動受到Exclude
清單保護。您可以透過先從資料庫讀取實體
TryUpdateModel
,然後呼叫 並傳入明確允許的屬性清單來防止編輯場景中的過度發布。 這就是這些教學課程中使用的方法。許多開發人員首選的防止過度發布的另一種方法是使用檢視模型而不是具有模型綁定的實體類別。 意即僅在檢視模型中包含您想要更新的屬性。 MVC 模型綁定完成後,將檢視模型屬性複製到實體執行個體,可以選擇使用 AutoMapper 等工具。 在實體執行個體上使用 db.Entry 將其狀態設為 Unchanged,然後將檢視模型中包含的每個實體屬性的 Property("PropertyName").IsModified 設為 true。 這個方法可同時運用在編輯及建立案例中。
除了
Bind
屬性之外,try-catch
區塊是您對支架程式碼所做的唯一更改。 若在儲存變更時捕捉到衍生自 DataException 的例外狀況,則會顯示一般錯誤訊息。 DataException 例外狀況有時候是因為某些外部因素造成的,而非程式設計上的錯誤,因此系統會建議使用者再試一次。 雖然在此範例中並未實作,但生產環境品質的應用程式應記錄例外狀況。 如需詳細資訊,請參閱監視及遙測 (使用 Azure 建置現實世界的雲端應用程式)中的深入解析記錄檔一節。Views\Student\Create.cshtml 中的程式碼與您在 Details.cshtml 中看到的程式碼類似,不同之處在於每個欄位使用
EditorFor
和ValidationMessageFor
幫助器而不是DisplayFor
。 這是相關程式碼:<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 編輯方法
將
Edit
操作方法替換為以下程式碼 HttpPostAttribute:[HttpPost, ActionName("Edit")] [ValidateAntiForgeryToken] public ActionResult EditPost(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var studentToUpdate = db.Students.Find(id); if (TryUpdateModel(studentToUpdate, "", new string[] { "LastName", "FirstMidName", "EnrollmentDate" })) { try { db.SaveChanges(); return RedirectToAction("Index"); } catch (DataException /* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator."); } } return View(studentToUpdate); }
注意
在 Controllers\StudentController.cs 中,
HttpGet Edit
方法 (沒有HttpPost
屬性的Find
方法) 使用該方法來檢索選定的實體,如您在該方法中看到的那樣。Student
Details
您不需要變更這個方法。這些變更實施了安全最佳實踐,以防止過度發布。支架產生一個
Bind
屬性,並將模型綁定器建立的實體新增至具有 Modified 標誌的實體集中。 不再建議使用該程式碼,因為Bind
屬性會清除Include
參數中未列出的欄位中任何預先存在的資料。 將來,MVC 控制器支架將被更新,以便它不會為 Edit 方法產生Bind
屬性。新程式碼讀取現有實體並呼叫 TryUpdateModel 以根據已發布表單資料中的使用者輸入更新欄位。 實體框架的自動變更追蹤在實體上設定 EntityState.Modified 標誌。 當呼叫 SaveChanges 方法時,此Modified標誌會導致實體框架建立 SQL 語句來更新資料庫行。 並發衝突將被忽略,資料庫行的所有欄位都會更新,包括使用者未變更的列。 (後面的教學課程將介紹如何處理並發衝突,如果您只想更新資料庫中的個別欄位,則可以將實體設為 EntityState.Unchanged,並將個別欄位設為 EntityState.Modified。)
為了防止過度發布,您希望透過編輯頁面可更新的欄位會在
TryUpdateModel
參數中列出。 雖然目前沒有額外保護的欄位,但列出您希望模型繫結器繫結的欄位可確保您於未來將欄位新增到資料模型中時,新增的欄位會自動獲得保護,直到您明確的在這裡新增它們為止。由於這些更改,HttpPost Edit 方法的方法簽章與 HttpGet edit 方法相同;因此您已將方法重新命名為 EditPost。
提示
實體狀態以及 Attach 和 SaveChanges 方法
資料庫內容會追蹤實體在記憶體中是否與其在資料庫中相對應的資料列保持同步,並且此項資訊會決定當您呼叫
SaveChanges
方法時會發生什麼事情。 例如,當您將新實體傳遞給 Add 方法時,該實體的狀態將設為Added
。 然後,當您呼叫 SaveChanges 方法時,資料庫上下文會發出 SQLINSERT
指令。實體可為下列狀態中的其中一個:
Added
. 該實體尚不存在於資料庫中。SaveChanges
方法必須發出一個INSERT
聲明。Unchanged
.SaveChanges
方法針對這個實體不需要進行任何動作。 當您從資料庫讀取一個實體時,實體便會以此狀態開始。Modified
. 實體中一部分或全部的屬性值已經過修改。SaveChanges
方法必須發出一個UPDATE
聲明。Deleted
. 實體已遭標示刪除。SaveChanges
方法必須發出一個DELETE
聲明。Detached
. 實體未獲得資料庫內容追蹤。
在桌面應用程式中,狀態變更通常會自動進行設定。 在桌面類型的應用程式中,您讀取實體並變更其某些屬性值。 這會使得其實體狀態自動變更為
Modified
。 然後,當您呼叫SaveChanges
時,實體框架會產生一條 SQL 語句UPDATE
,該語句僅更新您變更的實際屬性。Web 應用程式的斷開連接性質不允許這種連續的序列。 讀取實體的 DbContext 在頁面呈現後被釋放。 當呼叫
HttpPost
Edit
動作方法時,會發出一個新請求,並且您有一個新的 DbContext 執行個體,因此您必須手動將實體狀態設為 然後,當您呼叫Modified.
時,實體框架會更新資料庫行的所有列SaveChanges
,因為 context 無法知道您更改了哪些屬性。如果希望 SQL
Update
語句只更新使用者實際變更的欄位,可以透過某種方式 (例如隱藏欄位) 保存原始值,以便在呼叫HttpPost
Edit
方法時可用。 然後,您可以使用原始值建立Student
實體,使用該實體的原始版本呼叫該方法Attach
,將SaveChanges.
實體的值更新為新值,然後呼叫 。Views\Student\Edit.cshtml 中的 HTML 和 Razor 程式碼與您在 Create.cshtml 中看到的類似,無需進行任何變更。
透過啟動程式、選擇學生索引標籤,然後按一下編輯超連結來運行該頁面。
變更一部分的資料,然後按一下 [儲存]。 您可以在索引頁面中看到更改的資料。
關閉瀏覽器。
更新 [刪除] 頁面
在 ControllersHttpGetAttribute\StudentController.csDelete
中,該方法的範本程式碼使用該方法來檢索所選實體,如您在 Find
和 Student
方法中看到的那樣Details
Edit
。 然而,若要在呼叫 SaveChanges
失敗時實作自訂錯誤訊息,您需要將一些功能新增至此方法及其對應的檢視。
如同您在更新及建立作業中所看到的,刪除作業需要兩個動作方法。 回應 GET 請求而呼叫的方法會顯示一個檢視,使用戶有機會批准或取消刪除操作。 若使用者核准,則便會建立 POST 要求。 當發生這種情況時,將呼叫 HttpPost
Delete
方法,然後該方法實際上執行刪除操作。
您將向 try-catch
方法添加一個 HttpPostAttributeDelete
區塊來處理更新資料庫時可能發生的任何錯誤。 如果發生錯誤,HttpPostAttributeDelete
HttpGetAttributeDelete
方法將呼叫該方法,並向其傳遞一個指示發生錯誤的參數。 然後,HttpGetAttributeDelete
方法重新顯示確認頁面以及錯誤訊息,為使用者提供取消或重試的機會。
將操作方法替換為以下程式碼,HttpGetAttribute
Delete
程式碼管理錯誤報告:public ActionResult Delete(int? id, bool? saveChangesError=false) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } if (saveChangesError.GetValueOrDefault()) { ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator."; } Student student = db.Students.Find(id); if (student == null) { return HttpNotFound(); } return View(student); }
此程式碼接受一個可選參數,該參數指示在儲存變更失敗後是否呼叫該方法。 此參數是
false
當HttpGet
Delete
方法被呼叫而之前沒有失敗時的參數。 當HttpPost
Delete
方法呼叫它以回應資料庫更新錯誤時,參數為true
,並且錯誤訊息將傳遞到檢視。將 HttpPostAttribute
Delete
操作方法 (名為DeleteConfirmed
) 替換為以下程式碼,該程式碼執行實際的刪除操作並捕獲任何資料庫更新錯誤。[HttpPost] [ValidateAntiForgeryToken] public ActionResult Delete(int id) { try { Student student = db.Students.Find(id); db.Students.Remove(student); db.SaveChanges(); } catch (DataException/* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. return RedirectToAction("Delete", new { id = id, saveChangesError = true }); } return RedirectToAction("Index"); }
此程式碼會擷取選取的實體,然後呼叫 Remove 方法將實體的狀態設為
Deleted
。 當呼叫SaveChanges
時,便會產生 SQLDELETE
命令。 您也將動作方法的名稱從DeleteConfirmed
變更為Delete
。 支架程式碼命名HttpPost
Delete
方法以賦予DeleteConfirmed
HttpPost
方法唯一的簽名。 (CLR 要求重載方法具有不同的方法參數。) 既然簽名是唯一的,您可以堅持 MVC 約定,並對HttpPost
和HttpGet
delete 方法使用相同的名稱。如果優先考慮提高大容量應用程式的效能,則可以透過使用以下程式碼替換呼叫
Find
和Remove
方法的程式碼行來避免不必要的 SQL 查詢來檢索行:Student studentToDelete = new Student() { ID = id }; db.Entry(studentToDelete).State = EntityState.Deleted;
此程式碼僅使用主鍵值執行個體化
Student
實體,然後將實體狀態設為Deleted
。 這便是 Entity Framework 要刪除實體所需要的一切資訊。如前所述,
HttpGet
Delete
方法不會刪除資料。 回應 GET 請求而執行刪除操作 (或執行任何編輯操作、建立操作或任何其他變更資料的操作) 會產生安全風險。在 Views\Student\Delete.cshtml 中,在
h2
標題和h3
標題之間新增錯誤訊息,如下例所示:<h2>Delete</h2> <p class="error">@ViewBag.ErrorMessage</p> <h3>Are you sure you want to delete this?</h3>
透過啟動程式、選擇學生索引標籤,然後按一下刪除超連結來運行該頁面。
在顯示您確定要刪除此內容嗎?的頁面上選擇刪除。
顯示索引頁面,但不顯示已刪除的學生。 (您將在並發教學課程中看到正在運行的錯誤處理程式碼的範例。)
關閉資料庫連線
若要盡快關閉資料庫連線並釋放它們所佔用的資源,請在使用完上下文執行個體後將其釋放。 這就是為什麼支架程式碼在 StudentController.cs 中的類別末尾提供 Dispose 方法的原因,如下範例所示:StudentController
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
基類 Controller
已經實現了該接口,因此此程式碼只需向該方法添加一個重寫即可明確處置上下文執行個體。IDisposable
Dispose(bool)
處理交易
根據預設,Entity Framework 隱含性的實作了交易。 在您對多行或多表進行更改然後調用 SaveChanges
的情況下,實體框架會自動確保所有更改要么成功,要么全部失敗。 若有些變更已先完成,之後卻發生錯誤,則這些變更都會自動進行復原。 對於需要更多控制的場景 (例如,如果您想要將在實體框架外部完成的操作包含在交易中),請參閱使用事務。
取得程式碼
其他資源
您現在擁有一組完整的頁面,可以對 Student
實體執行簡單的 CRUD 操作。 您使用 MVC 幫助程式為資料欄位產生 UI 元素。 有關 MVC 幫助程式的更多信息,請參閱使用 HTML 幫助程式渲染表單 (本文適用於 MVC 3,但仍與 MVC 5 相關)。
可以在 ASP.NET 資料存取 - 推薦資源中找到其他 EF 6 資源的連結。
下一步
在本教學課程中,您已:
- 建立了詳細資訊頁面
- 更新 [建立] 頁面
- 更新了 HttpPost 編輯方法
- 更新 [刪除] 頁面
- 關閉資料庫連線
- 已處理的交易
繼續閱讀下一篇文章,了解如何為項目添加排序、篩選和分頁。