EFCore查询语句生成流程、让EFCore支持批量Update/Delete/MergeInto
引子
之前發現了一款叫 EFCore.BulkExtensions 的 nuget 包。里面提供了大量的 BulkInsertOrUpdateOrDelete 和 BatchUpdate 的拓展,可以很方便的解決批量更新和刪除的問題,不用讓 EFCore 一條一條的刪除和更新。
其中幾個比較有用的函數簽名是
Task<int> BatchDeleteAsync(this IQueryable<T> queryable); Task<int> BatchUpdateAsync(this IQueryable<T> queryable, Expression<Func<T, T>> updateExpression);但是在升級到 ASP.NET Core 3.1 的時候,所有 Where 中的?someArray.Contains(i.Key)?全部掛掉了。而我的程序里用這一語句比較多,遂下載了其源代碼并合并了當時作者幾個月都沒合并的一個PR。
研究代碼,總結了該程序的基本運行過程:
通過反射獲取各種私有變量來訪問到 DbContext
updateExpression 由這個包自己訪問表達式樹獲得
讓 IQueryable 執行 GetEnumerator 讓 EFCore 生成對應的 Select 語句,進行字符串拼接
由 DbContext.Database.ExecuteSqlRaw 來完成語句執行
但是這過程有幾個問題:
有幾種句式 updateExpression 會翻譯不了
由其原來實現的 updateExpression 翻譯后的某些參數的 SQL 類型不對
我需要一個 INSERT INTO SELECT FROM 的句式,它不支持
我需要一個 upsert 功能,但是原來的 BulkInsertOrUpdate 不能在原表基礎上操作
遂研究?IQueryable.Provider.Execute<T>?是什么執行流程。
語句生成過程
我覺得在翻代碼的過程中,有這么一首歌比較符合我的心情:如果你愿意一層一層一層一層的撥開我的心,你會發現,你會訝異,你是我最壓抑最深處的秘密。
調用?QueryCompiler.ExtractParameters,將其中的閉包捕捉變量參數化
檢查是否已經緩存了這個查詢表達式,如果沒有則轉入?QueryCompilationContext?處理,否則轉到8
QueryTranslationPreprocessor?處理,在原來的表達式樹上先跳舞
QueryableMethodTranslatingExpressionVisitor?將原來的表達式樹翻譯成一個?ShapedQueryExpression,而這一個表達式則包含了幾個部分:SelectExpression、ShaperExpression?和?ResultCardinality。其中前者是可以翻譯成 SQL 語句的表達式,中間的是將查詢出來的元組映射到實體類型,最后一個是查詢的維度(Enumerable、Single、SingleOrDefault)
QueryTranslationPostprocessor?處理,其中比較重要的是將查詢的字段加入 SELECT 的 Projection 列表
ShapedQueryCompilingExpressionVisitor?將?ShapedQueryExpression?緩存,并轉換成為?IRelationcalCommandCache,然后構造一個?QueryableEnumerable?的 NewExpression。前者包含了該查詢語句需要的參數、查詢語法樹、查詢字符串,后者是進行語句執行的類
將上述 NewExpression 和將?QueryCompilationContext?中的查詢參數加到?QueryContext?中的語句合并成為一個代碼塊,然后 Lambda Compile
生成?DbCommand?由?IRelationcalCommandCache?獲取字符串并加入各種參數進行查詢
翻譯結束了,查詢到這里也就可以開始了。
支持批量操作?
IRelationalCommandCache?是怎么生成字符串的呢?沒錯,就是?QuerySqlGenerator?啦。
那么,也就是說,我們能過拿到 Select Expression 的話,一切都好說。
上述過程中,最后的?IRelationalCommandCache?中會包含這個?SelectExpression。我們可以魔改這個啊!
DELETE?語句的生成比較簡單。我們構建一個?DeleteExpression?類,將要刪除的 Table、刪除中的 Predicate、刪除個數限制 Limit、原來的一些 Join 全部獲取出來,就好了。然后在我們自己繼承的?SqlServerQuerySqlGenerator?中實現這個部分。
INSERT INTO SELECT?也比較簡單,只要構建一個?InsertIntoSelectExpression?類,將要插入的表 Table 和 SelectExpression 保存起來,就好了。
UPDATE SET?可能比較麻煩。但是我們可以騷操作啊!將那個 updateExpression 變成 Select 的字段,然后再讀取 SelectExpression 中的 ProjectionExpression 不就好了嗎~我真是個小天才。
MERGE INTO?是最煩的,因為結構過于復雜,涉及到 Target、Source、JoinPredicate、Limit、Matched、NotMatchedByTarget、NotMatchedBySource。過程中還要實現一些表的更名之類的。目前我只是實現了這些,但是想做出?Matched When?功能以后再發布到 nuget 上,這個實現實在是過于復雜,不知道有沒有人幫幫我啊 TAT。
由于翻譯?SqlExpression?最方便還是基于?QuerySqlGenerator?操作,所以就寫一個?EnhancedQuerySqlGenerator?類來滿足我們的需求,并在 DbContextOptionsBuilder 那邊將這個 Factory 替換掉。
實現了這些,GitHub 地址:Microsoft.EntityFrameworkCore.Bulk,可以在 github packages 上下載目前版本的 nuget 包。
另外?src/Internal/TranslationGoThrough.cs?中有上述語句生成過程的一個縮影,和系統版本幾乎一致,唯一不同的是修改了?ExtractParameters?函數。
因為原來的 Extract 過程有一個事情很詭異:在生成參數的時候,我們可以進行一些本地執行,但是如果不阻止某些本地執行程的話,可能會導致 UPDATE 語句的字段全部空。例如 updateExpression 中沒有利用到原表的參數并且不捕捉閉包變量的時候,那么不會被本地執行,但是如果沒有利用到原表的參數還捕捉閉包變量的時候,它就會被直接本地執行,字段空啦~(確實不懂他們這段代碼邏輯怎么寫的,你生成查詢的時候優化這個的話,怎么不把前面一個也優化掉啊……
原文地址:https://www.90yang.com/efcore-query-sql-generation/
總結
以上是生活随笔為你收集整理的EFCore查询语句生成流程、让EFCore支持批量Update/Delete/MergeInto的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 科个普:进程、线程、并发、并行
- 下一篇: 【翻译】.NET 5 Preview2发