教學課程:瞭解 MVC 5 Web 應用程式的進階 EF 案例
在上一個教學課程中,您已實作每個階層的資料表繼承。 本教學課程包含數個主題,當您超越使用 Entity Framework Code First 開發 ASP.NET Web 應用程式的基本概念時,這些主題很有用。 前幾節有逐步指示,可逐步引導您完成程式碼和使用 Visual Studio 來完成工作:後續各節會介紹數個主題,後面接著簡短簡介,後面接著資源連結以取得詳細資訊。
對於大部分的主題,您將使用您已建立的頁面。 若要使用原始 SQL 進行大量更新,您將建立新的頁面,以更新資料庫中所有課程的點數:
在本教學課程中,您:
- 執行原始 SQL 查詢
- 執行無追蹤查詢
- 檢查傳送至資料庫的 SQL 查詢
您也會瞭解:
- 建立抽象層
- Proxy 類別
- 自動變更偵測
- 自動驗證
- Entity Framework Power Tools
- Entity Framework 原始程式碼
必要條件
執行原始 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的 方法中 Details
,將 db.Departments.FindAsync
方法呼叫取代為 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 University 系統管理員想要能夠在資料庫中執行大量變更,例如變更每個課程的點數。 如果該大學有大量的課程,擷取全部課程作為實體並個別進行變更的效率不佳。 在本節中,您將實作網頁,讓使用者能夠指定要變更所有課程點數的因素,而您將執行 SQL UPDATE
語句來進行變更。
在CourseController.cs中,新增 UpdateCourseCredits
和 HttpPost
的方法 HttpGet
:
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
接收下拉式清單的選取值。 如果未選取任何專案,此參數將會是 null。
SelectList
包含所有部門的集合會傳遞至下拉式清單的檢視。 傳遞至建構函式的參數 SelectList
會指定值功能變數名稱、文字功能變數名稱和選取的專案。
針對存放 Get
庫的 方法 Course
,程式碼會指定篩選運算式、排序次序,以及導覽屬性的 Department
積極式載入。 如果下拉式清單中 (未選取任何專案,則篩選運算式一律會傳回 true
null SelectedDepartment
) 。
在 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,而不需要撰寫存放庫程式碼。
如需如何實作存放庫和工作單位模式的詳細資訊,請參閱 本教學課程系列的 Entity Framework 5 版本。 如需在 Entity Framework 6 中實作 TDD 的方法相關資訊,請參閱下列資源:
Proxy 類別
例如,當 Entity Framework 建立實體實例 (時,當您執行查詢) 時,通常會將它們建立為動態產生之衍生類型的實例,做為實體的 Proxy。 例如,請參閱下列兩個偵錯工具映射。 在第一個影像中,您會看到 student
變數在您具現化實體之後立即是預期的 Student
類型。 第二個影像中,使用 EF 從資料庫讀取學生實體之後,您會看到 Proxy 類別。
此 Proxy 類別會覆寫實體的某些虛擬屬性,以在存取屬性時自動插入執行動作的勾點。 這個機制用於的其中一個函式是延遲載入。
在大部分情況下,您不需要注意此 Proxy 的使用方式,但有例外狀況:
- 在某些情況下,您可能會想要防止 Entity Framework 建立 Proxy 實例。 例如,當您序列化實體時,通常會想要 POCO 類別,而不是 Proxy 類別。 避免序列化問題的其中一種方法是將資料傳輸物件序列化 (DTO) 而不是實體物件,如 搭配 Entity Framework 使用 Web API 教學課程所示。 另一種方式是 停用 Proxy 建立。
- 當您使用
new
運算子具現化實體類別時,不會取得 Proxy 實例。 這表示您不會取得延遲載入和自動變更追蹤等功能。 這通常沒關係;您通常不需要延遲載入,因為您正在建立不在資料庫中的新實體,而且如果您明確將實體標示為Added
,則通常不需要變更追蹤。 不過,如果您需要延遲載入,而且需要變更追蹤,您可以使用 類別的DbSet
Create方法來建立具有 Proxy 的新實體實例。 - 您可能會想要從 Proxy 類型取得實際的實體類型。 您可以使用 類別的
ObjectContext
GetObjectType方法來取得 Proxy 類型實例的實際實體類型。
如需詳細資訊,請參閱在 MSDN 上使用 Proxy 。
自動變更偵測
Entity Framework 藉由比較實體的目前值與原始值,判斷實體如何變更 (以及因此需要將哪些更新傳送至資料庫)。 查詢或附加實體時,會儲存原始值。 會導致自動變更偵測的一些方法如下:
DbSet.Find
DbSet.Local
DbSet.Remove
DbSet.Add
DbSet.Attach
DbContext.SaveChanges
DbContext.GetValidationErrors
DbContext.Entry
DbChangeTracker.Entries
如果您要追蹤大量的實體,而且您在迴圈中多次呼叫其中一個方法,可能會暫時關閉自動變更偵測,藉此使用 AutoDetectChangesEnabled 屬性來改善效能。 如需詳細資訊,請參閱 自動偵測 MSDN 上的變更。
自動驗證
當您呼叫 SaveChanges
方法時,Entity Framework 預設會在更新資料庫之前驗證所有已變更實體之所有屬性中的資料。 如果您已更新大量實體,且已經驗證過資料,則不需要這項工作,而且您可以暫時關閉驗證來縮短儲存變更的程式。 您可以使用 ValidateOnSaveEnabled 屬性來執行此動作。 如需詳細資訊,請參閱 MSDN 上的 驗證 。
Entity Framework Power Tools
Entity Framework Power Tools 是 Visual Studio 增益集,可用來建立這些教學課程中顯示的資料模型圖表。 這些工具也可以執行其他功能,例如根據現有資料庫中的資料表產生實體類別,以便搭配 Code First 使用資料庫。 安裝工具之後,操作功能表中會出現一些其他選項。 例如,當您在方案總管中以滑鼠右鍵按一下內容類別別時,您會看到 和Entity Framework選項。 這可讓您產生圖表。 當您使用 Code First 時,您無法變更圖表中的資料模型,但您可以四處移動,以便更容易瞭解。
Entity Framework 原始程式碼
Entity Framework 6 的原始程式碼可在 GitHub取得。 您可以提出 Bug,也可以為 EF 原始程式碼提供自己的增強功能。
雖然原始程式碼已開啟,但 Entity Framework 完全支援為 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 和 Entity Framework 小組的其他成員協助進行程式碼檢閱,並協助偵錯我們在更新 EF 5 和 EF 6 教學課程時所發生的許多移轉問題。
對常見錯誤進行疑難排解
無法建立/陰影複製
錯誤訊息:
當該檔案已經存在時,無法建立/陰影複製 ' < filename > '。
解決方法
請稍候幾秒,然後重新整理頁面。
無法辨識Update-Database
PMC) 中命令的錯誤訊息 Update-Database
(:
'Update-Database' 一詞無法辨識為 Cmdlet、函式、腳本檔案或可操作程式的名稱。 請檢查名稱拼字,如果名稱含有路徑,請確認路徑正確,然後再試一次。
解決方法
結束 Visual Studio。 重新開啟專案,然後再試一次。
驗證失敗
PMC) 中命令的錯誤訊息 Update-Database
(:
一或多個實體的驗證失敗。 如需詳細資訊,請參閱 'EntityValidationErrors' 屬性。
解決方法
此問題的其中一個原因是方法執行時 Seed
發生驗證錯誤。 如需偵錯方法的 Seed
秘訣,請參閱植入和偵錯 Entity Framework (EF) DB。
HTTP 500.19 錯誤
錯誤訊息:
HTTP 錯誤 500.19 - 內部伺服器錯誤 要求的頁面無法存取,因為頁面的相關組態資料無效。
解決方法
您可以取得此錯誤的其中一種方式是有多個解決方案複本,每個複本都使用相同的埠號碼。 您通常可以結束 Visual Studio 的所有實例,然後重新開機您正在處理的專案,來解決此問題。 如果無法運作,請嘗試變更埠號碼。 以滑鼠右鍵按一下專案檔,然後按一下 [屬性]。 選取 [Web] 索引標籤,然後在 [ 專案 URL ] 文字方塊中變更埠號碼。
搜尋 SQL Server 執行個體時發生錯誤
錯誤訊息:
和 SQL Server 建立連線時,發生與網路相關或執行個體特定的錯誤。 找不到或無法存取伺服器。 檢查執行個體名稱是否正確以及 SQL Server 執行個體是否設定為允許遠端連接。 (提供者:SQL 網路介面,錯誤:26 - 搜尋指定的伺服器/執行個體時發生錯誤)
解決方法
檢查連接字串。 如果您已手動刪除資料庫,請變更建構字串中的資料庫名稱。
取得程式碼
其他資源
如需如何使用 Entity Framework 處理資料的詳細資訊,請參閱 MSDN 上的 EF 檔頁面 和 ASP.NET 資料存取 - 建議的資源。
如需如何在建置 Web 應用程式之後部署 Web 應用程式的詳細資訊,請參閱 MSDN Library 中的 ASP.NET Web 部署 - 建議的資源 。
如需與 MVC 相關的其他主題相關資訊,例如驗證和授權,請參閱 ASP.NET MVC - 建議的資源。
後續步驟
在本教學課程中,您:
- 執行原始 SQL 查詢
- 執行無追蹤查詢
- 已檢查傳送至資料庫的 SQL 查詢
您也已瞭解:
- 建立抽象層
- Proxy 類別
- 自動變更偵測
- 自動驗證
- Entity Framework Power Tools
- Entity Framework 原始程式碼
這會完成在 ASP.NET MVC 應用程式中使用 Entity Framework 的這一系列教學課程。 如果您想要瞭解 EF Database First,請參閱 DB First 教學課程系列。