深入理解 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]中查看這些類。

EF Core 讀取數據庫需要下面五部分:

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

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

倉庫地址: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

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

您可能也會喜歡…