Dapper、Entity Framework 和混合應用

發表於2016-05-21

你大概注意到了,自 2008 年以來,我寫過許多關於 Entity Framework(即 Microsoft 物件關係對映器 (ORM))的文章,ORM 一直是主要的 .NET 資料訪問 API。市面上還有許多其他 .NET ORM,但是有一個特殊類別因其強大的效能得到的關注最高,那就是微型 ORM。我聽人提到最多的微型 ORM 是 Dapper。據不同的開發者說,他們使用 EF 和 Dapper 制定了混合解決方案,讓每個 ORM 能夠在單個應用程式中做它最擅長的事,這最終激發了我的興趣,促使我在最近抽出時間來一探究竟。

在閱讀大量文章和部落格文章,與開發者聊過天並熟悉過 Dapper 後,我想與大家分享我的一些發現,尤其是和像我這樣,可能聽說過 Dapper 但並不知道它是什麼或者並不知道它的工作原理的人分享,同時說說人們為什麼這麼喜歡它。需要提醒你的是,我根本不是什麼專家。目前我只是為了滿足我的好奇心而變得足夠了解,並且希望激發你的興趣,從而進一步探索。

為什麼是 Dapper?

Dapper 的歷史十分有趣,它是從你可能再熟悉不過的資源中衍生的: Marc Gravell 和 Sam Saffron 在研究 Stack Overflow,解決此平臺的效能問題時構建了 Dapper。考慮到 Stack Overflow 是一個流量極高的站點,那麼必然存在效能上的問題。根據 Stack Exchange About 網頁,在 2015 年,Stack Overflow 擁有 57 億的網頁瀏覽量。

在 2011 年,Saffron 撰寫過一篇關於他和 Gravell 所做的工作的部落格文章,名為“我如何學會不再擔憂和編寫我自己的 ORM”(bit.ly/),這篇文章介紹了 Stack 當時存在的效能問題,該問題源於 LINQ to SQL 的使用。他在文中詳細介紹了為什麼編寫自定義 ORM,其中 Dapper 就是優化 Stack Overflow 上的資料訪問的答案。五年後的今天,Dapper 已被廣泛使用並已成為開源軟體。Gravell 和 Stack 及團隊成員 Nick Craver 繼續在 github.com/StackExchange/dapper-dot-net 上積極地管理專案。

Dapper 簡介

Dapper 主要能夠讓你練習你的 SQL 技能,按你認為的那樣構建查詢和命令。它接近於“金屬”而非標準的 ORM,免除了解釋查詢的工作,例如將 LINQ to EF 解釋為 SQL。Dapper 不具備炫酷的轉換功能,比如打散傳遞到 WHERE IN 從句的列表。但在大多數情況下,你傳送到 Dapper 的 SQL 已準備好執行,而查詢可以更快地到達資料庫。

如果你擅長 SQL,那麼你將有把握編寫效能最高的命令。你需要建立某些型別的 IDbConnection 來執行查詢,比如帶有已知連線字串的 SqlConnection。然後,Dapper 可以通過其 API 為你執行查詢以及—假如查詢結果的架構與目標型別的屬性相匹配—自動例項化物件並向物件填充查詢結果。此處還有另一個顯著的效能優勢: Dapper 能夠有效快取它獲悉的對映,從而實現後續查詢的極速反序列化。我將填充的類 DapperDesigner(如圖 1 中所示)被定義用來管理構建整齊構架的設計器。

圖 1 DapperDesigner 類

我執行查詢的專案引用了我通過 NuGet 獲取的 Dapper(安裝包 dapper)。下面是從 Dapper 呼叫以為 DapperDesigners 表中所有行執行查詢的示例:

需要注意的是,對於本文中的程式碼清單,當我希望使用表中的所有列時,我使用的是 select * 而非明確投影的查詢列。sqlConn 連同其連線字串是現有的例項化 SqlConnection 物件,但是尚未開啟過。

Query 方法是 Dapper 提供的擴充套件方法。在此行執行時,Dapper 開啟連線,建立 DbCommand,準確地按照我編寫的內容執行查詢,例項化結果中的每行的 DapperDesigner 物件並將值從查詢結果推送到物件的屬性。Dapper 可以通過幾種方式將結果值與屬性進行匹配,即使屬性名稱與列名稱不相匹配,又或者即使屬性的順序與匹配的列的順序不同。它不會讀心術,所以別期望它弄清涉及的對映,例如列的順序或名稱和屬性不同步的大量字串值。我確實用它做了幾個奇怪的實驗,我想看看它如何響應,同時我也配置了控制 Dapper 如何推斷對映的目標設定。

Dapper 和關係查詢

我的 DapperDesigner 型別擁有多種關係,比如一對多(與產品)、一對一 (ContactInfo) 和多對多(客戶端)。我已經試驗過跨這些關係執行查詢,而且 Dapper 能夠處理這些關係。這絕對不像使用 Include 方法或投影表述 LINQ to EF 查詢那麼簡單。我的 TSQL 技能被推到極限,這是因為 EF 在過去幾年讓我變得如此懶惰。

下面是使用我在資料庫中使用 SQL 進行跨一對多關係的查詢的示例:

注意 Query 方法要求我指定兩種必須構建的型別,並指示要返回的型別—由最終型別引數 (DapperDesigner) 表述。我首先使用多行匿名函式構建圖表,將相關產品新增到其父設計器物件,然後將每個設計器返回到 Query 方法返回的 IEnumerable。

通過我對 SQL 的最佳嘗試,這樣做的不利之處在於結果是扁平的,就像使用 EF Include 方法時一樣。每個產品我將獲取一行並複製一下設計器。Dapper 擁有可以返回多個結果集的 MultiQuery 方法。與 Dapper 的 GridReader 組合,這些查詢的效能肯定將勝過 EF Includes。

編碼難度加大,執行速度變快

表述 SQL 並填充相關物件是我讓 EF 在此背景中處理的任務,所以需要更多精力來編碼。但是如果你要處理的資料量很大,那麼執行時效能非常重要,這當然值得努力。在我的示例資料庫中擁有 30,000 個設計器。僅有幾個擁有產品。我做了一些簡單的基準測試,確保我所做的是同類比較。在檢視測試結果前,有些關於我如何測量的重點需要大家理解。

請記住,預設情況下,EF 的設計目的是跟蹤為查詢結果的物件。這意味著它建立了額外的跟蹤物件(需要做一些工作),並且它也需要與這些跟蹤物件互動。而 Dapper 只是將結果轉儲到記憶體。所以當進行效能對比時,讓 EF 的更改跟蹤不再迴圈非常重要。為此,我使用 AsNoTracking 方法定義我的所有 EF 查詢。同時,當對比效能時,你需要應用大量的標準基準模式,比如給資料庫熱身、反覆執行查詢以及拋棄最慢時間和最快時間。

你可以看到我如何在下載示例中構建我的基準測試的詳情。我仍然認為這些測試是“輕量級”基準測試,此處只是為了展現差異。對於較高的基準,你需要多次迭代(500 次以上),而我只進行了 25 次,這是遠遠不夠的,同時還需要將你執行的系統的效能考慮在內。我在筆記本上使用 SQL Server LocalDB 例項進行這些測試,所以我的結果僅用於比較。

我在測試中跟蹤的的時間為執行查詢和構建結果的時間。未計入例項化連線或 DbContexts 的時間。因為反覆使用 DbContext,所以構建記憶體內模型的時間不計入內,因為每個應用程式示例僅構建一次,而不是每個查詢都要構建。

圖 2 顯示了 Dapper 和 EF LINQ 查詢的“select *”測試,從中你可以看到我的測試模式的基本構造。注意,除收集實際時間外,我還在收集每次迭代的時間並整理到列表(名為“時間”)中以供進一步分析。

圖 2 查詢所有 DapperDesigners 時 EF 與 Dapper 的對比測試

關於同類對比,還有一個問題。 Dapper 使用原始 SQL。預設情況下,使用 LINQ to EF 表述 EF 查詢並且必須做一些工作才能為你構建 SQL。一旦構建好 SQL,即使是依靠引數的 SQL,它將被快取到應用程式的記憶體,以減少重複工作。此外,EF 可以使用原始 SQL 執行查詢,所以我考慮到了這兩種方法。圖 3列出了四組測試的對比結果。下載包含更多測試。

圖 3 基於 25 次迭代執行查詢和填充物件的平均時間(以毫秒計),排除最快和最慢時間

*AsNoTracking 查詢 關係 LINQ to EF* EF Raw SQL* Dapper Raw SQL
所有設計器(3 萬行) 96 98 77
所有帶產品的設計器(3 萬行) 1 : * 251 107 91
所有帶客戶端的設計器(3 萬行) * : * 255 106 63
所有帶聯絡人的設計器(3 萬行) 1 : 1 322 122 116

圖 3 顯示的場景中,我們可以很容易地跨 LINQ to Entities 使用 Dapper 製作一個案例。但是原始 SQL 查詢之間的細微差異可能不總是在使用 EF 的系統中為特定任務切換到 Dapper 的正當理由。理所當然,大家的需求各有不同,所以這可能影響 EF 查詢和 Dapper 之間的差異程度。但是,在 Stack Overflow 等高流量系統中,甚至是每個查詢儲存的大量毫秒時間都可能至關重要。

用於其他暫留需求的 Dapper 和 EF

到目前為止,我測量了簡單查詢,並在其中從所返回型別的準確匹配屬性的表中回拉所有列。如果你將查詢投影到型別會如何呢? 只要結果構架與型別相匹配,Dapper 將無法觀察到建立物件的差異。但是,如果投影結果與為屬於模型一部分的型別不一致,EF 不得不多做些工作。

DapperDesignerContext 擁有一個針對 DapperDesigner 型別的 DbSet。在我的系統中有另一個名為 MiniDesigner 的型別,它擁有一個 DapperDesigner 屬性的子集。

MiniDesigner 不屬於我的 EF 資料模型,所以 DapperDesigner­Context 不知道這種型別。我發現與使用借用原始 SQL 的 EF 相比,使用 Dapper 查詢所有這 30,000 行並將其投影到 30,000 個 MiniDesigner 物件要快 25%。我再次建議你做自己的效能分析,併為你自己的系統做出決策。

Dapper 也可用於將資料推送到資料庫,其中包含允許你識別必須用於命令指定引數的屬性的方法,不論你使用的是原始 INSERT 或 UPDATE 命令,還是對資料庫執行函式或儲存過程。我並沒有對這些任務做任何效能對比。

現實世界中的混合 Dapper 和 EF

有許多將 Dapper 用於 100% 資料暫留的系統。但是回憶起來,我的興趣是由談論混合解決方案的開發者激起的。在某些情況下,還存在已有 EF 並希望微調特定問題區域的系統。在其他情況下,團隊選擇使用 Dapper 執行所有查詢,使用 EF 執行所有儲存。

有人回覆了我在 Twitter 上釋出的關於這方面的問題,答案千變萬化。

@garypochron 告訴我他的團隊“將 Dapper 用於高需區域並使用資原始檔維護 SQL 的組織。“ 而熱門 EF Reverse POCO Generator 的作者 Simon Hughes (@s1monhughes) 的習慣恰好相反—預設使用 Dapper,遇到棘手問題時則使用 EF,對此我感到很吃驚。他告訴我“只要可能,我都會使用 Dapper。如果是比較複雜的更新,我會使用 EF。”

我也見過各種混合方法是由於要分離關注點而非提高效能而推動的討論。最常見的討論是利用 EF 上的 ASP.NET Identity 的預設依賴性,然後在解決方案中使用 Dapper 進行其餘儲存。

除效能外,更直接地處理資料庫還擁有其他優點。SQL Server 專家 Rob Sullivan (@datachomp) 和 Mike Campbell (@angrypets) 也對 Dapper 青睞有加。Rob 指出你可以利用 EF 不允許訪問的資料庫功能,比如全文搜尋。從長期來看,特殊功能是關於效能的。

另一方面,有些任務只能使用 EF 完成,使用 Dapper 根本完成不了(更改跟蹤除外)。一個很好的例子是我在構建為本文建立的解決方案時利用的功能—即使用 EF Code First Migrations 在模型更改時遷移資料庫的能力。

Dapper 並不適合每一個人。@damiangray 告訴我 Dapper 不是他的解決方案之選,因為他需要能夠將 IQueryables(不是真實資料)從系統的一部分返回到另一部分。這個推遲執行查詢的主題已在 Dapper 的 GitHub 儲存庫中提出,如果你想詳細瞭解此主題,請訪問 bit.ly/22CJzJl

在設計混合系統時,使用 Command Query Separation (CQS) 是個不錯的方法,你可以在其中為特定型別的交易設計獨立的模型,至少我著迷於此。這樣一來,你不必設法去構建普通的資料訪問程式碼以使用 EF 和 Dapper,因為構建此程式碼通常會犧牲每個 ORM 的好處。在我創作這篇文章時,Kurt Dowswell 釋出了一篇名為“Dapper、EF 和 CQS”(bit.ly/1LEjYvA) 的博文。對我來說得心應手,對你來說亦是如此。

對於那些期待 CoreCLR 和 ASP.NET Core 的人來說,Dapper 已演變為能夠支援這些功能的軟體。你可以在 Dapper 的 GitHub 儲存庫中的文章 (bit.ly/1T5m5Ko) 中找到更多資訊。

最後,我看了看 Dapper。我認為怎麼樣?

我怎麼樣? 我很遺憾沒能儘早正視 Dapper,同時也因最終實現了願望而感到很高興。我始終推薦 AsNoTracking 或建議使用資料庫中的檢視或過程緩解效能問題。它從未讓我或我的客戶失望過。但是現在我知道我還有另一招妙計要推薦給對從使用 EF 的系統中榨出更多效能感興趣的開發者。這不是我們所謂的穩操勝券。我的建議將用來探索 Dapper、測量效能差異(大規模)以及找到效能與編碼難度之間的平衡點。

想想 StackOverflow 的顯著用途:查詢問題、註釋和答案,然後連同一些後設資料(編輯)和使用者資訊返回附有註釋和答案的問題圖表。它們反覆執行相同型別的查詢並標繪出相同形狀的結果。Dapper 的設計更擅長這種型別的反覆查詢,並且每次都會變得更智慧、更快速。即使你的系統中沒有設計為供 Dapper 處理的海量交易,你也可能找到滿足你需求的混合解決方案。

相關文章