[渣譯文] 使用 MVC 5 的 EF6 Code First 入門 系列:爲ASP.NET MVC應用程序使用高級功能

這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這裏是第十二篇:爲ASP.NET MVC應用程序使用高級功能html

原文:Advanced Entity Framework 6 Scenarios for an MVC 5 Web Applicationios

譯文版權全部,謝絕全文轉載——但您能夠在您的網站上添加到該教程的連接。程序員

在以前的教程中,您已經實現了繼承。本教程引入了當你在使用實體框架Code First來開發ASP.NET web應用程序時能夠利用的高級功能。web

對於本教程中所介紹的大多數主題,您將使用您已經建立的網頁,使用原始的SQL進行批量更新。而後您將建立一個新的頁面來更新數據庫中全部課程的學分。sql

以及使用非跟蹤的查詢,你將在系編輯頁面添加一個新的驗證邏輯。數據庫

執行原始的SQL查詢

實體框架Code First API包含的方法使您能夠直接發送SQL命令到數據庫中。您有如下幾種選擇:api

  • 使用DbSet.SqlQuery方法來進行查詢並返回實體類型。返回的對象類型必須是預期的DbSet對象,它們會由數據庫上下文自動跟蹤。除非您關閉跟蹤。(參見下一節的AsNoTracking方法)
  • 使用Database.SqlQuery方法來進行查詢並返回非實體類型。返回的對象不會被數據庫上下文跟蹤,即便您使用該方法來檢索實體類型。
  • Database.ExecuteSqlCommand用於非查詢類型的命令。

使用實體框架的優勢之一是它可讓你無需手工輸入大量代碼來實現存取數據的特定方法。經過自動生成SQL查詢及命令,將你從繁瑣的手工編碼中解放出來。但在特殊狀況下,您可能須要執行手工建立的特定的SQL查詢,這些方法可以實現這一功能併爲你提供異常處理。瀏覽器

當你常常性地在web應用程序中執行SQL命令時,你必須採起必要的預防措施來保護你的站點不受SQL注入攻擊。其中的一個辦法就是使用參數化的查詢,確保來自web頁的的字符串不會被解釋爲SQL命令。在本教程中,當您使用用戶輸入查詢時,您將使用參數化的查詢。緩存

調用一個查詢來返回實體

DbSet<TEntity>類提供了一個方法,您可使用該方法來執行一個查詢並返回一個實體類型。要觀察該方法是如何工做的,你須要對Department控制器中的Details方法進行一些更改。mvc

在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版本中執行單元測試驅動,請參閱:

代理類

在實體框架建立實體實例時(例如當你執行一個查詢時),它老是建立做爲動態生成的派生自實體的實體對象的代理。例以下面的兩個調試器截圖,在第一個圖像中,您看到了一個預期爲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 MSDNASP.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平臺及工具團隊的高級程序員,做家。

相關文章
相關標籤/搜索