教學課程:了解 MVC 5 Web 應用程式的進階 EF 場景
在上一教學課程中,您實作了按層次結構表繼承。 本教學課程介紹了幾個主題,當您超越使用 Entity Framework Code First 開發 ASP.NET Web 應用程式的基礎知識時,需要注意這些主題。 前幾個部分包含逐步說明,引導您完成程式碼並使用 Visual Studio 完成任務。
對於大多數主題,您將使用已建立的頁面。 若要使用原始 SQL 進行批次更新,您將建立一個新頁面來更新資料庫中所有課程的學分數:
在本教學課程中,您已:
- 執行原始 SQL 查詢
- 執行非追蹤查詢
- 檢查傳送到資料庫的 SQL 查詢
您還了解:
- 建立抽象層
- 代理類
- 自動變更偵測
- 自動驗證
- 實體框架電動工具
- 實體框架原始碼
先決條件
執行原始 SQL 查詢
Entity Framework Code First API 包含讓您能夠將 SQL 指令直接傳遞到資料庫的方法。 下列選項可供您選擇:
- 對傳回實體類型的查詢使用 DbSet.SqlQuery 方法。 傳回的
DbSet
物件必須是該物件期望的類型,並且資料庫上下文會自動追蹤它們,除非您關閉追蹤。 (請參閱以下有關 AsNoTracking 方法的部分。) - 對於傳回非實體類型的查詢,請使用 Database.SqlQuery 方法。 即使您使用這個方法來擷取實體類型,資料庫內容也不會追蹤傳回的資料。
- 將 Database.ExecuteSqlCommand 用於非查詢命令。
使用 Entity Framework 的優點之一,是它可避免將程式碼繫結至太接近儲存資料之特定方法的位置。 它可透過產生 SQL 查詢和命令來達成此目的,同時這也可讓您不必自行撰寫。 但在某些特殊情況下,您需要執行手動建立的特定 SQL 查詢,這些方法可讓您處理此類例外狀況。
如同在 Web 應用程式中執行 SQL 命令一樣,您必須採取一些預防措施,以保護您的網站免於遭受 SQL 插入式攻擊。 執行這項操作的方法之一是使用參數化查詢,以確定網頁所提交的字串無法解譯為 SQL 命令。 在本教學課程中,您會在將使用者輸入整合到查詢時,使用參數化查詢。
呼叫返回實體的查詢
DbSet<TEntity> 類別提供了一種方法,可用來執行傳回類型實體的查詢。TEntity
要了解其工作原理,您將更改 Department
控制器 Details
方法中的程式碼。
在 DepartmentController.cs 中,在 db.Departments.FindAsync
方法中,Details
將方法調用替換為 db.Departments.SqlQuery
方法調用,如下面突出顯示的程式碼所示:
public async Task<ActionResult> Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
// Commenting out original code to show how to use a raw SQL query.
//Department department = await db.Departments.FindAsync(id);
// Create and execute raw SQL query.
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);
}
若要確認新的程式碼運作正常,請選取 [部門] 索引標籤,然後針對其中一個部門選取 [詳細資料] 。 確保所有資料均按預期顯示。
呼叫傳回其他類型物件的查詢
先前您已針對顯示每個註冊日期之學生數目的 About 頁面,建立學生統計資料方格。 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 語句替換為 SQL 語句,如以下所反白的程式碼所示:
public ActionResult About()
{
// Commenting out LINQ to show how to do the same thing in SQL.
//IQueryable<EnrollmentDateGroup> = from student in db.Students
// group student by student.EnrollmentDate into dateGroup
// select new EnrollmentDateGroup()
// {
// EnrollmentDate = dateGroup.Key,
// StudentCount = dateGroup.Count()
// };
// SQL version of the above LINQ code.
string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
+ "FROM Person "
+ "WHERE Discriminator = 'Student' "
+ "GROUP BY EnrollmentDate";
IEnumerable<EnrollmentDateGroup> data = db.Database.SqlQuery<EnrollmentDateGroup>(query);
return View(data.ToList());
}
運行“關於”頁面。 驗證它顯示的資料是否與之前相同。
呼叫更新查詢
假設 Contoso 大學管理員希望能夠在資料庫中執行批次更改,例如更改每門課程的學分數。 如果該大學有大量的課程,擷取全部課程作為實體並個別進行變更的效率不佳。 在本部分中,您將實作一個網頁,使用戶能夠指定一個因素來更改所有課程的學分數,並且您將透過執行 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
方法,並在文字方塊中輸入 multiplier
值。 然後,程式碼執行更新課程的 SQL,並將受影響的行數傳回 ViewBag.RowsAffected
變數中的檢視。 當檢視取得該變數中的值時,它會顯示更新的行數,而不是文字方塊和提交按鈕。
在 CourseController.cs 中,右鍵點擊其中一種 UpdateCourseCredits
方法,然後按一下新增檢視。 將出現新增檢視對話方塊。 保留預設值並選擇新增。
在 Views\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>
藉由選取 [課程] 索引標籤,然後將 "/UpdateCourseCredits" 新增至瀏覽器位址列中的 URL 結尾 (例如:http://localhost:50205/Course/UpdateCourseCredits
),以執行 UpdateCourseCredits
方法。 在文字方塊中輸入數目:
按一下更新。 您會看到受影響的行數。
按一下 [回到清單],以查看課程與已修訂學分數的清單。
有關原始 SQL 查詢的詳細信息,請參閱 MSDN 上的原始 SQL 查詢。
無追蹤查詢
當資料庫內容擷取資料表資料列並建立代表他們的實體物件時,根據預設,它會追蹤在記憶體中的實體是否與資料庫中的內容保持同步。 記憶體中的資料所扮演的角色是一個快取,並會在您更新實體時使用。 這個快取通常在 Web 應用程式當中是不需要的,因為內容執行個體通常壽命都很短 (每次要求都會建立一個新的並進行處置),並且通常讀取實體的內容都會在實體重新獲得利用前遭到處置。
您可以使用 AsNoTracking 方法停用對記憶體中實體物件的追蹤。 您會想要進行這項操作的常見案例包括下列情況:
- 查詢檢索如此大量的資料,關閉追蹤可能會顯著提高效能。
- 您想要附加一個實體以便更新它,但您之前出於不同目的檢索了相同實體。 由於實體已由資料庫內容進行追蹤,您無法連結到您想要變更的實體。 處理這種情況的一種方法是在先前的查詢中使用
AsNoTracking
選項。
有關示範如何使用 AsNoTracking 方法的範例,請參閱本教學課程的早期版本。 本版本的教學課程不會在 Edit 方法中的模型綁定程式所建立的實體上設定 Modified 標誌,因此不需要 AsNoTracking
。
檢查傳送到資料庫的 SQL
有時能夠看到傳送至資料庫的實際 SQL 查詢很有幫助。 在先前的教學課程中,您了解如何在攔截器程式碼中執行此操作;現在您將看到一些無需編寫攔截器程式碼即可完成此操作的方法。 要嘗試此操作,您將查看一個簡單的查詢,然後查看當您添加急切載入、篩選和排序等選項時會發生什麼情況。
在 Controllers/CourseController 中,將 Index
替換為以下程式碼,以暫時停止急切載入:
public ActionResult Index()
{
var courses = db.Courses;
var sql = courses.ToString();
return View(courses.ToList());
}
現在在 return
語句上設定斷點 (F9,遊標位於該行)。 按 F5 以偵錯模式執行項目,然後選擇「課程索引」頁面。 當程式碼到達斷點時,檢查 sql
變數。 您會看到傳送到 SQL Server 的查詢。 這是一個簡單的 Select
聲明。
{SELECT
[Extent1].[CourseID] AS [CourseID],
[Extent1].[Title] AS [Title],
[Extent1].[Credits] AS [Credits],
[Extent1].[DepartmentID] AS [DepartmentID]
FROM [Course] AS [Extent1]}
按一下放大鏡以在文字視覺化工具中查看查詢。
現在,您將向課程索引頁面新增一個下拉列表,以便用戶可以篩選特定部門。 您將按標題對課程進行排序,並指定 Department
導航屬性的預先載入。
在 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
語句上的斷點。
此方法接收 SelectedDepartment
參數中下拉清單的選定值。 如果未選擇任何內容,則該參數將為空。
包含所有部門的 SelectList
集合將傳遞到下拉清單的檢視。 傳遞給 SelectList
建構函數的參數指定值欄位名稱、文字欄位名稱和所選項目。
對於 Course
儲存庫的 Get
方法,程式碼指定了篩選表達式、排序順序以及 Department
導航屬性的預先載入。 如果下拉清單中未選擇任何內容 (即 SelectedDepartment
為空),則篩選表達式始終傳回 true
。
在 Views\Course\Index.cshtml 中的開始 table
標記之前,新增以下程式碼以建立下拉清單和提交按鈕:
@using (Html.BeginForm())
{
<p>Select Department: @Html.DropDownList("SelectedDepartment","All")
<input type="submit" value="Filter" /></p>
}
在仍設定斷點的情況下,執行課程索引頁面。 繼續執行程式碼第一次遇到斷點的操作,以便頁面顯示在瀏覽器中。 從下拉清單中選擇一個部門,然後按一下篩選器。
這次第一個斷點將用於部門查詢下拉式清單。 跳過該步驟並在下次程式碼到達斷點時查看 query
變數,以便查看 Course
查詢現在的樣子。 您會看到類似以下內容:
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
您可以看到 JOIN
查詢現在是一個隨 Department
資料一起載入 Course
資料的查詢,並且它包含一個 WHERE
子句。
刪除 var sql = courses.ToString()
線。
建立抽象層
許多開發人員撰寫程式碼以實作存放庫和工作單元模式,作為使用 Entity Framework 之程式碼周圍的包裝函式。 這些模式主要用來建立資料存取層和應用程式的商務邏輯層之間的抽象層。 實作這些模式可協助隔離您的應用程式與資料存放區中的變更,並可促進自動化單元測試或測試驅動開發 (TDD)。 然而,對於使用 EF 的應用程式來說,編寫額外的程式碼來實現這些模式並不總是最佳選擇,原因如下:
- EF 內容類別本身會隔離您的程式碼與資料存放區特有的程式碼。
- EF 內容類別可作為您使用 EF 進行之資料庫更新的工作單元類別。
- Entity Framework 6 中引入的功能可讓您更輕鬆地實作 TDD,而無需編寫儲存庫程式碼。
有關如何實現存儲庫和工作單元模式的更多信息,請參閱本教學課程系列的實體框架 5 版本。 有關在 Entity Framework 6 中實作 TDD 的方法的信息,請參閱以下資源:
代理類
當實體框架建立實體執行個體時 (例如,當您執行查詢時),它通常會將它們建立為動態產生的衍生類型的執行個體,該衍生類型充當實體的代理。 例如,請參閱以下兩個偵錯器影像。 在第一張圖片中,您可以在執行個體化實體後立即看到 student
變數是預期的 Student
類型。 在第二張圖中,使用 EF 從資料庫讀取學生實體後,您會看到代理類別。
此代理類別會覆寫實體的一些虛擬屬性,以插入掛鉤,以便在存取該屬性時自動執行操作。 該機制的用途之一是延遲載入。
大多數時候您不需要了解代理的這種使用,但也有例外:
- 在某些情況下,您可能希望阻止實體框架建立代理執行個體。 例如,當您序列化實體時,您通常需要 POCO 類,而不是代理類。 避免序列化問題的一種方法是序列化資料傳輸物件 (DTO) 而不是實體物件,如將 Web API 與實體框架結合使用教學課程中所示。 另一種方法是禁用代理建立。
- 當您使用
new
運算子執行個體化實體類別時,您不會取得代理執行個體。 這意味著您無法獲得延遲加載和自動更改追蹤等功能。 這通常是沒問題的;您通常不需要延遲加載,因為您正在建立一個不在資料庫中的新實體,並且如果您明確將該實體標記為Added
。 但是,如果您確實需要延遲載入並且需要更改跟踪,則可以使用該DbSet
類別的 Create 方法建立具有代理的新實體執行個體。 - 您可能希望從代理類型取得實際的實體類型。 您可以使用
ObjectContext
類別的 GetObjectType 方法來取得代理類型執行個體的實際實體類型。
有關詳細信息,請參閱 MSDN 上的使用代理。
自動變更偵測
Entity Framework 藉由比較實體的目前值與原始值,判斷實體如何變更 (以及因此需要將哪些更新傳送至資料庫)。 查詢或附加實體時,會儲存原始值。 會導致自動變更偵測的一些方法如下:
DbSet.Find
DbSet.Local
DbSet.Remove
DbSet.Add
DbSet.Attach
DbContext.SaveChanges
DbContext.GetValidationErrors
DbContext.Entry
DbChangeTracker.Entries
如果您正在追蹤大量實體,並且在循環中多次呼叫這些方法之一,則可以透過使用 AutoDetectChangesEnabled 屬性暫時關閉自動變更偵測來獲得顯著的效能改進。 有關詳細信息,請參閱 MSDN 上的自動檢測變更。
自動驗證
當您呼叫 SaveChanges
方法時,預設情況下,實體框架會在更新資料庫之前驗證所有已變更實體的所有屬性中的資料。 如果您已更新大量實體並且已驗證資料,則這項工作是不必要的,您可以透過暫時關閉驗證來縮短儲存變更的流程所需的時間。 您可以使用 ValidateOnSaveEnabled 屬性來執行此操作。 有關詳細信息,請參閱 MSDN 上的驗證。
實體框架電動工具
Entity Framework Power Tools 是一個 Visual Studio 加載項,用於建立這些教學課程中所示的資料模型圖。 這些工具還可以執行其他功能,例如根據現有資料庫中的表產生實體類,以便您可以透過 Code First 使用資料庫。 安裝這些工具後,上下文選單中會出現一些附加選項。 例如,當您在解決方案資源管理器中右鍵點擊上下文類別時,您會看到實體框架選項。 這使您能夠產生圖表。 當您使用 Code First 時,您無法變更圖中的資料模型,但您可以移動內容以使其更易於理解。
實體框架原始碼
Entity Framework 6 的原始碼可在 GitHub 上取得。 您可以提交錯誤,並且可以為 EF 原始程式碼貢獻自己的增強功能。
儘管原始程式碼是開放的,但實體框架作為 Microsoft 產品受到完全支援。 Microsoft Entity Framework 小組將控制接受哪些貢獻,並測試所有的程式碼變更以確保每次發行的品質。
通知
- Tom Dykstra 編寫了本教學課程的原始版本,共同編寫了 EF 5 更新,並編寫了 EF 6 更新。 Tom 是 Microsoft Web 平台和工具內容團隊的高級程式設計作家。
- Rick Anderson (twitter @RickAndMSFT) 完成了 EF 5 和 MVC 4 教學課程的大部分更新工作,並共同撰寫了 EF 6 更新。 Rick 是 Microsoft 的高級程式設計作家,專注於 Azure 和 MVC。
- Rowan Miller 和實體框架團隊的其他成員協助進行程式碼審查,並協助偵錯我們更新 EF 5 和 EF 6 教學課程時出現的許多遷移問題。
疑難排解常見錯誤
無法建立/磁碟區副本
錯誤訊息:
當該檔案已存在時,無法建立/卷影複製<檔案名稱>。
解決方案
等待幾秒鐘並重新整理頁面。
更新資料庫無法識別
錯誤訊息 (來自 PMC 中的 Update-Database
命令):
術語「更新資料庫」不被識別為 cmdlet、函數、指令碼檔案或可操作程序的名稱。 請檢查名稱的拼寫,或者如果包含路徑,請確認路徑是否正確,然後再試一次。
解決方案
結束 Visual Studio。 重新開啟專案並重試。
驗證失敗
錯誤訊息 (來自 PMC 中的 Update-Database
命令):
一個或多個實體的驗證失敗。 有關更多詳細信息,請參閱“EntityValidationErrors”屬性。
解決方案
導致此問題的原因之一是 Seed
方法執行時出現驗證錯誤。 有關偵錯 Seed
方法的提示,請參閱播種和偵錯實體框架 (EF) DB。
HTTP 500.19 錯誤
錯誤訊息:
HTTP 錯誤 500.19 - 內部伺服器錯誤 無法存取所要求的頁面,因為該頁面的相關設定資料無效。
解決方案
出現此錯誤的一種方法是擁有解決方案的多個副本,每個副本都使用相同的連接埠號碼。 通常可以透過退出 Visual Studio 的所有執行個體,然後重新啟動正在處理的專案來解決此問題。 如果這不起作用,請嘗試變更連接埠號碼。 右鍵點擊項目文件,然後按一下屬性。 選擇 Web 索引標籤,然後在專案 URL 文字方塊中變更連接埠號碼。
搜尋 SQL Server 執行個體時發生錯誤
錯誤訊息:
和 SQL Server 建立連線時,發生與網路相關或執行個體特定的錯誤。 找不到或無法存取伺服器。 檢查執行個體名稱是否正確以及 SQL Server 執行個體是否設定為允許遠端連接。 (提供者:SQL 網路介面,錯誤:26 - 搜尋指定的伺服器/執行個體時發生錯誤)
解決方案
檢查連接字串。 如果您手動刪除了資料庫,請變更建構字串中的資料庫名稱。
取得程式碼
其他資源
有關如何使用實體框架處理資料的詳細信息,請參閱 MSDN 上的 EF 文件頁面和 ASP.NET 資料存取 - 建議資源。
有關如何在建置 Web 應用程式後對其進行部署的詳細信息,請參閱 MSDN 庫中的 ASP.NET Web 部署 - 建議資源。
有關與 MVC 相關的其他主題(例如身份驗證和授權)的信息,請參閱 ASP.NET MVC - 推薦資源。
下一步
在本教學課程中,您已:
- 執行原始 SQL 查詢
- 執行非追蹤查詢
- 檢查傳送到資料庫的 SQL 查詢
您還了解到:
- 建立抽象層
- 代理類
- 自動變更偵測
- 自動驗證
- 實體框架電動工具
- 實體框架原始碼
關於在 ASP.NET MVC 應用程式中使用實體框架的系列教學課程到此結束。 如果您想了解 EF Database First,請參閱 DB First 教學課程系列。