【EF6學習筆記】(十二)EF高級應用場景


本篇原文鏈接:Advanced Entity Framework Scenarios

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

1、Performing Raw SQL Queries (執行真正的SQL語句)

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

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

 

Performing Raw SQL Queries

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

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

2、Database.SqlQuery 可以用來執行查詢語句后返回不是實體類型的,同時也不會被數據庫上下文跟蹤,即便是返回的是實體類型;

3、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方法來不跟蹤內存里的實體對象。

典型的場景包括:

1、一次查詢很大量的數據,如果要跟蹤,就是極大的降低性能;

2、當准備附加一個實體計划更新時,如果因為其他原因,已經檢索同一個實體,而因為該實體被數據庫上下文所跟蹤,所以你就不可以附加這個實體。唯一的辦法就是在之前的檢索中采用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的情況下,再額外寫代碼實現這個模式已經不是最好的方式了,主要是以下原因:

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

2、EF的上下文類可以作為單元工作類來進行數據庫更新;

3、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步,從數據庫讀取數據后,就可以看到代理類:

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

大部分情況下,不需要關注代理類的工作,但是以下情況是特例:

1、在一些場景下,需要阻止EF產生代理類,比如你准備序列化實體的時候,肯定是希望是POCO類,而不是代理類;有一種辦法來解決這個序列化問題,就是用序列化DTO而不是序列化實體對象;如Using Web API with Entity Framework;(一般在WEB API里序列化實體要求比較多); 另外也可以直接關閉代理類:disable proxy creation.

2、當直接用new來實例化一個實體的時候,你得到的不是代理類,這以為着你得不到延時加載、數據變化跟蹤這些功能;這個應該也不是大問題,畢竟這個不是從數據庫里取的數據,通常不需要延時加載,如果你明確定義它為Added ,你也不需要數據跟蹤。 然而,你如果需要延時加載、數據變化跟蹤,可以用DbSet的Create方法來新建代理類;

3、如果你需要從一個代理類實例獲取真正的實體類型,可以使用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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM