EF學習筆記(十二):EF高級應用場景

學習總目錄:ASP.NET MVC5 及 EF6 學習筆記 - (目錄整理)html

上篇連接:EF學習筆記(十一):實施繼承ios

本篇原文連接:Advanced Entity Framework Scenariosweb

本篇主要講一些使用Code First創建ASP.NET WEB應用的時候除了基礎的方式之外的一些擴展方式方法:sql

一、Performing Raw SQL Queries (執行真正的SQL語句)數據庫

二、Performing no-tracking queries (執行無跟蹤的SQL語句)api

三、Examining SQL sent to the database (檢查發到數據庫的SQL語句)緩存

 

Performing Raw SQL Queries併發

EF Code First 有API 可讓用戶直接把SQL語句發給數據庫去執行,有如下幾種選擇:mvc

一、DbSet.SqlQuery  執行查詢語句後返回實體類型(即DbSet對象所預期的);同時被數據庫上下文自動跟蹤(除非手動關閉,可參看AsNoTracking 方法)app

二、Database.SqlQuery 能夠用來執行查詢語句後返回不是實體類型的,同時也不會被數據庫上下文跟蹤,即使是返回的是實體類型;

三、Database.ExecuteSqlCommand 執行非查詢的SQL語句

用EF的一個好處就是一些重複的語句能夠不用本身再寫,它能夠爲你生成一些SQL語句,能夠解放你不用本身再寫;

可是也有一些場景,須要你本身運行本身手動建立的SQL或者方法來處理一些特殊異常狀況。

在WEB網頁應用的時候,必須預防SQL注入攻擊。最好的辦法就是用參數化查詢語句,而不是拼接字符串。

Calling a Query that Returns Entities

DbSet<TEntity>類提供了一個方法能夠用來執行一個SQL語句,並返回一個實體類型。

簡單的例子:

        public async Task<ActionResult> Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }

            //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);
        }

Calling a Query that Returns Other Types of Objects

在前面一個章節設計了Home/About 裏面的代碼:

        public ActionResult About()
        {
            IQueryable<EnrollmentDateGroup> data = from student in db.Students
                                                   group student by student.EnrollmentDate into dateGroup
                                                   select new EnrollmentDateGroup()
                                                   {
                                                       EnrollmentDate = dateGroup.Key,
                                                       StudentCount = dateGroup.Count()
                                                   };
            return View(data.ToList());
        }

能夠替換爲:

        public ActionResult About()
        {
            //IQueryable<EnrollmentDateGroup> 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";
            IEnumerable<EnrollmentDateGroup> data = db.Database.SqlQuery<EnrollmentDateGroup>(query);
            return View(data.ToList());
        }

運行起來,效果是同樣的。

Calling an Update Query

假設須要一個頁面用來批量調整 Course的 Credits;

在Course控制器增長兩個Action:

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();
}

當Get UpdateCourseCredits請求的時候,就給用戶一個編輯框和一個提交按鈕;
當用戶輸入後,點擊提交後,返回帶有更新了多少條記錄的顯示信息的頁面;

爲UpdateCourseCredits建一個空的視圖,而後用下面代碼代替:

@model EFTest.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://XXXXXXX/Course/UpdateCourseCredits 這個URL:

而後輸入2後,點Update:

會顯示一共多少記錄被更新;而後返回Course主頁面看到全部的都乘以2了:

更多關於執行原生SQL語句的內容見:  Raw SQL Queries

No-Tracking Queries

 當一個數據庫上下文檢索數據錶行並創建一個實體對象用來表示它的時候,默認是跟蹤在內存裏的實體和在數據庫裏的保持同步。

在內存裏的數據就像緩存用來更新實體數據。而緩存在WEB應用中不是必須的,由於上下文實例是典型的短生命週期的,每次請求會新建一個,用完就disposed,在實體數據被再使用的時候,上一個上下文實例已經被disposed了。

因此能夠用AsNoTracking方法來不跟蹤內存裏的實體對象。

典型的場景包括:

一、一次查詢很大量的數據,若是要跟蹤,就是極大的下降性能;

二、當準備附加一個實體計劃更新時,若是由於其餘緣由,已經檢索同一個實體,而由於該實體被數據庫上下文所跟蹤,因此你就不能夠附加這個實體。惟一的辦法就是在以前的檢索中採用AsNoTracking方式。

這裏原文說到之前的教程中作了這個測試,並提供了連接;

我在這裏按照之前的教程作這個測試:

先爲Department 控制器加一個私有方法來檢查數據庫,是否是這個Administrator已經對應了一個Department,若是已經對應,則直接提示錯誤;

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);
        }
    }
}

把Edit HttpPost Action 改爲如下簡單的方式,原先檢查併發的先註釋掉:

        [HttpPost]
        [ValidateAntiForgeryToken]
        public 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;
                    db.SaveChanges();
                    return RedirectToAction("Index");
                }
            }
            catch (DbUpdateConcurrencyException ex)
            {
                var entry = ex.Entries.Single();
                var clientValues = (Department)entry.Entity;
            }
            ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", department.InstructorID);
            return View(department);
        }

下面進行測試:先編輯一個Department,而後把Administrator選爲和另外一個Department同樣,這個時候就不會繼續執行SaveChange的操做了,而是直接報錯:

若是返回到部門主頁,再點擊編輯,此次隨便編輯其餘值,好比Budget :

就會出現錯誤:

就是由於附件這個實體的時候,前面檢查Administrator的時候已經去數據庫查詢全部Administrator 爲 Kapoor, Candace 的Dempartment,致使Economics部門已經被讀取,並已經跟蹤;

當Edit Action嘗試爲 MVC自動綁定的Department 實體 更改標識的時候,會致使這個錯誤,由於這個Department 已經被讀取,並被跟蹤。

要解決這個問題,也很簡單,在ValidateOneAdministratorAssignmentPerInstructor 方法中,加入AsNoTracking(),以下:

var duplicateDepartment = db.Departments
   .Include("Administrator")
   .Where(d => d.PersonID == department.PersonID)
   .AsNoTracking()
   .FirstOrDefault();

這樣改好就沒有問題了。

 Examining SQL sent to the database

有時候須要可以看到實際發到數據庫的SQL語句,對於調試程序有很大幫助,前面的教程講了採用攔截的方式,把一些信息攔截到Output去查看;

下面準備說一個簡單的方案來查看實際發到數據庫的SQL語句:

把Course的Index Action變爲如下簡單的方式:

        public ActionResult Index()
        {

            var courses = db.Courses;
            var sql = courses.ToString();
            return View(courses.ToList());

            //var courses = db.Courses.Include(c => c.Department);
            //return View(courses.ToList());
        }

而後在 return語句加上斷點: 選中行點F9便可:

而後按F5執行應用,點擊進入Course\Index頁面,會停留在這一行:

而後光標停留在上一行sql上面,就能夠看到sql 的值:

點那個放大鏡圖標,能夠看的詳細點:

 下面爲Course Index頁面加一個Department 下拉式的過濾;

修改Course Index Action 爲:

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());
}

再在Index視圖 Table元素前面加上:

@using (Html.BeginForm())
{
    <p>Select Department: @Html.DropDownList("SelectedDepartment","All")   
    <input type="submit" value="Filter" /></p>
}

效果如圖:

能夠在return語句加上斷點:

這個時候運行到這裏的時候,就能夠看sql的值:(加入了Department的查詢語句)

Repository and unit of work patterns

倉庫和單元工做模式是不少開發者會去寫代碼實現的,在數據訪問層和業務邏輯層之間加入一個虛擬層;

這種模式能夠幫助應用與數據存儲變化隔離開,可促進實現單元測試或者TDD test-driven development;

然而,在使用EF的狀況下,再額外寫代碼實現這個模式已經不是最好的方式了,主要是如下緣由:

一、EF 的上下文類已經實現應用代碼和數據交互部分的隔離;

二、EF的上下文類能夠做爲單元工做類來進行數據庫更新;

三、EF6 可讓這種模式實現起來更簡單,而不用再本身寫倉庫代碼;

更多學習倉庫和單元工做模式,能夠參考:the Entity Framework 5 version of this tutorial series.

EF6如何實現TDD,能夠參考:

How EF6 Enables Mocking DbSets more easily

Testing with a mocking framework

Testing with your own test doubles

Proxy classes 代理類

 當EF建立一個實體實例的時候,它通常會爲這個實體動態生成一個衍生類型做爲代理,而後再建立這個代理的實例;

能夠看原文中的兩個圖,第1個圖,能夠看到Student申明的時候是Student 類型,但到第2步,從數據庫讀取數據後,就能夠看到代理類:

代理類重載了一些虛擬導航屬性,用一些執行動做的鉤子來填充,當這些虛擬屬性被訪問的時候,能夠自動執行;這種機制主要是爲了 延時加載;

大部分狀況下,不須要關注代理類的工做,可是如下狀況是特例:

一、在一些場景下,須要阻止EF產生代理類,好比你準備序列化實體的時候,確定是但願是POCO類,而不是代理類;有一種辦法來解決這個序列化問題,就是用序列化DTO而不是序列化實體對象;如Using Web API with Entity Framework;(通常在WEB API裏序列化實體要求比較多); 另外也能夠直接關閉代理類:disable proxy creation.

二、當直接用new來實例化一個實體的時候,你獲得的不是代理類,這覺得着你得不到延時加載、數據變化跟蹤這些功能;這個應該也不是大問題,畢竟這個不是從數據庫裏取的數據,一般不須要延時加載,若是你明肯定義它爲Added ,你也不須要數據跟蹤。 然而,你若是須要延時加載、數據變化跟蹤,能夠用DbSet的Create方法來新建代理類;

三、若是你須要從一個代理類實例獲取真正的實體類型,可使用ObjectContextGetObjectType方法從一個代理類實例獲取實體類型;

更多代理類信息參考:Working with Proxies

Automatic change detection 自動變化偵測

EF經過比較當前值和原始值來肯定哪些實體變化了(於是要更新到數據庫裏),原始值是在被檢索或者附加的時候存儲,一些方法引起自動變化偵測: 

  • DbSet.Find
  • DbSet.Local
  • DbSet.Remove
  • DbSet.Add
  • DbSet.Attach
  • DbContext.SaveChanges
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries

若是在跟蹤不少實體,而且在一個循環操做中,要Call上面這些方法不少次的話,那麼臨時性關閉數據變化偵測(經過使用AutoDetectChangesEnabled這個屬性值)是能夠帶來很大的性能提高的;

更多信息見:Automatically Detecting Changes

 Automatic validation 自動數據校驗

 EF默認會在SaveChange的時候校驗數據,而若是是很大量的數據,並且已經被校驗過了,那麼能夠經過臨時性關閉自動校驗(經過使用ValidateOnSaveEnabled這個屬性值)來提升性能;

更多信息見: Validation

相關文章
相關標籤/搜索