.net EF Core專題:EF Core 讀取數(shù)據(jù)時(shí)發(fā)生了什么?
原文:https://bit.ly/2UMiDLb
作者:Jon P Smith
翻譯:王亮
聲明:我翻譯技術(shù)文章不是逐句翻譯的,而是根據(jù)我自己的理解來表述的。其中可能會去除一些本人實(shí)在不知道如何組織但又不影響理解的句子。
本文將為你詳細(xì)描繪 EF Core 從數(shù)據(jù)庫中讀取數(shù)據(jù)的“幕后”視圖。我將揭開兩種數(shù)據(jù)庫讀取方式的面紗:一個(gè)是普通的查詢,另一個(gè)是使用 AsNoTracking 方法的非跟蹤查詢。我還將通過一個(gè)實(shí)驗(yàn)來演示我是如何解決我的一個(gè)客戶遇到的性能問題。
我假設(shè)你對 EF Core 已經(jīng)有了一定的認(rèn)識,但在深入學(xué)習(xí)之前,我們先來了解一下如何使用 EF Core,以確保我們已經(jīng)掌握了一些基本知識。這是一個(gè)“深入研究”的課題,所以我準(zhǔn)備大量的技術(shù)細(xì)節(jié),希望我的描述方式你能理解。
本文是“深入理解 EF Core”系列中的第一篇。以下是本系列文章列表:
- 當(dāng) EF Core 從數(shù)據(jù)庫讀取數(shù)據(jù)時(shí)發(fā)生了什么?(本文)
- 當(dāng) EF Core 寫入數(shù)據(jù)到數(shù)據(jù)庫時(shí)發(fā)生了什么?(敬請期待)
概要
- EF Core 有兩種方法從數(shù)據(jù)庫中讀取數(shù)據(jù)(也稱為查詢):普通 LINQ 查詢和包含 AsNoTracking 方法的非跟蹤 LINQ 查詢。
- 這兩種方法查詢的返回類(被稱為實(shí)體類),它連接的其它的實(shí)體類(即所謂的導(dǎo)航屬性)也被同時(shí)加載,但這兩種法如何連接及連接的內(nèi)容是不一樣的。
- 普通查詢接受的是 DbContext 執(zhí)行讀取時(shí)所有數(shù)據(jù)的副本——此時(shí)的實(shí)體類稱為被跟蹤。這允許加載的實(shí)體類參與數(shù)據(jù)庫的更新操作。
- 普通查詢還會有一些其它的復(fù)雜底層實(shí)現(xiàn),稱為關(guān)系修補(bǔ)(fixup),用于描述讀入的實(shí)體類和其他被跟蹤實(shí)體之間的連接關(guān)系。
- AsNoTracked 非跟蹤查詢沒有副本,所以它沒有被跟蹤——這意味著它比普通查詢更快。這也意味著它不會用于數(shù)據(jù)庫的寫操作。
- 最后,我將展示 EF Core 普通查詢中一個(gè)鮮為人知的特性,以此作為示例,說明通過導(dǎo)航屬性連接實(shí)體類的關(guān)系是多么智能。
EF Core 如何讀取數(shù)據(jù)庫數(shù)據(jù)
提示:如果你已經(jīng)對 EF Core 有一定的認(rèn)識,那么你可以跳過這一節(jié),這部分只是一個(gè)如何讀取數(shù)據(jù)庫的例子。
為了能讓你更好地理解,我先描述一個(gè)數(shù)據(jù)庫結(jié)構(gòu),然后再給出一個(gè)簡單的數(shù)據(jù)庫讀取示例。下面是一些基本表的結(jié)構(gòu)和它們之間的關(guān)系。
這些表被映射到具有類似名稱的類,例如 Book、BookAuthor、Author,這些類的屬性名稱與表的字段名稱相同。由于篇幅有限,我不打算展開來講這些類,但您可以在我的 GitHub 倉庫[1]中查看這些類。
EF Core 讀取數(shù)據(jù)庫需要下面五部分:
- 數(shù)據(jù)庫服務(wù)器,如 SQL server, Sqlite, PostgreSQL 等。
- 具有數(shù)據(jù)的數(shù)據(jù)庫。
- 映射到數(shù)據(jù)表的類(稱為實(shí)體類)。
- 一個(gè)繼承 DbContext 的類,該類包含 EF Core 的配置。
- 最后,從數(shù)據(jù)庫讀取數(shù)據(jù)的命令。
下面的單元測試代碼來自我的 GitHub 創(chuàng)庫[2],展示了一個(gè)簡單的示例,它從現(xiàn)有數(shù)據(jù)庫中讀取 4 個(gè) Book 實(shí)體及其關(guān)聯(lián)的 BookAuthor 和 Authors 實(shí)體。
[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); } }
現(xiàn)在,如果我們將單元測試代碼對應(yīng)到上面的 5 部分,結(jié)果是這樣的:
- 數(shù)據(jù)庫服務(wù)器——第 5 行:我選擇了一個(gè) Sqlite 數(shù)據(jù)庫服務(wù)器,在本例中是
SqliteInMemory.CreateOptions
方法,它使用我的一個(gè) NuGet 包 EfCore.TestSupport 創(chuàng)建了一個(gè)內(nèi)存數(shù)據(jù)庫(內(nèi)存中的數(shù)據(jù)庫對于單元測試非常有用,因?yàn)槟憧梢詾檫@個(gè)測試建立一個(gè)新的空數(shù)據(jù)庫)。 - 具有數(shù)據(jù)的數(shù)據(jù)庫——第 6 行:我將在下一篇文章介紹數(shù)據(jù)是如何寫入數(shù)據(jù)庫的,現(xiàn)在假設(shè)有一個(gè)數(shù)據(jù)庫包含 4 本書信息,其中兩本書的作者是同一個(gè)人。
- 實(shí)體類——代碼里這里沒有展示,但是你可以在這里查看這些類[1]。其中有一個(gè) Books 實(shí)體類,通過一個(gè)名為 BookAuhor 的實(shí)體類多對多關(guān)聯(lián) Authors 實(shí)體類。
- 一個(gè)繼承 DbContext 的類——第 7 行:EfCoreContext 類繼承了 DbContext 類并配置了從類到數(shù)據(jù)庫的映射關(guān)系(你可以在我的 GitHub 倉庫[3] 中查看該類)。
- 從數(shù)據(jù)庫讀取數(shù)據(jù)的命令——第 10 到 13 行,這是一個(gè)查詢:
- 第 10 行 — context 為 EfCoreContext 的實(shí)例,通過它訪問你的數(shù)據(jù)庫,
.Books
表示您希望訪問 Books 表。 - 第 11 行 — Include 被稱為貪婪加載,它告訴 EF Core 當(dāng)它加載 Books 時(shí),也應(yīng)該加載關(guān)聯(lián)到的所有 BookAuthor 實(shí)體類。
- 第 12 行 — ThenInclude 是繼續(xù)貪婪加載,它告訴 EF Core 當(dāng)它加載一個(gè) BookAuthor 時(shí),它也應(yīng)該加載關(guān)聯(lián)到該 BookAuthor 的 Author 實(shí)體類。
所有這一切查詢出來是一個(gè)結(jié)果集,其中有普通屬性,像 Books 的 Title 屬性;有關(guān)聯(lián)實(shí)體類的導(dǎo)航屬性,像 Books 的 AuthorsLink 屬性。
這個(gè)示例稱為查詢或讀取,也是四種數(shù)據(jù)庫訪問類型之一,即 CRUD(新增、讀取、更新和刪除)。我將在下一篇文章中介紹新增和更新。
EF Core 如何表示讀取的數(shù)據(jù)
當(dāng)你查詢數(shù)據(jù)庫時(shí),EF Core 會將數(shù)據(jù)庫返回的數(shù)據(jù)轉(zhuǎn)換為實(shí)體類并填充導(dǎo)航屬性的值。在本節(jié)中,我們將研究兩種類型的查詢步驟——普通查詢(即沒有 AsNoTracking 方法,也稱為讀寫查詢)和添加了 AsNoTracking 方法的非跟蹤查詢(稱為只讀查詢)。
我們先來看一下最初 LINQ 語句是如何轉(zhuǎn)換成數(shù)據(jù)庫相應(yīng)的查詢命令然后返回?cái)?shù)據(jù)的。對于我們將要看到的兩種類型的查詢來說,這是很常見的操作。關(guān)于查詢的第一部分,請參見下圖。
有一些非常復(fù)雜的代碼將你的 LINQ 轉(zhuǎn)換為數(shù)據(jù)庫查詢命令,但這些內(nèi)部細(xì)節(jié)我們不必關(guān)心。如果你的 LINQ 不能被翻譯,你會從 EF Core 得到一個(gè)異常消息,其中包含類似“不能被翻譯”的描述詞語。此外,當(dāng)數(shù)據(jù)返回時(shí),像 Value Converters[4] 這樣的特性可能會調(diào)整數(shù)據(jù)。
本節(jié)展示了查詢的第一部分,其中 LINQ 被轉(zhuǎn)換為數(shù)據(jù)庫命令并返回所有正確的值?,F(xiàn)在我們來看查詢的第二部分,在這里 EF Core 獲取返回值并將它們轉(zhuǎn)換為實(shí)體類的實(shí)例,并填充導(dǎo)航屬性。我們將分別看看兩種類型的查詢。
1. 普通查詢(讀寫查詢)
普通查詢讀取數(shù)據(jù)的方式可以修改數(shù)據(jù)并更新到數(shù)據(jù)庫,這就是我將其稱為讀寫查詢的原因。它不會自動更新數(shù)據(jù)(請參閱下一篇文章,了解如何寫入數(shù)據(jù)庫)。如果你要更新數(shù)據(jù),你的查詢必須是讀寫查詢。
我在介紹中給出的示例執(zhí)行的是一個(gè)普通讀寫查詢,讀取帶有 AuthorsLink 實(shí)例的示例。下面是該示例的查詢部分的代碼:
var books = context.Books .Include(r => r.AuthorsLink) .ThenInclude(r => r.Author) .ToList();
然后 EF Core 通過三個(gè)步驟將這些值轉(zhuǎn)換并填充含有導(dǎo)航屬性的實(shí)體類。下圖顯示了這三個(gè)步驟以及生成的實(shí)體類及其導(dǎo)航屬性的實(shí)體類。
讓我們來分析一下這三個(gè)步驟:
- 創(chuàng)建類并填充數(shù)據(jù)。它接受數(shù)據(jù)庫返回的值,并填充非導(dǎo)航(稱為標(biāo)量)屬性、字段等。在 Book 實(shí)體類中,是 BookId(主鍵)、Title 等屬性——參見上圖左下角淺藍(lán)色矩形。
- 修補(bǔ)關(guān)聯(lián)關(guān)系。首先是填入主鍵和外鍵的信息,它們定義如何相互關(guān)聯(lián)數(shù)據(jù)。然后,EF Core 使用這些鍵設(shè)置實(shí)體類之間的導(dǎo)航屬性(如圖中藍(lán)色粗線所示)。這個(gè)關(guān)系的修補(bǔ)所需的信息不僅是查詢讀入的實(shí)體類,它還會查看 DbContext 中跟蹤的每個(gè)實(shí)體,并填充導(dǎo)航屬性。這是一個(gè)強(qiáng)大的功能,但你的被跟蹤實(shí)體越多,所需消耗時(shí)間也越多——這就是為什么需要 AsNoTracking 來實(shí)現(xiàn)更快的查詢。
- 創(chuàng)建跟蹤快照。跟蹤快照是返回給用戶的實(shí)體類的一個(gè)副本,加上它所隱藏的與每個(gè)實(shí)體類的關(guān)聯(lián)關(guān)系——若一個(gè)實(shí)體處于被跟蹤狀態(tài),這意味著它將會發(fā)生修改并會寫入到數(shù)據(jù)庫中。
2. 非跟蹤查詢(只讀查詢)
非跟蹤查詢,即使用 AsNoTracking 方法的查詢,是一個(gè)只讀查詢。這意味著,當(dāng) SaveChanges 方法被調(diào)用時(shí),你讀取的任何內(nèi)容都不會被寫入數(shù)據(jù)庫。非跟蹤查詢的查詢效率更高,在下一節(jié)中,我將介紹非跟蹤查詢以及與普通查詢的其他區(qū)別。
在前文的示例之后,我修改了查詢代碼,添加了下面的 AsNoTracking 方法(請看第 2 行):
var books = context.Books .AsNoTracking() .Include(r => r.AuthorsLink) .ThenInclude(r => r.Author) .ToList();
這里的 LINQ 查詢只有上面的普通查詢的前兩個(gè)步驟(沒有第三個(gè)步驟)。下圖顯示了 AsNoTracking 查詢的步驟。
步驟如下:
- 創(chuàng)建類并填充數(shù)據(jù)。它接受數(shù)據(jù)庫返回的值,并填充非導(dǎo)航(稱為標(biāo)量)屬性、字段等。在 Book 實(shí)體類中,是 BookId(主鍵)、Title 等屬性——參見上圖左下角淺藍(lán)色矩形。
- 修補(bǔ)關(guān)聯(lián)關(guān)系。首先是填入主鍵和外鍵的信息,它們定義如何相互關(guān)聯(lián)數(shù)據(jù)。然后,EF Core 使用這些鍵設(shè)置實(shí)體類之間的導(dǎo)航屬性(如圖中藍(lán)色粗線所示)。這個(gè)關(guān)系的修補(bǔ)所需的信息不僅是查詢讀入的實(shí)體類,它還會查看 DbContext 中跟蹤的每個(gè)實(shí)體,并填充導(dǎo)航屬性。這是一個(gè)強(qiáng)大的功能,但你的被跟蹤實(shí)體越多,所需消耗時(shí)間也越多——這就是為什么需要 AsNoTracking 來實(shí)現(xiàn)更快的查詢。
普通查詢和非跟蹤查詢的區(qū)別
現(xiàn)在讓我們比較這兩種查詢比較明顯的區(qū)別。
- 非跟蹤查詢查詢的性能更好。使用非跟蹤查詢查詢的主要原因是性能。非跟蹤查詢查詢表現(xiàn)為:
- 稍微快一點(diǎn),使用的內(nèi)存稍微少一點(diǎn),因?yàn)樗恍枰獎?chuàng)建跟蹤快照。
- 避免沒有必要的跟蹤快照可以提高 SaveChanges 的性能,因?yàn)樗槐貦z查跟蹤快照以查找更改。
- 稍微快一點(diǎn),因?yàn)樾扪a(bǔ)關(guān)聯(lián)關(guān)系時(shí)沒有所謂的身份解析。這就是為什么你會得到兩個(gè)具有相同數(shù)據(jù)的 Author 實(shí)例。
- 非跟蹤查詢修補(bǔ)關(guān)聯(lián)關(guān)系時(shí)只鏈接查詢中的實(shí)體。在普通查詢中,我已經(jīng)說過修補(bǔ)關(guān)聯(lián)關(guān)系時(shí)連接的是查詢中的實(shí)體和當(dāng)前跟蹤的實(shí)體,但是非跟蹤查詢只修補(bǔ)查詢中的實(shí)體關(guān)系。
- 非跟蹤查詢并不總是代表數(shù)據(jù)庫關(guān)系。這兩種類型查詢之間的關(guān)系修補(bǔ)的另一個(gè)區(qū)別是,非跟蹤查詢關(guān)系修補(bǔ)更快,它不需要標(biāo)識的解析。這可以為數(shù)據(jù)庫中的同一行生成多個(gè)實(shí)例——見上圖右下角藍(lán)色的 Author 實(shí)體和注釋。如果只是向用戶顯示數(shù)據(jù),那么這種差異并不重要,但是如果具有業(yè)務(wù)邏輯,那么多個(gè)實(shí)例不能正確反映數(shù)據(jù)的結(jié)構(gòu),就可能會有問題。
對層級數(shù)據(jù)有用的關(guān)系修補(bǔ)特性
關(guān)聯(lián)關(guān)系修補(bǔ)的步驟是非常智能的,特別是在普通查詢中。下面我想向你展示我是如何利用關(guān)系修補(bǔ)的特性來解決一個(gè)客戶項(xiàng)目中的性能問題的。
我曾在一家公司工作,那里的許多數(shù)據(jù)處理都是層次化結(jié)構(gòu)的,即數(shù)據(jù)具有一系列深度不確定的關(guān)聯(lián)關(guān)系。問題是我必須先解析整個(gè)層次結(jié)構(gòu),然后才能呈現(xiàn)這些數(shù)據(jù)。我最初是通過貪婪的方式加載前兩個(gè)層級,然后顯式地加載更深的層級來實(shí)現(xiàn)這一點(diǎn)的。它可以工作,但是性能非常慢,并且數(shù)據(jù)庫因大量單數(shù)據(jù)庫訪問而超載。
這不得不讓我思考解決辦法,如果普通查詢的關(guān)系修補(bǔ)那么智能的話,它能幫助我提高查詢的性能嗎?它可以!讓我給你舉一個(gè)公司員工的例子。下圖顯示了我們想要加載的公司的層次結(jié)構(gòu)。
你可以接龍式地使用 .Include(x => x.WorksForMe).ThenInclude(x => x.WorksForMe)… 等等來加載所需的層級信息,但結(jié)果是一個(gè) .Include(x => x.WorksForMe) 就夠了。因?yàn)?EF Core 的關(guān)系修補(bǔ)為你做了剩下的事情,這一點(diǎn)很驚奇,但也很有用。
例如,如果我想查詢角色為 Development 的所有員工(每個(gè)員工都有一個(gè)名為 WhatTheyDo 的屬性和名為 Role 的屬性,該 Role 包含他們工作的部門),我可以這樣編寫代碼:
var devDept = context.Employees .Include(x => x.WorksFromMe) .Where(x => x.WhatTheyDo.HasFlag(Roles.Development)) .ToList();
這將創(chuàng)建一個(gè)查詢,用于加載角色為 Development 的所有員工,并且在員工實(shí)體類上修補(bǔ)與 WorksFoMe 導(dǎo)航屬性(集合)和 Manager 導(dǎo)航屬性(單個(gè))的關(guān)系。通過只執(zhí)行一個(gè)查詢,既提高了查詢花費(fèi)的時(shí)間,又減少了數(shù)據(jù)庫服務(wù)器上的負(fù)載。
總結(jié)
你已經(jīng)看到了兩種類型的查詢,我稱之為 a)普通的讀寫查詢,和 b) 非跟蹤的只讀查詢。對于每一種查詢類型,我都向你展示了 EF Core “幕后”是如何讀取數(shù)據(jù)并展示的。他們工作方式的不同也表現(xiàn)出他們的優(yōu)勢和劣勢。
非跟蹤查詢是只讀查詢的解決方案,因?yàn)樗绕胀ㄗx寫查詢更快。但是您應(yīng)該記住關(guān)系修補(bǔ)的機(jī)制,它可以在數(shù)據(jù)庫只有一個(gè)關(guān)系的情況下創(chuàng)建類的多個(gè)實(shí)例。
普通的讀寫查詢是查詢跟蹤實(shí)體的解決方案,這意味著你可以在創(chuàng)建、更新和刪除數(shù)據(jù)時(shí)使用它們。普通的讀寫查詢確實(shí)會占用更多的時(shí)間和內(nèi)存資源,但是有一些有用的特性,比如自動鏈接到其他被跟蹤的實(shí)體類實(shí)例。
我希望這篇文章對您有用。祝你編程快樂!
[1]. https://bit.ly/2MXK3ZY
[2]. https://bit.ly/2Yza7QQ
[3]. https://bit.ly/2Y0UORO
[4]. https://bit.ly/2YEyg8j
相關(guān)文章
asp.net下UTF-7轉(zhuǎn)GB2312編碼的代碼(中文)
UTF-7轉(zhuǎn)換GB2312編碼的方法2010-07-07Asp.net內(nèi)置對象之Cookies(簡介/屬性方法/基本操作及實(shí)例)
本文將圍繞cookies了解Cookies對象/Cookie對象的屬性和方法/Cookie的基本操作及實(shí)例:Cookie的寫入和讀取/Cookie對象相比Session、Application的優(yōu)缺點(diǎn)扥等,感興趣的朋友可以了解下,或許對你學(xué)習(xí)cookies有所幫助2013-02-02ASP.NET Mvc開發(fā)之刪除修改數(shù)據(jù)
這篇文章主要介紹了ASP.NET Mvc開發(fā)中的刪除修改數(shù)據(jù)功能,感興趣的小伙伴們可以參考一下2016-03-03ASP.NET Core 數(shù)據(jù)保護(hù)(Data Protection)中篇
這篇文章主要為大家再一次介紹了ASP.NET Core 數(shù)據(jù)保護(hù)(Data Protection),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09.NET Core中如何實(shí)現(xiàn)或使用對象池?
什么是對象池?簡單來說它就是一種為對象提供可復(fù)用性能力的軟件設(shè)計(jì)思路,對象池就是通過“借”和“還”這樣兩個(gè)動作來保證對象可以被重復(fù)使用,這篇文章主要給大家介紹了關(guān)于.NET Core中如何實(shí)現(xiàn)或使用對象池的相關(guān)資料,需要的朋友可以參考下2021-07-07ASP.NET GridView控件在列上格式化時(shí)間及DataFormatString使用
在GridView綁定日期格式的時(shí)候,數(shù)據(jù)庫中的日期為2008-07-04,而GridView顯示的是2007-07-04 000000,多了后面一截很不美觀,想把它去掉不知道有什么好的方法,感興趣的朋友可以了解本文,或許對你有所幫助2013-01-01ASP.NET輸出PNG圖片時(shí)出現(xiàn)GDI+一般性錯(cuò)誤的解決方法
偶原來的用ASP.NET生成驗(yàn)證碼圖片時(shí)用的是JPG格式,今天想把它改成PNG格式的,結(jié)果就出現(xiàn)GDI+一般性錯(cuò)誤,查了N久資料,才發(fā)現(xiàn)解決的辦法,對分享此解決辦法的網(wǎng)友深表感謝2009-01-01