爲ASP.NET MVC應用程序使用高級功能
這是微軟官方教程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
除此之外,本教程對以下課題進行了簡單介紹並提供了參考鏈接:
- 倉儲和單元工作模式
- 代理類
- 自動變化監測
- 自動驗證
- Visua Studio實體框架工具
- 實體框架的源代碼
對於本教程中所介紹的大多數主題,您將使用您已經創建的網頁,使用原始的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);
}
// 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);
}
要驗證新代碼是否工作正常,請運行應用程序,轉到系頁面並點擊某個系的詳情。
你可以看到一切如之前一樣正常工作。
調用一個查詢來返回其他類型的對象
在較早的教程中您創建了一個學生統計網格用來顯示每個註冊日期中註冊的學生數目。這段代碼使用了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()
{
// 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());
}
運行頁面,它會顯示和之前一樣的數據。
調用更新查詢
假設管理員想要能夠在數據庫進行批量操作,例如爲每一門課程更改學分。如果學校有大量的課程,針對每一門課程分別進行更新無疑是效率非常低下的做法。在本節中你會實現一個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>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”到瀏覽器地址欄中的末尾,如Http://localhost:xxxx/UpdateCourseCredits,打開頁面,並在文本框中輸入一個數字:
點擊更新,你會看到受影響的課程:
然後返回列表,你會看到所有課程都進行了更新:
有關更多使用原始SQL查詢的信息,請參閱MSDN上的Raw SQL Queries。
非跟蹤查詢
當數據庫上下文檢索數據行並創建實體對象時,默認情況下它會跟蹤內存中的實體是否與數據庫中的同步。當您更新一個實體時,內存中的數據作爲緩存。這種緩存在web應用程序中經常是不可用的,因爲上下文實例通常是短生命期的(每個請求都會創建一個新實例),並且上下文經常在讀取過實體並使用後就將它們銷燬了。
您可以使用AsNoTracking方法來來禁用跟蹤內存中的實體對象。在以下幾種典型場景中,你可能需要這樣做:
需要檢索大量的數據,而關閉跟蹤可能會顯著提高性能。
您需要附加一個實體來更新它,但它是之前基於不同的目的獲取的同一個實體對象。因爲該實體已經被數據庫的上下文跟蹤,你無法附加該實體以進行更改。這種情況下,你需要對較早的查詢使用AsNoTracking選項。
檢查發送到數據庫的SQL
有時候,查看實際被髮送到數據庫的SQL查詢是很有幫助的,在較早的教程中,您看到了如何使用攔截器代碼來執行這一工作,現在你將看到如何不使用攔截器的方法。要嘗試該方法,你會檢查一個簡單查詢並觀察添加比如預先加載、過濾及排序,看看到底發生了什麼。
在CourseController.cs,使用下面的代碼替換原先的,以停止預先加載:
public ActionResult Index()
{
var courses = db.Courses;
var sql = courses.ToString();
return View(courses.ToList());
}
然後在return語句上設置一個斷點,並按下F5在調試模式下運行該項目,選擇課程索引頁,當代碼到達斷點時,檢查query變量,你將看到被髮送的SQL的查詢,它是一個簡單的select語句。
{SELECT
[Extent1].[CourseID] AS [CourseID],
[Extent1].[Title] AS [Title],
[Extent1].[Credits] AS [Credits],
[Extent1].[DepartmentID] AS [DepartmentID]
FROM [Course] AS [Extent1]}
你可以在監視窗口中使用文本可視化工具來檢視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>Select Department: @Html.DropDownList("SelectedDepartment","All")
<input type="submit" value="Filter" /></p>
}
運行索引頁,在一次遇到斷點時繼續運行以便顯示頁面,從下拉列表中選擇一個系並點擊篩選:
按照剛纔的方法查看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
你現在可以看到上面的查詢語句是一個加載部門和課程數據的連接查詢,幷包含一個WHERE子句。
刪除代碼中的var sql = conrses.ToString();
倉儲和單元工作模式
許多開發人員編寫代碼作爲包裝來實現實體框架的倉儲和單元工作模式。這些模式在商業邏輯層和數據存取層之間創建了一個抽象層。實施這些模式可以幫助你的應用程序從數據存儲的改變中隔離出來,並且促進自動化的單元測試開發。但是,對使用實體框架的程序編寫額外的代碼來實現這些模式並不是最佳的選擇,有以下幾個原因:
實體框架上下文類本身就可以將你的代碼從特定代碼的數據存儲中隔離。
當你使用實體框架時,對於數據庫更新操作實體框架上下文類可以作爲一個工作單元類。
在實體框架6版本中引入的功能使它在無需編寫倉儲代碼的情況下來實現單元測試驅動。
有關如何執行倉儲及單元工作模式的詳細信息,請參閱the Entity Framework 5 version of this tutorial series。有關如何在實體框架6版本中執行單元測試驅動,請參閱:
代理類
在實體框架創建實體實例時(例如當你執行一個查詢時),它總是創建作爲動態生成的派生自實體的實體對象的代理。例如下面的兩個調試器截圖,在第一個圖像中,您看到了一個預期爲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平臺及工具團隊的高級程序員,作家。