EFCore查缺补漏(二):查询
相關文章:?EFCore查缺補漏
第 20 輪 TechEmpower 評測結果出爐了,ASP.NET Core 的 Plaintext 成績名列前茅,帶著 EFCore 的測試卻在 Single query / Multiple queries / Fortunes 中落了下風,成績遠不如 dapper,更不如直接 ado.net。
人人都說 EFCore 性能差,人人都在寫性能低的代碼……
EFCore 是如何進行查詢的?除了查詢語句本身的合理性,EFCore 本身的性能瓶頸又會出現在哪里呢?如何讓 EFCore 的查詢變得更快呢?
今天,先從?IQueryable?這個接口說起。
IQueryable 與 IEnumerable
IEnumerable<>?的核心作用是提供一些基礎數據通過?GetEnumerator?函數來創建一個?IEnumerator<>。
IEnumerator<>?是一個非常單純的單向迭代器,你可以像系統自帶的集合類那樣手動實現一個,也可以你可以通過自己編寫?yield return?/?yield break?語句,讓編譯器將你的程序控制流和變量狀態保存在編譯器翻譯設計的專有?Enumerator?中。
除此之外,System.Linq?這個命名空間提供了大量針對?IEnumerable<>?的拓展。這些拓展將?IEnumerator<>?們通過類似于責任鏈模式的方法組合起來,提供了很多神奇的 LINQ 功能。
而?IQueryable<>?是什么呢?
IQueryable<>?接口除了實現?IEnumerable<>?以外,還有三個成員
Expression:保存了一個表達式樹
ElementType:這個?IQueryable<>?的返回類型
Provider:一個?IQueryProvider?實例對象
而?IQueryProvider?則有這樣幾個成員函數
IQueryable<> CreateQuery(Expression expression)?根據傳入的表達式樹構建一個?IQueryable?對象
TResult Execute(Expression expression)?執行這個表達式,獲得對應結果
再參考?System.Linq.Queryable?對?IQueryable<>?的拓展函數的實現
// System.Linq.Queryable public static int Count<TSource>(this IQueryable<TSource> source) {if (source == null)throw Error.ArgumentNull(nameof(source));return source.Provider.Execute<int>(Expression.Call(null,CachedReflectionInfo.Count_TSource_1(typeof(TSource)),source.Expression)); }public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate) {if (source == null)throw Error.ArgumentNull(nameof(source));if (predicate == null)throw Error.ArgumentNull(nameof(predicate));return source.Provider.CreateQuery<TSource>(Expression.Call(null,CachedReflectionInfo.Where_TSource_2(typeof(TSource)),source.Expression, Expression.Quote(predicate))); }那么我們有如下結論
IQueryable<>?保存著一個查詢表達式樹和一個?IQueryProvider
IQueryProvider?支撐著?IQueryable<>?的創建和查詢執行
IQueryable<>?的拓展函數們僅僅是將表達式樹拼接成與函數調用相同形態的表達式
而在 EFCore 中,完成這樣功能的類則是?EntityQueryable<>?和?EntityQueryProvider。后者在 EFCore 的依賴注入容器中是 Scoped 服務?IAsyncQueryProvider?的實現,完成所有的?IQueryable<>?的創建,并將所有的?Execute?和?ExecuteAsync?的請求轉發給?IQueryCompiler?這一服務。
而?IQueryCompiler?中的執行代碼大約是這樣的
// Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler public virtual TResult Execute<TResult>(Expression query) {Check.NotNull(query, nameof(query));var queryContext = _queryContextFactory.Create();query = ExtractParameters(query, queryContext, _logger);var compiledQuery= _compiledQueryCache.GetOrAddQuery(_compiledQueryCacheKeyGenerator.GenerateCacheKey(query, async: false),() => CompileQueryCore<TResult>(_database, query, _model, false));return compiledQuery(queryContext); }其中
_compiledQueryCache?是一個由?IMemoryCache?驅動的緩存
_compiledQueryCacheKeyGenerator?是將該表達式與數據庫模型、數據庫驅動等信息的 Hash 值合并產生一個對應的?QueryCacheKey
_queryContextFactory?用于生成一個?QueryContext?實例
ExtractParameters?將查詢表達式中的閉包變量等計算完畢并加入?queryContext?實例
compiledQuery?是一個?Func<QueryContext, TResult>?實例,又稱為?QueryExecutor
而其中?QueryContext?支持和提供
并行檢測(也就是 EFCore 一個上下文實例只能有一個正在執行的查詢語句的基石)
執行邏輯(例如失敗重試)
異步任務的 CancellationToken
各種日志輸出
查詢參數的添加和讀取
實體跟蹤和狀態管理
在關系型數據庫驅動中,RelationalQueryContext?另外附加
數據庫連接
生成 SQL 片段的工廠類
我們每次要執行一個查詢,就要先在這個內存緩存中查找是否已經有編譯好的執行語句;而這個緩存的鍵需要利用表達式樹來生成。如果我們的查詢過于復雜,則會對緩存帶來一定的性能負擔。
Expression 樹與 EF.CompileQuery
我們稍后討論?CompileQueryCore?的作用。先來討論一下表達式樹吧。
眾所周知,EFCore 的強類型特性是由表達式樹這個玩意帶來的。
編譯器為了減少人為構建表達式樹的負擔,提供了語法糖,讓我們可以像寫 Lambda 函數一樣書寫表達式。然而編譯器并沒有開洞,而是實打實的進行了表達式樹的構建。
可以在圖上看到,我們經常執行的根據 ID 查找一個實體的操作會產生如此之多的中間代碼。甚至,藍框內還有閉包變量捕捉的步驟。
如果我們的查詢很簡單,那似乎也沒什么……如果我們要執行一個超級復雜的查詢,又要 join 好幾個表又要 concat 還要 group 呢?
表達式樹畢竟是表達式樹,為了創建表達式樹,這么多中間代碼總是需要執行的。
有沒有辦法直接跳過這么多表達式樹的構建呢?有的。看?EF.CompileQuery。其中的一個典型函數
public static Func<TContext, IEnumerable<TResult>> CompileQuery<TContext, TResult>([NotNull] Expression<Func<TContext, IQueryable<TResult>>> queryExpression)where TContext : DbContext=> new CompiledQuery<TContext, IEnumerable<TResult>>(queryExpression).Execute;這里實際上是構建了一個?CompileQuery<,>?類型的對象,并且將他的 Execute 函數打包成委托返回。
你可能會問,這不是還需要表達式樹嗎?
那么請回顧官方文檔中這個函數的使用場景:將該函數返回的委托放在 靜態字段 或者 單例的成員變量 中。也就是說,對于某個參數設計好了的函數,這個?EF.CompileQuery?函數理應只執行一次。
CompiledQuery 對象在第一次執行時,通過接下來的代碼創建一個?Func<QueryContext, TResult>?委托;在這個委托中,本次執行產生的 SQL 語句表達式樹、SQL 語句文本會被緩存下來;在第二次執行的時候,就跳過對表達式樹的處理,直接執行上面那個委托,甚至在特定情況下快進到直接執行 SQL 語句文本了。
// Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler public virtual Func<QueryContext, TResult> CreateCompiledQuery<TResult>(Expression query) {Check.NotNull(query, nameof(query));query = ExtractParameters(query, _queryContextFactory.Create(), _logger, parameterize: false);return CompileQueryCore<TResult>(_database, query, _model, false); }是不是感覺會快上很多?我們可以直接獲得這個委托,而不是通過字典查找,可以節省很多時間;而且不涉及查詢語句緩存,不會存在系統內其他查詢太多、某些不常用查詢被定期清理掉的情況。
個人認為,在需要比較高性能的同時,又不是直接執行 SQL 語句文本的情況下,這樣兩種情況可以嘗試使用?EF.CompileQuery:
查詢表達式樹太過復雜
高頻訪問路徑上的查詢
另外,如果查詢是只讀,不涉及到實體的增刪改,此時完全可以考慮到使用?AsNoTracking?這類拓展,將實體更改追蹤關掉,在此基礎上可以再提高一點性能,大約能與 Dapper 和 ADO.NET 直接讀取數據的性能比肩(沒有測試數據)。
筆者在閱讀網上現存的對?EF.CompileQuery?的介紹中,有讀到過一篇說這個函數不接受?ToList?和?ToListAsync?之類的函數,說這個功能支持不完全。
ToList?是?IEnumerable<>?的拓展方法,并不是?IQueryable<>?的拓展方法。也就是說,ToList?實際上是將之前的?IQueryable<>?進行了?foreach?枚舉,并手動構建?List<>?對象,所以說不支持似乎情有可原。
而?ToListAsync?是 EFCore 的拓展方法。實際上,他的代碼是這樣的:
public static async Task<List<TSource>> ToListAsync<TSource>([NotNull] this IQueryable<TSource> source,CancellationToken cancellationToken = default) {var list = new List<TSource>();await foreach (var element in source.AsAsyncEnumerable().WithCancellation(cancellationToken)){list.Add(element);}return list; }對,利用了?IAsyncEnumerable?和?await foreach?來達到異步查詢的目的。所以,當我們需要使用?ToList、ToArray、ToDictionary?類似功能的時候,使用那個?Func<TContext, IAsyncEnumerable<TResult>>?然后手動構架集合就好了。
這里再簡單給幾個使用這個函數使用的例子吧。
private static readonly Func<MyContext, int, Task<User>> _findUser =EF.CompileAsyncQuery((MyContext context, int id) => context.Users.Where(u => u.Id == id).FirstOrDefault());private static readonly Func<MyContext, IAsyncEnumerable<UserDto>> _listUsers =EF.CompileAsyncQuery((MyContext context) => context.Users.Select(u => new UserDto(u.Id, u.Name, true)));private static readonly Func<MyContext, DateTimeOffset, CancellationToken, Task<int>> _countUsers =EF.CompileAsyncQuery((MyContext context, DateTimeOffset time, CancellationToken _) => context.Users.Where(u => u.RegisterTime < time).Count());public async Task DoAsync(CancellationToken cancellationToken = default) {using var context = CreateContext();var user = await _findUser(context, 233);var list = new List<UserDto>();await foreach (var item in _listUsers(context).WithCancellation(cancellationToken)){list.Add(item);}var count = await _countUsers(context, DateTimeOffset.Now.AddDays(-1), cancellationToken); }可以創建帶有 CancellationToken 的異步版本。同步版本就不用 CancellationToken 了。
真正的查詢編譯與執行
我們需要結合 SqlServer 這個關系型數據庫解釋所謂的?QueryExecutor。其他關系型數據庫的大致構建過程其實差不多,非關系型的 InMemory 和 Cosmos 的驅動用的少就不解釋了哈。
以?context.Set<User>().Where(u => u.Id != id).ToList()?這一查詢的翻譯為例。
我們可以發現,這個查詢的?QueryExecutor?是?Func<QueryContext, IEnumerable<User>>,傳入?queryContext?會返回一個?IEnumerable?對象。
在 EFCore 3.1 中,返回的是?QueryingEnumerable;在 EFCore 5.0 中,返回?SingleQueryingEnumerable?或者?SplitQueryingEnumerable?或者?FromSqlQueryingEnumerable。5.0 的改動是因為帶來了?AsSplitQuery?這個拓展,避免笛卡爾爆炸的問題。
先跳過?QueryExecutor?函數體,看看返回值。
以?SingleQueryingEnumerable?為例,我們看到它實現了?IEnumerable?和?IAsyncEnumerable。以下是?GetEnumerator?結果的?MoveNext?和?InitializeReader?的實現。
private bool InitializeReader(DbContext _, bool result) {EntityFrameworkEventSource.Log.QueryExecuting();var relationalCommand = _relationalCommandCache.GetRelationalCommand(_relationalQueryContext.ParameterValues);_dataReader = relationalCommand.ExecuteReader(new RelationalCommandParameterObject(_relationalQueryContext.Connection,_relationalQueryContext.ParameterValues,_relationalCommandCache.ReaderColumns,_relationalQueryContext.Context,_relationalQueryContext.CommandLogger,_detailedErrorsEnabled));_resultCoordinator = new SingleQueryResultCoordinator();_relationalQueryContext.InitializeStateManager(_standAloneStateManager);return result; }public bool MoveNext() {try{using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()){if (_dataReader == null){_relationalQueryContext.ExecutionStrategyFactory.Create().Execute(true, InitializeReader, null);}var hasNext = _resultCoordinator.HasNext ?? _dataReader.Read();Current = default;if (hasNext){while (true){_resultCoordinator.ResultReady = true;_resultCoordinator.HasNext = null;Current = _shaper(_relationalQueryContext, _dataReader.DbDataReader, _resultCoordinator.ResultContext,_resultCoordinator);if (_resultCoordinator.ResultReady){// We generated a result so null out previously stored values_resultCoordinator.ResultContext.Values = null;break;}if (!_dataReader.Read()){_resultCoordinator.HasNext = false;// Enumeration has ended, materialize last element_resultCoordinator.ResultReady = true;Current = _shaper(_relationalQueryContext, _dataReader.DbDataReader, _resultCoordinator.ResultContext,_resultCoordinator);break;}}}return hasNext;}}catch (Exception exception){_queryLogger.QueryIterationFailed(_contextType, exception);throw;} }其中
ExecutionStrategy?處理查詢重試之類的執行邏輯
_relationalCommandCache?保存了翻譯的 SQL 表達式和語句文本
_dataReader?保存著 ADO.NET 中常用的?DbCommand,DbDataReader,DbConnection?等工具,是的,底層讀取庫就是 ADO.NET
_resultCoordinator?將 ADO.NET 讀取的一行結果存入?ResultContext,然后當一條記錄完整讀取以后(如果有集合 Include,則是集合整個讀取完成),由?_shaper?轉換成最終實體
這個?QueryingEnumerable?是如何構建出來的?終于到了喜聞樂見的?CompileQueryCore?函數講解了。
// Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler public virtual Func<QueryContext, TResult> CompileQueryCore<TResult>([NotNull] IDatabase database,[NotNull] Expression query,[NotNull] IModel model,bool async)=> database.CompileQuery<TResult>(query, async);// Microsoft.EntityFrameworkCore.Storage.Database public virtual Func<QueryContext, TResult> CompileQuery<TResult>(Expression query, bool async)=> Dependencies.QueryCompilationContextFactory.Create(async).CreateQueryExecutor<TResult>(query);// Microsoft.EntityFrameworkCore.Query.QueryCompilationContext public virtual Func<QueryContext, TResult> CreateQueryExecutor<TResult>(Expression query) {query = _queryTranslationPreprocessorFactory.Create(this).Process(query);// Convert EntityQueryable to ShapedQueryExpressionquery = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Visit(query);query = _queryTranslationPostprocessorFactory.Create(this).Process(query);// Inject actual entity materializer// Inject trackingquery = _shapedQueryCompilingExpressionVisitorFactory.Create(this).Visit(query);// If any additional parameters were added during the compilation phase (e.g. entity equality ID expression),// wrap the query with code adding those parameters to the query contextquery = InsertRuntimeParameters(query);var queryExecutorExpression = Expression.Lambda<Func<QueryContext, TResult>>(query,QueryContextParameter);try{return queryExecutorExpression.Compile();}finally{Logger.QueryExecutionPlanned(new ExpressionPrinter(), queryExecutorExpression);} }嗯,這玩意……老套娃人了。
這里的代碼其實挺抽象的。另外不要被那個?.Compile()?嚇到,那個只是利用表達式樹把動態構建的函數體生成為真正的委托對象而已,我們需要在它 Compile 之前看到這個函數體內部究竟是什么。
至于查詢翻譯過程本身的設計,這次先不介紹。
依然是上面那個例子,調試 EFCore 源代碼設斷點,可以看到,此時?queryExecutor?是這樣的:
return new SingleQueryingEnumerable<User>(relationalQueryContext: (RelationalQueryContext)queryContext,relationalCommandCache: value(RelationalCommandCache),shaper: value(Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, User>),contextType: typeof(MyContext),standAloneStateManager: False,detailedErrorsEnabled: False);在剛才的過程中,?InsertRuntimeParameters?并沒有實際發揮作用,因為閉包捕捉的?id?變量早在?QueryCompiler.ExtractParameters?時就變為了?ParameterExpression?并被加入?QueryContext,而并沒有在查詢翻譯過程中使用任何“運行時參數”。
在不使用導航屬性的情況下,試了幾個常見的例子,基本上都不會觸發“運行時參數”。筆者找到了這樣的兩個例子:
public class User {public int Id { get; set; } }public class Tenant {public int Id { get; set; }public ICollection<User> Users { get; set; } }modelBuilder.Entity<Tenant>(entity => entity.HasMany(e => e.Users).WithOne());var uu = new User { /* .. */ }; context.Users.Where(u => u.Equals(uu)).ToList(); context.Tenants.Where(t => t.Users.Contains(uu)).ToList();C#
Copy
此時?queryExecutor?是這樣的:
queryContext.AddParameter("__entity_equality_uu_0_Id",new Func<QueryContext, int?>(queryContext => ParameterValueExtractor(queryContext, "__uu_0", IProperty)).Invoke(queryContext));return new SingleQueryingEnumerable<User>(relationalQueryContext: (RelationalQueryContext)queryContext,relationalCommandCache: value(RelationalCommandCache),shaper: value(Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, User>),contextType: typeof(MyContext),standAloneStateManager: False,detailedErrorsEnabled: False);也就是說,所謂“運行時參數”是 SQL 語句執行時的參數,但是,要想讀取它的值,就需要反過來從?QueryContext?中讀取。為什么會在?QueryContext?中呢?因為之前?QueryCompiler.ExtractParameters?的時候,這個對象的整體被加入了?QueryContext?中,而不是被直接計算好。對于需要判斷實體包含和實體相等的情況,就需要用到這種奇怪的方法。
現在問題來了:relationalCommandCache?和?shaper?是在何時構建好的?
前者保存著翻譯完成的 SQL 表達式樹的對象,他會在創建?DbCommand?對象的時候,調用?QuerySqlGenerator?將表達式樹拍平成為 SQL 語句文本。
這里我們來研究一下后者這個?shaper?委托。
這個委托也是由 EFCore 動態創建的,但是這個委托的具體實現是和數據庫類型有關系的。在關系型數據庫中,由?RelationalShapedQueryCompilingExpressionVisitor?進行創建。
對于剛才直接拿到實體的情況,它產生的代碼是這樣的
// SELECT [u].[Id], [u].[Name], [u].[RegisterTime], [u].[TenantId] // FROM [Users] AS [u]User Shape(QueryContext queryContext, DbDataReader dataReader, ResultContext resultContext, SingleQueryResultCoordinator resultCoordinator) {User var1 ={IEntityType entityType1;var materializationContext1 = new MaterializationContext(valueBuffer: ValueBuffer.Empty,context: queryContext.Context);User instance1 = null;InternalEntityEntry entry1 = queryContext.TryGetEntry(key: value(IKey: "Key: User.Id PK"),keyValues: new object[] { dataReader.GetInt32(0) },throwOnNullKey: True,out bool hasNullKey1));if (!hasNullKey1){if (entry1 != default(InternalEntityEntry)){entityType1 = entry1.EntityType;instance1 = (User)entry1.Entity;}else{ValueBuffer shadowValueBuffer1 = ValueBuffer.Empty;entityType1 = value("EntityType: User");instance1 = entityType1 switch{value("EntityType: User") =>{// EFCore生成的shadow property,此處為 int? TenantIdshadowValueBuffer1 = new ValueBuffer(new[]{dataReader.IsDBNull(3) ? default(object) : dataReader.GetInt32(3)});User instance = new User();instance.<Id>k__BackingField = dataReader.GetInt32(0);instance.<Name>k__BackingField = dataReader.IsDBNull(1)? default(string): dataReader.GetString(1);instance.<RegisterTime>k__BackingField = dataReader.GetFieldValue(2);block-return instance;},_ => null,};entry1 = entityType1 == default(IEntityType)? default(InternalEntityEntry): queryContext.StartTracking(entityType1, instance1, shadowValueBuffer1);}}block-return instance1;};return var1; }注意此處擺出的代碼是從 EFCore 生成的表達式樹改寫而來,與原來的表達式樹并不完全相同,原來的一些寫法在 C# 中無法直接表達(例如 kotlin 那樣,一對花括號最后一個值作為整個花括號的值,此處用?block-return?表示;以及?default(void)?作為三元表達式值的使用),所以稍有改寫。
扔代碼出來不是讓大家看懂,而是讓大家體會一下。Don’t try to understand it, feel it.
可以看到大致的實體生成過程,以及實體跟蹤的流程:先看上下文是否已經追蹤了這樣的實體,有則直接使用,無則跳過。
而?switch?則是給實體繼承關系做出的設計。在有實體繼承的情況下,entityType1?的值是通過讀取查詢結果某個 Shadow Property 字段來確定的。
如果使用?AsNoTracking?標記查詢呢?
// SELECT [u].[Id], [u].[Name], [u].[RegisterTime], [u].[TenantId] // FROM [Users] AS [u]User Shape(QueryContext queryContext, DbDataReader dataReader, ResultContext resultContext, SingleQueryResultCoordinator resultCoordinator) {User var1 ={IEntityType entityType1;var materializationContext1 = new MaterializationContext(valueBuffer: ValueBuffer.Empty,context: queryContext.Context);User instance1 = null;if (((object)dataReader.GetInt32(0)) != null){ValueBuffer shadowValueBuffer1 = ValueBuffer.Empty;entityType1 = value("EntityType: User");instance1 = entityType1 switch{value("EntityType: User") =>{User instance = new User();instance.<Id>k__BackingField = dataReader.GetInt32(0);instance.<Name>k__BackingField = dataReader.IsDBNull(1)? default(string): dataReader.GetString(1);instance.<RegisterTime>k__BackingField = dataReader.GetFieldValue(2);block-return instance;},_ => null,};}block-return instance1;};return var1; }可以看到,實體跟蹤相關的代碼沒了,Shadow Property 相關的也沒了,畢竟上下文不追蹤這個實體,怎么會知道有哪些虛擬屬性呢。上下文能有什么壞心思呢。
如果是查詢中 Select 創建了一個非實體類型呢?(這里其實和?.Count()、.Sum()?之類的函數效果差不多)
例如?context.Users.Select(u => new UserDto(u.Id, u.Name, false)).ToList();。
// SELECT [u].[Id], [u].[Name] // FROM [Users] AS [u]UserDto Shape(QueryContext queryContext, DbDataReader dataReader, ResultContext resultContext, SingleQueryResultCoordinator resultCoordinator) {var param0 = (int?)dataReader.GetInt32(0);var param1 = dataReader.IsDBNull(1) ? default(string) : dataReader.GetString(1);return new UserDto((int)param0, param1, false); }嗯,甚至直接跳過了?IEntityType?的檢查……不過也正常,畢竟這里沒有一個實體對應多種 CLR 類型的狀況。
再來一個使用了單個實體 Include 的吧。以?context.Users.Include(u => u.Tenant).ToListAsync()?為例
// SELECT [u].[Id], [u].[Name], [u].[RegisterTime], [u].[TenantId], [t].[Id] // FROM [Users] AS [u] // LEFT JOIN [Tenants] AS [t] ON [u].[TenantId] = [t].[Id]User Shape(QueryContext queryContext, DbDataReader dataReader, ResultContext resultContext, SingleQueryResultCoordinator resultCoordinator) {User var1 ={IEntityType entityType1;var materializationContext1 = new MaterializationContext(valueBuffer: ValueBuffer.Empty,context: queryContext.Context);User instance1 = null;InternalEntityEntry entry1 = queryContext.TryGetEntry(key: value(IKey: "Key: User.Id PK"),keyValues: new object[] { dataReader.GetInt32(0) },throwOnNullKey: True,out bool hasNullKey1));if (!hasNullKey1) { ... } // 此處與上述帶 Tracking 的類似block-return instance1;};Tenant var2 ={IEntityType entityType2;var materializationContext2 = new MaterializationContext(valueBuffer: ValueBuffer.Empty,context: queryContext.Context);Tenant instance2 = null;InternalEntityEntry entry2 = queryContext.TryGetEntry(key: value(IKey: "Key: Tenant.Id PK"),keyValues: new object[] { dataReader.IsDBNull(4) ? default(object) : dataReader.GetInt32(4) },throwOnNullKey: False,out bool hasNullKey2));if (!hasNullKey2) { ... } // 此處與上述帶 Tracking 的類似block-return instance2;};IncludeReference(queryContext: queryContext,entity: var1,relatedEntity: var2,navigation: value("Navigation: User.Tenant (Tenant) ToPrincipal Tenant Inverse: Users"),inverseNavigation: value("Navigation: Tenant.Users (ICollection<User>) Collection ToDependent User Inverse: Tenant"),fixup: (entity, relatedEntity) =>{entity.<Tenant>k__BackingField = relatedEntity;// value(ClrICollectionAccessor<Tenant, ICollection<User>, User>) = inverseNavigation.GetCollectionAccessor()value(IClrICollectionAccessor).Add(relatedEntity, entity, forMaterialization: True);},trackingQuery: True);return var1; }// Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor+ShaperProcessingExpressionVisitor private static void IncludeReference<TEntity, TIncludingEntity, TIncludedEntity>(QueryContext queryContext,TEntity entity,TIncludedEntity relatedEntity,INavigationBase navigation,INavigationBase inverseNavigation,Action<TIncludingEntity, TIncludedEntity> fixup,bool trackingQuery)where TEntity : classwhere TIncludingEntity : class, TEntitywhere TIncludedEntity : class {if (entity is TIncludingEntity includingEntity){if (trackingQuery&& navigation.DeclaringEntityType.FindPrimaryKey() != null){// For non-null relatedEntity StateManager will set the flagif (relatedEntity == null){queryContext.SetNavigationIsLoaded(includingEntity, navigation);}}else{navigation.SetIsLoadedWhenNoTracking(includingEntity);if (relatedEntity != null){fixup(includingEntity, relatedEntity);if (inverseNavigation != null&& !inverseNavigation.IsCollection){inverseNavigation.SetIsLoadedWhenNoTracking(relatedEntity);}}}} }再來一個集合 Include 的吧。集合的特別復雜。
以?context.Tenants.Include(t => t.Users).ToListAsync()?為例
// SELECT [t].[Id], [u].[Id], [u].[Name], [u].[RegisterTime], [u].[TenantId] // FROM [Tenants] AS [t] // LEFT JOIN [Users] AS [u] ON [t].[Id] = [u].[TenantId] // ORDER BY [t].[Id], [u].[Id]Tenant Shape(QueryContext queryContext, DbDataReader dataReader, ResultContext resultContext, SingleQueryResultCoordinator resultCoordinator) {if (resultContext.Values == null){Tenant var1 ={var materializationContext1 = new MaterializationContext(valueBuffer: ValueBuffer.Empty,context: queryContext.Context);Tenant instance1 = null;InternalEntityEntry entry1 = queryContext.TryGetEntry(key: value(IKey: "Key: Tenant.Id PK"),keyValues: new object[] { dataReader.GetInt32(0) },throwOnNullKey: True,out bool hasNullKey1));if (!hasNullKey1) { ... } // 此處與上述帶 Tracking 的類似block-return instance1;};resultContext.Values = new[] { var1 };InitializeIncludeCollection(collectionId: 0,queryContext: queryContext,dbDataReader: dataReader,resultCoordinator: resultCoordinator,entity: (Tenant)resultContext.Values[0],parentIdentifier: (queryContext, dataReader) => new object[] { (int?)dataReader.GetInt32(0) },outerIdentifier: (queryContext, dataReader) => new object[] { (int?)dataReader.GetInt32(0) },navigation: value("Navigation: Tenant.Users (ICollection<User>) Collection ToDependent User Inverse: Tenant"),clrCollectionAccessor: value(ClrCollectionAccessor),trackingQuery: True);}PopulateIncludeCollection(collectionId: 0,queryContext: queryContext,dbDataReader: dataReader,resultCoordinator: resultCoordinator,parentIdentifier: (queryContext, dataReader) => new object[] { (int?)dataReader.GetInt32(0) },outerIdentifier: (queryContext, dataReader) => new object[] { (int?)dataReader.GetInt32(0) },selfIdentifier: (queryContext, dataReader) => new object[] { dataReader.IsDBNull(1) ? default(int?) : (int?)dataReader.GetInt32(1) },parentIdentifierValueComparers: value(IReadOnlyList<ValueComparer>),outerIdentifierValueComparers: value(IReadOnlyList<ValueComparer>),selfIdentifierValueComparers: value(IReadOnlyList<ValueComparer>),innerShaper: (queryContext, dataReader, resultContext, resultCoordinator) =>{User var1 ={var materializationContext2 = new MaterializationContext(valueBuffer: ValueBuffer.Empty,context: queryContext.Context);User instance2 = null;InternalEntityEntry entry2 = queryContext.TryGetEntry(key: value(IKey: "Key: User.Id PK"),keyValues: new[] { dataReader.IsDBNull(1) ? default(object) : dataReader.GetInt32(1) },throwOnNullKey: False,out bool hasNullKey2));if (!hasNullKey2) { ... } // 此處與上述帶 Tracking 的類似block-return instance2;};return var1;},inverseNavigation: value("Navigation: User.Tenant (Tenant) ToPrincipal Tenant Inverse: Users"),fixup: (Tenant including, User included) =>{value(IClrICollectionAccessor).Add(including, included, True);included.<Tenant>k__BackingField = including;},trackingQuery: True);return resultCoordinator.ResultReady? (Tenant)resultContext.Values[0]: default(Tenant); }// Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor+ShaperProcessingExpressionVisitor private static void InitializeIncludeCollection<TParent, TNavigationEntity>(int collectionId,QueryContext queryContext,DbDataReader dbDataReader,SingleQueryResultCoordinator resultCoordinator,TParent entity,Func<QueryContext, DbDataReader, object[]> parentIdentifier,Func<QueryContext, DbDataReader, object[]> outerIdentifier,INavigationBase navigation,IClrCollectionAccessor clrCollectionAccessor,bool trackingQuery)where TParent : classwhere TNavigationEntity : class, TParent {object collection = null;if (entity is TNavigationEntity){if (trackingQuery){queryContext.SetNavigationIsLoaded(entity, navigation);}else{navigation.SetIsLoadedWhenNoTracking(entity);}collection = clrCollectionAccessor.GetOrCreate(entity, forMaterialization: true);}var parentKey = parentIdentifier(queryContext, dbDataReader);var outerKey = outerIdentifier(queryContext, dbDataReader);var collectionMaterializationContext = new SingleQueryCollectionContext(entity, collection, parentKey, outerKey);resultCoordinator.SetSingleQueryCollectionContext(collectionId, collectionMaterializationContext); }// Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor+ShaperProcessingExpressionVisitor private static void PopulateIncludeCollection<TIncludingEntity, TIncludedEntity>(int collectionId,QueryContext queryContext,DbDataReader dbDataReader,SingleQueryResultCoordinator resultCoordinator,Func<QueryContext, DbDataReader, object[]> parentIdentifier,Func<QueryContext, DbDataReader, object[]> outerIdentifier,Func<QueryContext, DbDataReader, object[]> selfIdentifier,IReadOnlyList<ValueComparer> parentIdentifierValueComparers,IReadOnlyList<ValueComparer> outerIdentifierValueComparers,IReadOnlyList<ValueComparer> selfIdentifierValueComparers,Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, TIncludedEntity> innerShaper,INavigationBase inverseNavigation,Action<TIncludingEntity, TIncludedEntity> fixup,bool trackingQuery)where TIncludingEntity : classwhere TIncludedEntity : class {var collectionMaterializationContext = resultCoordinator.Collections[collectionId];if (collectionMaterializationContext.Parent is TIncludingEntity entity){if (resultCoordinator.HasNext == false){// Outer Enumerator has endedGenerateCurrentElementIfPending();return;}if (!CompareIdentifiers(outerIdentifierValueComparers,outerIdentifier(queryContext, dbDataReader), collectionMaterializationContext.OuterIdentifier)){// Outer changed so collection has ended. Materialize last element.GenerateCurrentElementIfPending();// If parent also changed then this row is now pointing to element of next collectionif (!CompareIdentifiers(parentIdentifierValueComparers,parentIdentifier(queryContext, dbDataReader), collectionMaterializationContext.ParentIdentifier)){resultCoordinator.HasNext = true;}return;}var innerKey = selfIdentifier(queryContext, dbDataReader);if (innerKey.All(e => e == null)){// No correlated elementreturn;}if (collectionMaterializationContext.SelfIdentifier != null){if (CompareIdentifiers(selfIdentifierValueComparers, innerKey, collectionMaterializationContext.SelfIdentifier)){// repeated row for current element// If it is pending materialization then it may have nested elementsif (collectionMaterializationContext.ResultContext.Values != null){ProcessCurrentElementRow();}resultCoordinator.ResultReady = false;return;}// Row for new element which is not first element// So materialize the elementGenerateCurrentElementIfPending();resultCoordinator.HasNext = null;collectionMaterializationContext.UpdateSelfIdentifier(innerKey);}else{// First row for current elementcollectionMaterializationContext.UpdateSelfIdentifier(innerKey);}ProcessCurrentElementRow();resultCoordinator.ResultReady = false;}void ProcessCurrentElementRow(){var previousResultReady = resultCoordinator.ResultReady;resultCoordinator.ResultReady = true;var relatedEntity = innerShaper(queryContext, dbDataReader, collectionMaterializationContext.ResultContext, resultCoordinator);if (resultCoordinator.ResultReady){// related entity is materializedcollectionMaterializationContext.ResultContext.Values = null;if (!trackingQuery){fixup(entity, relatedEntity);if (inverseNavigation != null){inverseNavigation.SetIsLoadedWhenNoTracking(relatedEntity);}}}resultCoordinator.ResultReady &= previousResultReady;}void GenerateCurrentElementIfPending(){if (collectionMaterializationContext.ResultContext.Values != null){resultCoordinator.HasNext = false;ProcessCurrentElementRow();}collectionMaterializationContext.UpdateSelfIdentifier(null);} }嗯,這件事情很神奇。
理論上,在不帶過濾的情況下,One Include Many 和 Many Include One 應該是一致的?
為何代碼為什么差別這么大?實際上,這就是“查詢跟蹤”的神秘之處了。
不知道各位朋友是否在網上看見過這樣的文章,說使用?AsNoTracking?可以提高查詢性能,并且還建議大家直接?optionsBuilder.UseQueryTrackingBehavior(NoTracking)?
實際上,這樣有一種隱藏的坑:
如果你使用了一對多的 SQL JOIN,并且還保持著原來的實體形狀,如以下代碼所示:
var results = context.Tenants.AsNoTracking().Join(inner: context.Users.AsNoTracking(),outerKeySelector: t => t.Id,innerKeySelector: u => u.TenantId,resultSelector: (t, u) => new { t, u }).ToList();假設你的?results[0].t.Id == results[1].t.Id,也就是前兩條在數據庫中是同一個?t?實例,當你拉取到本地時,你會發現?object.ReferenceEquals(results[0].t, results[1].t) == false,或者說在本地不是同一個實例,而兩者僅僅是值相等。
如果上述代碼使用的都是?AsTracking,那么?object.ReferenceEquals(results[0].t, results[1].t) == true,從頭到尾只會創建一個這樣的實體。
為了保證 One Include Many 在未開啟實體追蹤的時候也正常工作,不得不使用一些奇怪的代碼來保證結果正確性。
這種情況是在筆者兩年前做 JOIN 以后拉到本地 GroupBy 的時候發現的。當年在已經寫了很多代碼以后才開啟了 NoTracking,結果導致很多原有功能失效。從那之后,筆者都是老老實實開 Tracking 的。
對,確實有很多“不開啟實體跟蹤”還要“生成出來的實體不重復”的情況,這種情況下要怎么做呢?
EFCore 5.0 新推出了?NoTrackingWithIdentityResolution。用這個就能保證上述的引用相等,但是實體不會最終被上下文追蹤。
另外,所謂“不開啟實體跟蹤”最大的影響場景,是查詢大量數據以后,對上下文進行多次保存操作。如果你覺得頻繁保存時處理大量實體變得異常緩慢,你應該考慮修改?DbContext.ChangeTracker.AutoDetectChangesEnabled?為?false,然后對所有修改過的實體手動調用?context.Update。這樣即使上下文追蹤上萬個實體,保存都是幾乎一瞬間的事情。
另外,實體追蹤還有什么用呢?
記得?IQueryable<>?們的一個拓展?.LoadAsync()?嗎?這個?.LoadAsync()?的注釋說,它的功能約等于?.ToListAsync()?并立馬將這個列表扔掉。那查詢到的東西怎么利用?使用?context.Set<MyEntity>().FindAsync(id)?即可。它僅在?id?不存在于本地的時候才到數據庫查找結果。
所以筆者給出的建議是
在性能要求高的關鍵路徑上盡量少使用導航屬性
盡量不依賴實體更改自動檢測
當然了,導航屬性還是很好的,編寫?Where?的時候可以少編寫很多?Join……
今天關于查詢編譯和查詢跟蹤的討論就到這里啦。
總結
以上是生活随笔為你收集整理的EFCore查缺补漏(二):查询的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 探索 .NET Core 依赖注入的 I
- 下一篇: Kubernetes 凭什么成了云原生应