深刻理解 EF Core:EF Core 讀取數據時發生了什麼?

閱讀本文大概須要 11 分鐘。數據庫

原文:https://bit.ly/2UMiDLb
做者:Jon P Smith
翻譯:王亮
聲明:我翻譯技術文章不是逐句翻譯的,而是根據我本身的理解來表述的。其中可能會去除一些本人實在不知道如何組織但又不影響理解的句子。編程

本文將爲你詳細描繪 EF Core 從數據庫中讀取數據的「幕後」視圖。我將揭開兩種數據庫讀取方式的面紗:一個是普通的查詢,另外一個是使用 AsNoTracking 方法的非跟蹤查詢。我還將經過一個實驗來演示我是如何解決個人一個客戶遇到的性能問題。服務器

我假設你對 EF Core 已經有了必定的認識,但在深刻學習以前,咱們先來了解一下如何使用 EF Core,以確保咱們已經掌握了一些基本知識。這是一個「深刻研究」的課題,因此我準備大量的技術細節,但願個人描述方式你能理解。性能

本文是「深刻理解 EF Core」系列中的第一篇。如下是本系列文章列表:單元測試

  • 當 EF Core 從數據庫讀取數據時發生了什麼?(本文)
  • 當 EF Core 寫入數據到數據庫時發生了什麼?(敬請期待)

概要

  • EF Core 有兩種方法從數據庫中讀取數據(也稱爲查詢):普通 LINQ 查詢和包含 AsNoTracking 方法的非跟蹤 LINQ 查詢。
  • 這兩種方法查詢的返回類(被稱爲實體類),它鏈接的其它的實體類(即所謂的導航屬性)也被同時加載,但這兩種法如何鏈接及鏈接的內容是不同的。
  • 普通查詢接受的是 DbContext 執行讀取時全部數據的副本——此時的實體類稱爲被跟蹤。這容許加載的實體類參與數據庫的更新操做。
  • 普通查詢還會有一些其它的複雜底層實現,稱爲關係修補(fixup),用於描述讀入的實體類和其餘被跟蹤實體之間的鏈接關係。
  • AsNoTracked 非跟蹤查詢沒有副本,因此它沒有被跟蹤——這意味着它比普通查詢更快。這也意味着它不會用於數據庫的寫操做。
  • 最後,我將展現 EF Core 普通查詢中一個不爲人知的特性,以此做爲示例,說明經過導航屬性鏈接實體類的關係是多麼智能。

EF Core 如何讀取數據庫數據

提示:若是你已經對 EF Core 有必定的認識,那麼你能夠跳過這一節,這部分只是一個如何讀取數據庫的例子。學習

爲了能讓你更好地理解,我先描述一個數據庫結構,而後再給出一個簡單的數據庫讀取示例。下面是一些基本表的結構和它們之間的關係。測試

這些表被映射到具備相似名稱的類,例如 Book、BookAuthor、Author,這些類的屬性名稱與表的字段名稱相同。因爲篇幅有限,我不打算展開來說這些類,但您能夠在個人 GitHub 倉庫[1]中查看這些類。spa

EF Core 讀取數據庫須要下面五部分:翻譯

  1. 數據庫服務器,如 SQL server, Sqlite, PostgreSQL 等。
  2. 具備數據的數據庫。
  3. 映射到數據表的類(稱爲實體類)。
  4. 一個繼承 DbContext 的類,該類包含 EF Core 的配置。
  5. 最後,從數據庫讀取數據的命令。

下面的單元測試代碼來自個人 GitHub 創庫[2],展現了一個簡單的示例,它從現有數據庫中讀取 4 個 Book 實體及其關聯的 BookAuthor 和 Authors 實體。3d

倉庫地址:https://bit.ly/2Yza7QQ

[Fact]
public void TestBookCountAuthorsOk()
{
    //SETUP
    var options = SqliteInMemory.CreateOptions<EfCoreContext>();
    //code to set up the database with four books, two with the same Author
    using (var context = new EfCoreContext(options))
    {
        //ATTEMPT
        var books = context.Books
            .Include(r => r.AuthorsLink)
            .ThenInclude(r => r.Author)
            .ToList();

        //VERIFY
        books.Count.ShouldEqual(4);
        books.SelectMany(x => x.AuthorsLink.Select(y => y.Author))
            .Distinct().Count().ShouldEqual(3);
    }
}

如今,若是咱們將單元測試代碼對應到上面的 5 部分,結果是這樣的:

  1. 數據庫服務器——第 5 行:我選擇了一個 Sqlite 數據庫服務器,在本例中是 SqliteInMemory.CreateOptions 方法,它使用個人一個 NuGet 包 EfCore.TestSupport 建立了一個內存數據庫(內存中的數據庫對於單元測試很是有用,由於你能夠爲這個測試創建一個新的空數據庫)。
  2. 具備數據的數據庫——第 6 行:我將在下一篇文章介紹數據是如何寫入數據庫的,如今假設有一個數據庫包含 4 本書信息,其中兩本書的做者是同一我的。
  3. 實體類——代碼裏這裏沒有展現,可是你能夠在這裏查看這些類[1]。其中有一個 Books 實體類,經過一個名爲 BookAuhor 的實體類多對多關聯 Authors 實體類。
  4. 一個繼承 DbContext 的類——第 7 行:EfCoreContext 類繼承了 DbContext 類並配置了從類到數據庫的映射關係(你能夠在個人 GitHub 倉庫[3] 中查看該類)。
  5. 從數據庫讀取數據的命令——第 10 到 13 行,這是一個查詢:
    • 第 10 行 — context 爲 EfCoreContext 的實例,經過它訪問你的數據庫,.Books 表示您但願訪問 Books 表。
    • 第 11 行 — Include 被稱爲貪婪加載,它告訴 EF Core 當它加載 Books 時,也應該加載關聯到的全部 BookAuthor 實體類。
    • 第 12 行 — ThenInclude 是繼續貪婪加載,它告訴 EF Core 當它加載一個 BookAuthor 時,它也應該加載關聯到該 BookAuthor 的 Author 實體類。

全部這一切查詢出來是一個結果集,其中有普通屬性,像 Books 的 Title 屬性;有關聯實體類的導航屬性,像 Books 的 AuthorsLink 屬性。

這個示例稱爲查詢或讀取,也是四種數據庫訪問類型之一,即 CRUD(新增、讀取、更新和刪除)。我將在下一篇文章中介紹新增和更新。

EF Core 如何表示讀取的數據

當你查詢數據庫時,EF Core 會將數據庫返回的數據轉換爲實體類並填充導航屬性的值。在本節中,咱們將研究兩種類型的查詢步驟——普通查詢(即沒有 AsNoTracking 方法,也稱爲讀寫查詢)和添加了 AsNoTracking 方法的非跟蹤查詢(稱爲只讀查詢)。

咱們先來看一下最初 LINQ 語句是如何轉換成數據庫相應的查詢命令而後返回數據的。對於咱們將要看到的兩種類型的查詢來講,這是很常見的操做。關於查詢的第一部分,請參見下圖。

有一些很是複雜的代碼將你的 LINQ 轉換爲數據庫查詢命令,但這些內部細節咱們沒必要關心。若是你的 LINQ 不能被翻譯,你會從 EF Core 獲得一個異常消息,其中包含相似「不能被翻譯」的描述詞語。此外,當數據返回時,像 Value Converters[4] 這樣的特性可能會調整數據。

本節展現了查詢的第一部分,其中 LINQ 被轉換爲數據庫命令並返回全部正確的值。如今咱們來看查詢的第二部分,在這裏 EF Core 獲取返回值並將它們轉換爲實體類的實例,並填充導航屬性。咱們將分別看看兩種類型的查詢。

1. 普通查詢(讀寫查詢)

普通查詢讀取數據的方式能夠修改數據並更新到數據庫,這就是我將其稱爲讀寫查詢的緣由。它不會自動更新數據(請參閱下一篇文章,瞭解如何寫入數據庫)。若是你要更新數據,你的查詢必須是讀寫查詢。

我在介紹中給出的示例執行的是一個普通讀寫查詢,讀取帶有 AuthorsLink 實例的示例。下面是該示例的查詢部分的代碼:

var books = context.Books
    .Include(r => r.AuthorsLink)
    .ThenInclude(r => r.Author)
    .ToList();

而後 EF Core 經過三個步驟將這些值轉換並填充含有導航屬性的實體類。下圖顯示了這三個步驟以及生成的實體類及其導航屬性的實體類。

讓咱們來分析一下這三個步驟:

  1. 建立類並填充數據。它接受數據庫返回的值,並填充非導航(稱爲標量)屬性、字段等。在 Book 實體類中,是 BookId(主鍵)、Title 等屬性——參見上圖左下角淺藍色矩形。
  2. 修補關聯關係。首先是填入主鍵和外鍵的信息,它們定義如何相互關聯數據。而後,EF Core 使用這些鍵設置實體類之間的導航屬性(如圖中藍色粗線所示)。這個關係的修補所需的信息不只是查詢讀入的實體類,它還會查看 DbContext 中跟蹤的每一個實體,並填充導航屬性。這是一個強大的功能,但你的被跟蹤實體越多,所需消耗時間也越多——這就是爲何須要 AsNoTracking 來實現更快的查詢。
  3. 建立跟蹤快照。跟蹤快照是返回給用戶的實體類的一個副本,加上它所隱藏的與每一個實體類的關聯關係——若一個實體處於被跟蹤狀態,這意味着它將會發生修改並會寫入到數據庫中。

2. 非跟蹤查詢(只讀查詢)

非跟蹤查詢,即便用 AsNoTracking 方法的查詢,是一個只讀查詢。這意味着,當 SaveChanges 方法被調用時,你讀取的任何內容都不會被寫入數據庫。非跟蹤查詢的查詢效率更高,在下一節中,我將介紹非跟蹤查詢以及與普通查詢的其餘區別。

在前文的示例以後,我修改了查詢代碼,添加了下面的 AsNoTracking 方法(請看第 2 行):

var books = context.Books
    .AsNoTracking()
    .Include(r => r.AuthorsLink)
    .ThenInclude(r => r.Author)
    .ToList();

這裏的 LINQ 查詢只有上面的普通查詢的前兩個步驟(沒有第三個步驟)。下圖顯示了 AsNoTracking 查詢的步驟。

步驟以下:

  1. 建立類並填充數據。它接受數據庫返回的值,並填充非導航(稱爲標量)屬性、字段等。在 Book 實體類中,是 BookId(主鍵)、Title 等屬性——參見上圖左下角淺藍色矩形。
  2. 修補關聯關係。首先是填入主鍵和外鍵的信息,它們定義如何相互關聯數據。而後,EF Core 使用這些鍵設置實體類之間的導航屬性(如圖中藍色粗線所示)。這個關係的修補所需的信息不只是查詢讀入的實體類,它還會查看 DbContext 中跟蹤的每一個實體,並填充導航屬性。這是一個強大的功能,但你的被跟蹤實體越多,所需消耗時間也越多——這就是爲何須要 AsNoTracking 來實現更快的查詢。

普通查詢和非跟蹤查詢的區別

如今讓咱們比較這兩種查詢比較明顯的區別。

  1. 非跟蹤查詢查詢的性能更好。使用非跟蹤查詢查詢的主要緣由是性能。非跟蹤查詢查詢表現爲:

    • 稍微快一點,使用的內存稍微少一點,由於它不須要建立跟蹤快照。
    • 避免沒有必要的跟蹤快照能夠提升 SaveChanges 的性能,由於它沒必要檢查跟蹤快照以查找更改。
    • 稍微快一點,由於修補關聯關係時沒有所謂的身份解析。這就是爲何你會獲得兩個具備相同數據的 Author 實例。
  2. 非跟蹤查詢修補關聯關係時只連接查詢中的實體。在普通查詢中,我已經說過修補關聯關係時鏈接的是查詢中的實體和當前跟蹤的實體,可是非跟蹤查詢只修補查詢中的實體關係。

  3. 非跟蹤查詢並不老是表明數據庫關係。這兩種類型查詢之間的關係修補的另外一個區別是,非跟蹤查詢關係修補更快,它不須要標識的解析。這能夠爲數據庫中的同一行生成多個實例——見上圖右下角藍色的 Author 實體和註釋。若是隻是向用戶顯示數據,那麼這種差別並不重要,可是若是具備業務邏輯,那麼多個實例不能正確反映數據的結構,就可能會有問題。

對層級數據有用的關係修補特性

關聯關係修補的步驟是很是智能的,特別是在普通查詢中。下面我想向你展現我是如何利用關係修補的特性來解決一個客戶項目中的性能問題的。

我曾在一家公司工做,那裏的許多數據處理都是層次化結構的,即數據具備一系列深度不肯定的關聯關係。問題是我必須先解析整個層次結構,而後才能呈現這些數據。我最初是經過貪婪的方式加載前兩個層級,而後顯式地加載更深的層級來實現這一點的。它能夠工做,可是性能很是慢,而且數據庫因大量單數據庫訪問而超載。

這不得不讓我思考解決辦法,若是普通查詢的關係修補那麼智能的話,它能幫助我提升查詢的性能嗎?它能夠!讓我給你舉一個公司員工的例子。下圖顯示了咱們想要加載的公司的層次結構。

你能夠接龍式地使用 .Include(x => x.WorksForMe).ThenInclude(x => x.WorksForMe)… 等等來加載所需的層級信息,但結果是一個 .Include(x => x.WorksForMe) 就夠了。由於 EF Core 的關係修補爲你作了剩下的事情,這一點很驚奇,但也頗有用。

例如,若是我想查詢角色爲 Development 的全部員工(每一個員工都有一個名爲 WhatTheyDo 的屬性和名爲 Role 的屬性,該 Role 包含他們工做的部門),我能夠這樣編寫代碼:

var devDept = context.Employees
    .Include(x => x.WorksFromMe)
    .Where(x => x.WhatTheyDo.HasFlag(Roles.Development))
    .ToList();

這將建立一個查詢,用於加載角色爲 Development 的全部員工,而且在員工實體類上修補與 WorksFoMe 導航屬性(集合)和 Manager 導航屬性(單個)的關係。經過只執行一個查詢,既提升了查詢花費的時間,又減小了數據庫服務器上的負載。

總結

你已經看到了兩種類型的查詢,我稱之爲 a)普通的讀寫查詢,和 b) 非跟蹤的只讀查詢。對於每一種查詢類型,我都向你展現了 EF Core 「幕後」是如何讀取數據並展現的。他們工做方式的不一樣也表現出他們的優點和劣勢。

非跟蹤查詢是隻讀查詢的解決方案,由於它比普通讀寫查詢更快。可是您應該記住關係修補的機制,它能夠在數據庫只有一個關係的狀況下建立類的多個實例。

普通的讀寫查詢是查詢跟蹤實體的解決方案,這意味着你能夠在建立、更新和刪除數據時使用它們。普通的讀寫查詢確實會佔用更多的時間和內存資源,可是有一些有用的特性,好比自動連接到其餘被跟蹤的實體類實例。

我但願這篇文章對您有用。祝你編程快樂!

[1]. https://bit.ly/2MXK3ZY
[2]. https://bit.ly/2Yza7QQ
[3]. https://bit.ly/2Y0UORO
[4]. https://bit.ly/2YEyg8j

相關文章
相關標籤/搜索