這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這里是第十二篇:為ASP.NET MVC應用程序使用高級功能
原文:Advanced Entity Framework 6 Scenarios for an MVC 5 Web Application
譯文版權所有,謝絕全文轉載——但您可以在您的網站上添加到該教程的鏈接。
在之前的教程中,您已經實現了繼承。本教程引入了當你在使用實體框架Code First來開發ASP.NET web應用程序時可以利用的高級功能。
對於本教程中所介紹的大多數主題,您將使用您已經創建的網頁,使用原始的SQL進行批量更新。然后您將創建一個新的頁面來更新數據庫中所有課程的學分。
以及使用非跟蹤的查詢,你將在系編輯頁面添加一個新的驗證邏輯。
執行原始的SQL查詢
實體框架Code First API包含的方法使您可以直接發送SQL命令到數據庫中。您有以下幾種選擇:
- 使用DbSet.SqlQuery方法來進行查詢並返回實體類型。返回的對象類型必須是預期的DbSet對象,它們會由數據庫上下文自動跟蹤。除非您關閉跟蹤。(參見下一節的AsNoTracking方法)
- 使用Database.SqlQuery方法來進行查詢並返回非實體類型。返回的對象不會被數據庫上下文跟蹤,即使您使用該方法來檢索實體類型。
- Database.ExecuteSqlCommand用於非查詢類型的命令。
使用實體框架的優點之一是它可以讓你無需手工輸入大量代碼來實現存取數據的特定方法。通過自動生成SQL查詢及命令,將你從繁瑣的手工編碼中解放出來。但在特殊情況下,您可能需要執行手工創建的特定的SQL查詢,這些方法能夠實現這一功能並為你提供異常處理。
當你經常性地在web應用程序中執行SQL命令時,你必須采取必要的預防措施來保護你的站點不受SQL注入攻擊。其中的一個辦法就是使用參數化的查詢,確保來自web頁的的字符串不會被解釋為SQL命令。在本教程中,當您使用用戶輸入查詢時,您將使用參數化的查詢。
調用一個查詢來返回實體
DbSet<TEntity>類提供了一個方法,您可以使用該方法來執行一個查詢並返回一個實體類型。要觀察該方法是如何工作的,你需要對Department控制器中的Details方法進行一些更改。
在DepartmentController.cs中,使用下面的代碼替換Details方法,高亮部分顯示了需要進行的更改:
public async Task<ActionResult> Details(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } //Department department = await db.Departments.FindAsync(id); string query = "select * from department where departmentid = @p0"; Department department = await db.Departments.SqlQuery(query, id).SingleOrDefaultAsync(); if (department == null) { return HttpNotFound(); } return View(department); }
要驗證新代碼是否工作正常,請運行應用程序,轉到系頁面並點擊某個系的詳情。
你可以看到一切如之前一樣正常工作。
調用一個查詢來返回其他類型的對象
在較早的教程中您創建了一個學生統計網格用來顯示每個注冊日期中注冊的學生數目。這段代碼使用了LINQ來進行操作:
var data = from student in db.Students group student by student.EnrollmentDate into dateGroup select new EnrollmentDateGroup() { EnrollmentDate = dateGroup.Key, StudentCount = dateGroup.Count() };
假設您要直接編寫SQL代碼來進行該項查詢而不是使用LINQ,您需要運行一個查詢以返回實體類型以外的對象,這意味着您需要使用Database.SqlQuery方法。
在HomeController.cs中,使用下面的代碼替換About方法,高亮部分顯示了需要進行的更改:
public ActionResult About() { //var data = from student in db.Students // group student by student.EnrollmentDate into dateGroup // select new EnrollmentDateGroup() // { // EnrollmentDate = dateGroup.Key, // StudentCount = dateGroup.Count() // }; string query = "select EnrollmentDate,count(*) as studentCount " + "From Person " + "where discriminator = 'Student' " + "group by EnrollmentDate "; var data = db.Database.SqlQuery<EnrollmentDateGroup>(query); return View(data.ToList()); }
運行頁面,它會顯示和之前一樣的數據。
調用更新查詢
假設管理員想要能夠在數據庫進行批量操作,例如為每一門課程更改學分。如果學校有大量的課程,針對每一門課程分別進行更新無疑是效率非常低下的做法。在本節中你會實現一個web頁面使用戶能夠修改全部課程的學分,通過使用SQL Update語句來進行這一更改,如下圖:
在CourseController.cs,添加HttpGet和HttpPost的UpdateCourseCredits方法:
public ActionResult UpdateCourseCredits() { return View(); } [HttpPost] public ActionResult UpdateCourseCredits(int ? multiplier) { if (multiplier != null) { ViewBag.RowsAffected = db.Database.ExecuteSqlCommand("update course Set Credits = Credits * {0}", multiplier); } return View(); }
當控制器處理HttpGet請求時,ViewBag.RowsAffected將不返回任何值。視圖將顯示一個空的文本框及提交按鈕。
當點擊更新按鈕時,調用HttpPost方法,獲取在文本框中輸入的值,代碼執行SQL來更新課程並在ViewBag.RowsAffected中返回受影響的行數。當視圖獲取該變量的值,它將顯示一條信息來說明已經更新的課程數目,而不是文本框和提交按鈕,如下圖所示:
在CourseController.cs,右鍵點擊UpdateCourseCredits方法,然后添加一個視圖:
使用下面的代碼替換視圖中的:
@model ContosoUniversity.Models.Course @{ ViewBag.Title = "UpdateCourseCredits"; } <h2>UpdateCourseCredits</h2> @if (ViewBag.RowsAffected == null) { using (Html.BeginForm()) { <p> Enter a number to multiply every course's credits by: @Html.TextBox("multiplier") </p> <p><input type="su" name="name" value="Update" /></p> } } @if (ViewBag.RowsAffected != null) { <p> Number of rows updated: @ViewBag.RowsAffected </p> } <div> @Html.ActionLink("Back to List", "Index") </div>
運行應用程序,添加"/UpdateCourseCredits"到瀏覽器地址欄中的末尾,如Http://localhost:40675/UpdateCourseCredits,打開頁面,並在文本框中輸入一個數字:
點擊更新,你會看到受影響的課程:
然后返回列表,你會看到所有課程都進行了更新:
有關更多使用原始SQL查詢的信息,請參閱MSDN上的Raw SQL Queries。
非跟蹤查詢
當數據庫上下文檢索數據行並創建實體對象時,默認情況下它會跟蹤內存中的實體是否與數據庫中的同步。當您更新一個實體時,內存中的數據作為緩存。這種緩存在web應用程序中經常是不可用的,因為上下文實例通常是短生命期的(每個請求都會創建一個新實例),並且上下文經常在讀取過實體並使用后就將它們銷毀了。
您可以使用AsNoTracking方法來來禁用跟蹤內存中的實體對象。在以下幾種典型場景中,你可能需要這樣做:
- 需要檢索大量的數據,而關閉跟蹤可能會顯著提高性能。
- 您需要附加一個實體來更新它,但它是之前基於不同的目的獲取的同一個實體對象。因為該實體已經被數據庫的上下文跟蹤,你無法附加該實體以進行更改。這種情況下,你需要對較早的查詢使用AsNoTracking選項。
在本節中你會實現上面第二個方案的業務邏輯。具體來說,你會強制執行一名教師不能在多個系中擔任主任的規則。
在DepartmentController.cs,添加一個新方法,使您可以從Edit和Create方法來調用它以確保沒有兩個系有相同的主任:
private void ValidateOneAdministratorAssignmentPerInstructor(Department department) { if (department.InstructorID != null) { Department duplicateDepartment = db.Departments .Include("Administrator") .Where(d => d.InstructorID == department.InstructorID) .FirstOrDefault(); if (duplicateDepartment != null && duplicateDepartment.DepartmentID != department.DepartmentID) { string errorMessage = string.Format( "教師{0} {1}已經是{2}系的主任。", duplicateDepartment.Administrator.FirstMidName, duplicateDepartment.Administrator.LastName, duplicateDepartment.Name); ModelState.AddModelError(string.Empty, errorMessage); } } }
在HttpPost的Edit方法中的try代碼塊中調用該方法來驗證,如下面的代碼:
[HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> Edit([Bind(Include = "DepartmentID,Name,Budget,StartDate,RowVersion,InstructorID")] Department department) { try { if (ModelState.IsValid) { ValidateOneAdministratorAssignmentPerInstructor(department); } if (ModelState.IsValid) { db.Entry(department).State = EntityState.Modified; await db.SaveChangesAsync(); return RedirectToAction("Index"); } } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var clientValues = (Department)entry.Entity; var databaseEntry = entry.GetDatabaseValues(); if (databaseEntry == null) { ModelState.AddModelError(string.Empty, "無法保存更改,系已經被其他用戶刪除。"); } else { var databaseValues = (Department)databaseEntry.ToObject(); if (databaseValues.Name != clientValues.Name) ModelState.AddModelError("Name", "當前值: " + databaseValues.Name); if (databaseValues.Budget != clientValues.Budget) ModelState.AddModelError("Budget", "當前值: " + String.Format("{0:c}", databaseValues.Budget)); if (databaseValues.StartDate != clientValues.StartDate) ModelState.AddModelError("StartDate", "當前值: " + String.Format("{0:d}", databaseValues.StartDate)); if (databaseValues.InstructorID != clientValues.InstructorID) ModelState.AddModelError("InstructorID", "當前值: " + db.Instructors.Find(databaseValues.InstructorID).FullName); ModelState.AddModelError(string.Empty, "當前記錄已經被其他人更改。如果你仍然想要保存這些數據," + "重新點擊保存按鈕或者點擊返回列表撤銷本次操作。"); department.RowVersion = databaseValues.RowVersion; } } catch (RetryLimitExceededException) { ModelState.AddModelError(string.Empty, "無法保存更改,請重試或聯系管理員。"); } ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", department.InstructorID); return View(department); }
運行系編輯頁面,嘗試將已經是系主任的教師更改為另一個系的主任,你會收到預期的錯誤消息:
再次運行系編輯頁面,更改預算金額並保存,您會看到一個錯誤:
該錯誤的出現是以下原因導致的:
- 該Edit方法調用了ValidateOneAdministratorAssignmentPerInstructor方法,用來在全部系中檢索系主任。這會導致要編輯的系被讀取,由於此讀取操作,該系實體正在被數據庫上下文跟蹤。
- Edit方法嘗試設置由模型綁定器創建的該實體的標志位為已修改的,並使用上下文隱式地嘗試附加該實體。但上下文無法附加該實體,因為它已經被上下文跟蹤了。
解決這一問題的一個辦法是保持內存中用於驗證查詢的跟蹤系實體的上下文,但這樣做沒有必要,因為你不需要更新該實體,或者重新從內存中讀取它,但這樣不會帶來任何好處。
在驗證方法中,指定不跟蹤,如下面的代碼所示:
Department duplicateDepartment = db.Departments .Include("Administrator") .Where(d => d.InstructorID == department.InstructorID) .AsNoTracking() .FirstOrDefault();
重復之前的操作,這一次更新被成功保存。
檢查發送到數據庫的SQL
有時候,查看實際被發送到數據庫的SQL查詢是很有幫助的,在較早的教程中,您看到了如何使用攔截器代碼來執行這一工作,現在你將看到如何不使用攔截器的方法。要嘗試該方法,你會檢查一個簡單查詢並觀察添加比如預先加載、過濾及排序,看看到底發生了什么。
在CourseController.cs,使用下面的代碼替換原先的,以停止預先加載。
public ActionResult Index() { var courses = db.Courses; var sql = courses.ToString(); return View(courses.ToList()); }
然后在return語句上設置一個斷點,並按下F5在調試模式下運行該項目,選擇課程索引頁,當代碼到達斷點時,檢查query變量,你將看到被發送的SQL的查詢,它是一個簡單的select語句。
你可以在監視窗口中使用文本可視化工具來檢視SQL。
現在將一個下拉列表添加到課程索引頁面,用戶可以用來篩選特定的系。你會使用標題來進行排序,並指定系導航屬性的預先加載。
在CourseController.cs,使用下面的代碼替換Index方法:
public ActionResult Index(int? SelectedDepartment) { var departments = db.Departments.OrderBy(q => q.Name).ToList(); ViewBag.SelectedDepartment = new SelectList(departments, "DepartmentID", "Name", SelectedDepartment); int departmentID = SelectedDepartment.GetValueOrDefault(); IQueryable<Course> courses = db.Courses .Where(c => !SelectedDepartment.HasValue || c.DepartmentID == departmentID) .OrderBy(d => d.CourseID) .Include(d => d.Department); var sql = courses.ToString(); return View(courses.ToList()); }
仍然在return上設置斷點。
該方法接收下拉列表中選擇的值,如果沒有任何項目被選擇,該參數為null。
一個包含所有系的SelectList集合被傳遞給視圖的下拉列表。傳遞給SelectList的構造器的參數指定了值字段名,文本字段名和所選擇的項目。
對於課程倉庫的Get方法,代碼指定了Department導航屬性的篩選器表達式,一個排序和延遲加載。如果下拉下表中沒有選擇任何項,篩選表達式總是返回true。
在Views\Course\Index.cshtml中,在table開始標記之前,插入下面的代碼來創建下拉列表和提交按鈕。
@using (Html.BeginForm()) { <p>選擇系:@Html.DropDownList("SelectedDepartment", "All")</p> <input type="submit" name="name" value="篩選" /> }
運行索引頁,在一次遇到斷點時繼續運行以便顯示頁面,從下拉列表中選擇一個系並點擊篩選:
按照剛才的方法查看SQL語句,你會看到一個包含內連接查詢的SQL。
SELECT [Project1].[CourseID] AS [CourseID], [Project1].[Title] AS [Title], [Project1].[Credits] AS [Credits], [Project1].[DepartmentID] AS [DepartmentID], [Project1].[DepartmentID1] AS [DepartmentID1], [Project1].[Name] AS [Name], [Project1].[Budget] AS [Budget], [Project1].[StartDate] AS [StartDate], [Project1].[InstructorID] AS [InstructorID], [Project1].[RowVersion] AS [RowVersion] FROM ( SELECT [Extent1].[CourseID] AS [CourseID], [Extent1].[Title] AS [Title], [Extent1].[Credits] AS [Credits], [Extent1].[DepartmentID] AS [DepartmentID], [Extent2].[DepartmentID] AS [DepartmentID1], [Extent2].[Name] AS [Name], [Extent2].[Budget] AS [Budget], [Extent2].[StartDate] AS [StartDate], [Extent2].[InstructorID] AS [InstructorID], [Extent2].[RowVersion] AS [RowVersion] FROM [dbo].[Course] AS [Extent1] INNER JOIN [dbo].[Department] AS [Extent2] ON [Extent1].[DepartmentID] = [Extent2].[DepartmentID] WHERE @p__linq__0 IS NULL OR [Extent1].[DepartmentID] = @p__linq__1 ) AS [Project1] ORDER BY [Project1].[CourseID] ASC
刪除代碼中的var sql = conrses.ToString();
倉儲和單元工作模式
許多開發人員編寫代碼作為包裝來實現實體框架的倉儲和單元工作模式。這些模式在商業邏輯層和數據存取層之間創建了一個抽象層。實施這些模式可以幫助你的應用程序從數據存儲的改變中隔離出來,並且促進自動化的單元測試開發。但是,對使用實體框架的程序編寫額外的代碼來實現這些模式並不是最佳的選擇,有以下幾個原因:
- 實體框架上下文類本身就可以將你的代碼從特定代碼的數據存儲中隔離。
- 當你使用實體框架時,對於數據庫更新操作實體框架上下文類可以作為一個工作單元類。
- 在實體框架6版本中引入的功能使它在無需編寫倉儲代碼的情況下來實現單元測試驅動。
有關如何執行倉儲及單元工作模式的詳細信息,請參閱the Entity Framework 5 version of this tutorial series。有關如何在實體框架6版本中執行單元測試驅動,請參閱:
- How EF6 Enables Mocking DbSets more easily
- Testing with a mocking framework
- Testing with your own test doubles
代理類
在實體框架創建實體實例時(例如當你執行一個查詢時),它總是創建作為動態生成的派生自實體的實體對象的代理。例如下面的兩個調試器截圖,在第一個圖像中,您看到了一個預期為Student類型的student變量,在實例化實體后,第二個圖像中你會看到該代理類。
代理類重寫了實體的一些虛屬性用來插入在訪問屬性時自動執行動作的鈎子。其中一個使用這種機制就功能就是延遲加載。
大多數時候你並不會察覺到代理,但也有例外:
- 某些情況下,你可能想要阻止實體框架創建代理實例。例如,通常你希望對一個POCO類的實體進行序列化,而不是代理類。一種避免序列化問題的方法是序列化數據傳輸對象(DTOs)而不是實體對象,比如Using Web API with Entity Framework。另一種方法就是disable proxy creation。
- 當你使用new運算符實例化一個實體類時,你得到的不是代理實例。這意味着你無法獲得諸如延遲加載和自動跟蹤的能力。這通常是好的:你一般不需要延遲加載,因為你需要創建一個並不在數據庫中存在的新的實體,當你顯式地將實體標記為Added時,你通常不需要修改跟蹤。然而,如果你需要延遲加載,你需要更改跟蹤,你可以通過使用DbSet類的Create方法通過代理來創建一個新實體對象。
- 你可能會想要從一種代理類型獲得一個真是的實體類型。ObjectContext類的GetObjectType方法可以用於獲得代理類型的實際實體類型。
更多的信息,請參閱MSDN上的Working with Proxies。
自動變化監測
實體框架使用比較實體的當前值和原始值來確定一個實體是否被更改(以及因此而需要發送到數據庫執行的更新)。實體在查詢或附加時,原始值被保存起來。一些會導致自動變化監測的方法如下:
- DbSet.Find
- DbSet.Local
- DbSet.Remove
- DbSet.Add
- DbSet.Attach
- DbContext.Savechanges
- DbContext.GetValidationErrors
- DbContext.Entry
- DbChangeTracker.Entries
如果您正在跟蹤大量實體,同時您在一個循環中調用了這些方法多次,您可能會通過使用AutoDetectChangesEnabled屬性來暫時關閉自動變化監測,從而獲得程序性能的改進。
自動驗證
當您調用SaveChanges方法時,在默認情況下,實體框架會在更新到數據庫之前對所有已更改的實體中的全部屬性進行驗證。如果您更新了大量的實體並且已經對數據進行了驗證,該工作是不必要的,你可以通過暫時關閉驗證來獲得更少的處理保存時間。你可以使用ValidateOnSaveEnabled屬性。
Entity Framework Power Tools
Entity Framework Power Tools是一個簡單的VS擴展,你可以使用它來創建本教程中展示的數據模型圖。該工具還可以做其他一些工作比如當你使用Code First時基於現有數據庫的表來生成實體類。安裝該工具后,你會在上下文菜單看到一些附加選項,例如,當你右鍵單擊解決方案資源管理器的上下文類,你會得到一個選項來生成一個圖表。當你使用Code First時無法修改關系圖中的數據模型,但你可以移動圖示使它更容易理解。
實體框架的源代碼
你可以在http://entityframework.codeplex.com/獲得實體框架6的源代碼,除了源代碼,你可以生成、跟蹤問題、探查功能等更多,你可以提交bug並貢獻你自己的增強功能給實體框架源代碼。
雖然源代碼是開放的,但實體框架是由微軟完全支持的產品。微軟實體框架團隊會不斷地接收反饋及測試更改,以確保每個版本的質量。
總結
這樣,在ASP.NET MVC應用程序中使用實體框架這一系列教程就全部完成了。有關如何使用實體框架的更多信息,請參閱EF documentation page on MSDN和ASP.NET Data Access - Recommended Resources。
有關如何在你建立應用程序后部署它,請參閱 ASP.NET Web Deployment - Recommended Resources。
關於更多MVC的信息,請參閱 ASP.NET MVC - Recommended Resources。
致謝
- Tom Dykstra基於實體框架5編寫了本教程的原始版本,並在之基礎上編寫了該教程。他是微軟Web平台和工具團隊的高級程序員作家。
- Rick Anderson在實體框架5和MVC4教程中做了大量工作並合著了實體框架6更新,他是微軟Azure和MVC的資深程序員作家。
- Rowan Miller和其他的實體框架團隊審查該教程並調試了大量的bug。
作者信息
Tom Dykstra - Tom Dykstra是微軟Web平台及工具團隊的高級程序員,作家。