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/advanced-entity-framework-scenarios-for-an-mvc-web-application
全文目錄:Contoso 大學 - 使用 EF Code First 創建 MVC 應用
在上一個教程中,你已經實現了倉儲和工作單元模式。這個教程涵蓋下列主題:
- 執行原始的 SQL 查詢
- 執行沒有跟蹤的查詢
- 檢查發送到數據庫的查詢
- 使用代理類
- 禁用修改的自動檢測
- 在保存修改時禁用驗證
多數內容使用你已經創建的頁面。為了使用原始的 SQL 進行批更新,你需要創建一個新的更新數據庫中所有課程學分的頁面。
以及在 Department 編輯頁面中增加新的驗證邏輯,使用非跟蹤的查詢。
10-1 執行原始的 SQL 查詢
在 EF 中包含有允許你直接將 SQL 命令發送到數據庫的 API 方法,你有以下選擇:
- 使用 DbSet.SqlQuery 方法進行查詢以返回實體類型。返回的對象類型必須是 DbSet 對象期望的類型。除非你關掉追蹤,數據庫上下文將自動追蹤。( 查看隨后關於 AsNoTracking 方法 )
- 使用 DbDatabase.SqlQuery 方法進行查詢,返回類型不是實體。返回的數據不會被數據庫上下文追蹤,即使使用這個方法獲取實體類型。
- 使用 DbDatabase.SqlCommand 用於非查詢命令。
使用 EF 的好處是使你不用過分關注處理存儲數據的特定方法。它通過自動生成需要的查詢命令來完成,這樣你就不用親自編寫這些代碼。但是,一個例外就是你可能需要執行手動創建的特殊命令,這些方法可以為你處理類似的意外場景。
在 Web 中執行 SQL 命令有一點總是對的,你必須注意保護你的站點免於 SQL 注入攻擊。一種方法就是使用參數化查詢來防止提交的命令沒有被注入。在這個教程中,在使用用戶的輸入生成查詢的時候將會使用參數化查詢。
10-1-1 調用返回實體的查詢
假設你希望 GenericRepository 類需要提供一個額外的過濾和排序靈活方法,而不需要你創建一個派生類,然后增加一個額外的方法。一種方法就是增加一個接受原始 SQL 命令的方法。這樣你就可以在控制其中指定任何類型的過濾和排序處理。比如 Where 子句依賴於連接或者子查詢。 在這一節中,你將要看到如何實現這樣的方法。
通過將如下代碼增加到 GenericRepository.cs 中, 創建 GetWithRawSql 方法
public virtual IEnumerable<TEntity> GetWithRawSql(string query, params object[] parameters) { return dbSet.SqlQuery(query, parameters).ToList(); }
在 CourseController.cs 中,在 Details 方法中調用新的方法,如下所示:
public ActionResult Details(int id) { var query = "SELECT * FROM Course WHERE CourseID = @p0"; return View(unitOfWork.CourseRepository.GetWithRawSql(query, id).Single()); }
在這里,你可以使用 GetByID 方法,但是,通過調用 GetWithSql 方法來驗證 GetWithRawSQL 方法正常工作。
運行 Details 頁面,驗證選擇查詢正確工作 ( 選擇 Course 窗格,然后選擇某個課程的 Details )
10-1-2 調用查詢返回其它類型的對象
早前的時候,你創建過一個關於頁面,其中顯示了每個注冊日注冊學生的數量。位於 HomeController.cs 中的代碼使用 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 方法中的 LINQ。
var query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount " + "FROM Person " + "WHERE EnrollmentDate IS NOT NULL " + "GROUP BY EnrollmentDate"; var data = db.Database.SqlQuery<EnrollmentDateGroup>(query);
運行 About 頁面,它會像從前一樣顯示出來。
10-1-3 調用更新查詢
假設 Contoso 大學的管理員希望能夠在數據庫中執行批量更新,例如修改每門課程的學分。如果大學有大量的課程,就意味着需要低效率地獲取全部的課程實體,然后一個一個修改,在這一節中,你將要實現一個 Web 頁面,允許用戶設定所有課程的學分,通過執行一個 SQL 的更新 UPDATE 語句完成。這個頁面如下圖所示。
在 Course 控制器中,以前你已經使用泛型的倉儲來讀取和更新課程實體。這與這次的批更新處理,需要創建一個不在泛型倉儲中的新倉儲方法。為了達到這個目的,你需要創建一個派生自 GenericRepository 基類的派生類 CourseRepository 。
在 DAL 文件夾中,創建 CourseRepository.cs ,使用下面的代碼替換生成的代碼。
using System; using ContosoUniversity.Models; namespace ContosoUniversity.DAL { public class CourseRepository : GenericRepository<Course> { public CourseRepository(SchoolContext context) : base(context) { } public int UpdateCourseCredits(int multiplier) { return context.Database.ExecuteSqlCommand("UPDATE Course SET Credits = Credits * {0}", multiplier); } } }
在 UnitOfWork.cs 中,將 Course 倉儲類型從 GenericRepository< Course > 修改為 CourseRepository:
private CourseRepository courseRepository;
public CourseRepository CourseRepository { get { if (this.courseRepository == null) { this.courseRepository = new CourseRepository(context); } return courseRepository; } }
在 CourseController.cs 中,增加 UpdateCourseCredits 方法
public ActionResult UpdateCourseCredits(int? multiplier) { if (multiplier != null) { ViewBag.RowsAffected = unitOfWork.CourseRepository.UpdateCourseCredits(multiplier.Value); } return View(); }
這個方法將會同時用於 HttpGet 和 HttpPost 。當調用 GET 方式的 UpdateCourseCredits 方法的時候,變量 multiplier 會是 null,視圖將會顯示空的文本框和提交按鈕,如前所示。
當 Update 按鈕被點擊之后,將會以 POST 方式調用方法。Multiplier 將會得到在文本框中輸入的值。代碼調用倉儲的 UpdateCourseCredits 方法,方法返回受到影響的行數,這個值存儲在 ViewBag 對象中。視圖通過 ViewBag 獲取受到影響的行數,它將顯示這個數字而不是文本框和提交按鈕,如下圖所示。
在 View\Course 文件夾中創建一個用來更新課程學分的視圖。
在 View\Course\UpdateCourseCredits.cshtml 中,使用下面的代碼替換原有代碼。
@model ContosoUniversity.Models.Course @{ ViewBag.Title = "UpdateCourseCredits"; } <h2>Update Course Credits</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="submit" value="Update" /> </p> } } @if (ViewBag.RowsAffected != null) { <p> Number of rows updated: @ViewBag.RowsAffected </p> } <div> @Html.ActionLink("Back to List", "Index") </div>
通過選擇 Course 窗格運行頁面,然后增加 “/UpdateCourseCredits” 到瀏覽器地址欄中,( 例如:http://localhost:50205/Course/UpdateCourseCredits )。在文本框中輸入一個數字。
點擊 Update,你會看到受影響的行數。
點擊 Back to List 查看修訂之后的課程學分列表。
關於執行原始查詢的更多內容,請查閱 EF 團隊博客的 Raw SQL Queries
10-2 非追蹤的查詢
當數據庫上下文從數據庫獲取數據行然后表示為實體的時候,默認情況下,會保持對內存中實體對象是否與數據庫中數據同步的追蹤。內存中的數據作為緩存,當你更新實體的時候被用來更新。這個緩存在 Web 應用程序中通常沒有必要,因為上下文對象的實例生命期很短 ( 對於每一次請求創建一個新的,然后釋放 ) ,在實體被再次使用之前數據庫上下文讀取的實體對象已經被釋放了。
你可以通過 AsNoTracking 方法來指定上下文對象是否追蹤實體對象。使用這個方法常見的場景如下:
- 對於查詢大量數據的查詢來說,關閉追蹤可以提高性能。
- 你更希望重新連接一個對象用來更新,盡管基於不同的目的以前獲取過同樣的對象。由於數據庫上下文已經追蹤了這個實體,你就不能連接你希望修改的實體。防止出現這種情況的一種方式就是在原來的查詢中使用 AsNoTracking 選項。
在這一節,你將要實現演示第二種場景的業務邏輯。具體來說,希望確認強制的業務規則:一個教師不能成為多個系的管理員。
在 DepartmentController.cs 中,增加一個可以在 Edit 和 Create 方法中調用的方法,確認兩個系不能有同一個的管理員。
private void ValidateOneAdministratorAssignmentPerInstructor(Department department) { if (department.PersonID != null) { var duplicateDepartment = db.Departments .Include("Administrator") .Where(d => d.PersonID == department.PersonID) .FirstOrDefault(); if (duplicateDepartment != null && duplicateDepartment.DepartmentID != department.DepartmentID) { var errorMessage = String.Format( "Instructor {0} {1} is already administrator of the {2} department.", duplicateDepartment.Administrator.FirstMidName, duplicateDepartment.Administrator.LastName, duplicateDepartment.Name); ModelState.AddModelError(string.Empty, errorMessage); } } }
增加在 HttpPost Edit 方法中的 try 代碼塊,如果經過驗證的話,調用新的方法。Try 代碼塊如下所示。
if (ModelState.IsValid) { ValidateOneAdministratorAssignmentPerInstructor(department); } if (ModelState.IsValid) { db.Entry(department).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); }
運行 Department 的 Edit 頁面,試着將已經是某個系管理員的教師設置為其他系的管理員。你會得到期望中的錯誤信息。
再次運行 Department 的 Edit 頁面,這一次修改 Budget ,當點擊 Save 的時候,你會看到如下信息。
異常提示信息是 “An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.” 這是因為順序發生了一下事件:
- Edit 方法調用 ValidateOneAdministratorAssignmentPerInstructor 方法,這導致獲取了 Kim Abercrombie 作為管理員的所有 Department 。結果是名為 English 的 Department 被讀取到內存中。因為這個 Department 被編輯了,不會有錯誤報告,作為這次讀取操作的結果,然而,從數據庫讀取的 English Department 實體已經被數據庫上下文跟蹤了。
- Edit 方法試圖設置通過模型綁定得到的 English Department 實體的 Modified 標志,但是失敗了,因為數據庫上下文已經跟蹤了 English 實體。
解決這個問題的一個方案是保持數據庫上下文,從內存中獲取跟蹤的 Department 實體。這樣做沒有什么好處,因為並不需要更新這個實體或者通過從內存中再次讀取這個實體來獲取好處。
在 DepartmentController.cs 文件的 ValidateOneAdministratorAssignmentPerInstructor 方法中,指定不需要跟蹤,如下所示:
var duplicateDepartment = db.Departments .Include("Administrator") .Where(d => d.PersonID == department.PersonID) .AsNoTracking() .FirstOrDefault();
重新編輯Department 的 Budget 。這次操作成功了。站點返回了預期的 Department Index 頁面,顯示了修改之后的 Budget 值。
10-3 檢查發送到數據庫的查詢
有的時候,能夠看到實際發送到數據庫的 SQL 很有幫助,如果希望的話,可以通過在調試器中檢查查詢變量或者調用查詢的 ToString 方法。嘗試一下,對一個簡單的查詢增加一些選項,例如預先加載,過濾和排序等等,看看發生了什么。
在 Controller/CourseController.cs 中,使用下面的代碼替換 Index 方法。
public ViewResult Index() { var courses = unitOfWork.CourseRepository.Get(); return View(courses.ToList()); }
現在,在 GenericRepository.cs 文件中GET 方法的 return query.ToList() 方法調用語句以及 orderBy( query ).ToList() 語句上增加一個斷點,在調試模式運行項目,選擇 Course 的 Index 頁面。當執行到斷點的時候,檢查 query 變量。你就會看到發送到數據庫的 SQL 語句。是簡單的 Select 語句。
{SELECT
[Extent1].[CourseID] AS [CourseID],
[Extent1].[Title] AS [Title],
[Extent1].[Credits] AS [Credits],
[Extent1].[DepartmentID] AS [DepartmentID]
FROM [Course] AS [Extent1]}
在 Visual Studio 的調試器窗口中,查詢語句可能很長,不便於顯示出來。為了查看完整的查詢,你可以復制變量的值粘貼到文本編輯器中。
現在你需要為 Course 的 Index 頁面上增加一個下拉列表,用戶可以用來過濾特定的 Department。通過標題可以進行排序,對 Department 導航屬性使用貪婪加載,在 CourseController.cs 文件中,使用下面的代碼替換原有內容。
public ActionResult Index(int? SelectedDepartment) { var departments = unitOfWork.DepartmentRepository.Get( orderBy: q => q.OrderBy(d => d.Name)); ViewBag.SelectedDepartment = new SelectList(departments, "DepartmentID", "Name", SelectedDepartment); int departmentID = SelectedDepartment.GetValueOrDefault(); return View(unitOfWork.CourseRepository.Get( filter: d => !SelectedDepartment.HasValue || d.DepartmentID == departmentID, orderBy: q => q.OrderBy(d => d.CourseID), includeProperties: "Department")); }
這個方法通過 SelectedDepartment 參數獲取下拉列表中的選中的值。如果沒有任何項目選中,參數為 null。
SelectList 集合中包含傳遞到視圖的下拉列表中的 Department。傳遞給 SelectList 構造器的參數設置了字段名稱,文本域名稱,以及選中的項目。
對於 Course 倉儲的 Get 方法來說,代碼設置了過濾表達式,排序順序,預先加載 Department 導航屬性。如果下拉列表沒有選中的話,過濾表達式總是返回 true ( 也就是說 SelectedDepartment 是 null )。
在 View\Course\Index.cshtml 中,在 table 開始標記之前,增加如下的代碼創建下拉列表和提交按鈕。
@using (Html.BeginForm()) { <p>Select Department: @Html.DropDownList("SelectedDepartment","All") <input type="submit" value="Filter" /></p> }
由於在 GenericRepository 類的斷點仍然有效,運行 Course 的 Index 頁面,重復上次的操作以命中斷點,在瀏覽器顯示頁面之后,從下拉列表中選中一個 Department,然后點擊 Filter。
這一次第一個斷點出現在下拉列表查詢 Department 的時候,跳過之后,在下次代碼到達斷點的時候,查看 query 變量中的 Course 查詢,你應該看到類似如下的查詢語句。
{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].[PersonID] AS [PersonID], [Extent2].[Timestamp] AS [Timestamp] FROM [Course] AS [Extent1] INNER JOIN [Department] AS [Extent2] ON [Extent1].[DepartmentID] = [Extent2].[DepartmentID] WHERE (@p__linq__0 IS NULL) OR ([Extent1].[DepartmentID] = @p__linq__1)}
你會看到現在是聯合查詢,通過Where 子句,依據 Course 數據加載了 Department 數據。
10.4 使用代理類
當 EF 創建實體對象的時候 ( 例如,在執行查詢的時候 ),EF 經常會創建動態生成的派生自實體的代理對象。代理重寫了實體的虛擬屬性來插入在訪問屬性的時候自動執行的鈎子。例如,這種機制用來支持延遲加載或者關聯。
多數時候你並不能察覺使用了代理,除了下面的情況之外:
- 有一些場景,你需要阻止 EF 創建代理實例。例如,序列化非代理的對象實例可能比序列化代理對象實例更加有效。
- 當使用 new 操作符實例化實體的時候,你並沒有得到代理實例。這意味着你不能獲得諸如延遲加載以及自動追蹤的能力。一般沒有問題,通常並不需要延遲加載,因為你在創建數據庫中並不存在的實體。在將實體標記為 Added 狀態的時候,也不需要追蹤。然而,如果你需要延遲加載,需要改變追蹤,就可以通過 DbSet 類的 Create 方法來創建實體代理對象。
- 可能需要通過代理類型對象實例獲取真實實體類型。可以使用 ObjectContext 類的 GetObjectType 方法來通過代理對象獲取實際對象。
更多信息,參看 EF 團隊博客的 Working with Proxies
10.5 禁用變化自動跟蹤
EF 通過比較實體的當前值與原始值來決定實體如何變化 ( 依此來決定發送到數據庫的更新 )。在查詢或者連接的時候原始值被保存起來。導致自動變化跟蹤監測的一些方法如下:
- DbSet.Find
- DbSet.Local
- DbSet.Remove
- DbSet.Add
- DbSet.Attach
- DbContext.SaveChanges
- DbContext.GetValidationErrors
- DbContext.Entry
- DbChangeTracker.Entries
如果你跟蹤大量的實體,而且在循環中多次調用上面提到的方法,通過使用 AutoDetectChangesEnabled 屬性暫時關閉變化檢測,可以得到顯著的性能提升,更多信息,參見 EF 團隊博客的 Automatically Detecting Changes。
10-6 在保存的時候禁用驗證
在調用 SaveChanges 方法的時候,在更新到數據庫之前,EF 驗證所有被修改實體的數據的所有屬性。如果你更新了大量的實體,而且你已經驗證了數據,這些工作就是不必要的,通過 ValidateOnSaveEntity 屬性臨時關閉驗證可以花費更要的時間保存修改。更多信息,參見 EF 團隊博客的 Validation。
10-7 EF 資源鏈接
更多地 EF 資源,參見如下資源:
- Introduction to the Entity Framework 4.1 (Code First)
- The Entity Framework Code First Class Library API Reference
- Entity Framework FAQ
- The Entity Framework Team Blog
- Entity Framework in the MSDN Library
- Entity Framework in the MSDN Data Developer Center
- Entity Framework Forums on MSDN
- Julie Lerman's blog
- Code First DataAnnotations Attributes
- Maximizing Performance with the Entity Framework in an ASP.NET Web Application
- Profiling Database Activity in the Entity Framework
- Entity Framework Power Tools
下面的 EF 團隊的博客提供了教程涉及的更多資源。
- Fluent API Samples. How to customize mapping using fluent API method calls.
- Connections and Models. How to connect to different types of databases.
- Pluggable Conventions. How to change conventions.
- Finding Entities. How to use the
Find
method with composite keys. - Loading Related Entities. Additional options for eager, lazy, and explicit loading.
- Load and AsNoTracking. More on explicit loading.
一些這里提到的博客是關於 CTP 版本的 EF Code First,多數資料是准確的,但是在正式發布版本中會有一些變化。
在 EF 中使用 LINQ 的信息,參見 MSDN 的 LINQ to Entities
使用 MVC 和 EF 的更多信息,參見 MVC Music Store.
在項目創建之后,如何發布的問題,參見 MSDN 的 ASP.NET Deployment Content