前言
上述我們簡單講解了幾個小問題,這節我們再來看看如標題EF Core中多次Include導致出現性能的問題,廢話少說,直接開門見山。
EntityFramework Core 3多次Include查詢問題
不要嫌棄我啰嗦,我們凡事從頭開始講解起,首先依然給出我們上一節的示例類:
public class EFCoreDbContext : DbContext { public EFCoreDbContext() { } public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer(@"Server=.;Database=EFTest;Trusted_Connection=True;"); } public class Blog { public int Id { get; set; } public string Name { get; set; } public List<Post> Posts { get; set; } } public class Post { public int Id { get; set; } public int BlogId { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get; set; } }
接下來我們在控制台進行如下查詢:
var context = new EFCoreDbContext(); var blog = context.Blogs.FirstOrDefault(d => d.Id == 1);
如上圖所示,生成的SQL語句一點毛病都么有,對吧,接下來我們來查詢導航屬性Posts,如下:
var context = new EFCoreDbContext(); var blog = context.Blogs.AsNoTracking() .Include(d => d.Posts).FirstOrDefault(d => d.Id == 1);
咦,不應該是INNER JOIN嗎,但最終生成的SQL語句我們可以看到居然是LEFT JOIN,關鍵是我們對Post類中的BlogId並未設置為可空,對吧,是不是很有意思。同時通過ORDER BY對兩個表的主鍵都進行了排序。這就是問題的引發點,接下來我們再引入兩個類:
/// <summary> /// 博客標簽 /// </summary> public class Tag { public int Id { get; set; } /// <summary> /// 標簽名稱 /// </summary> public string Name { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } } /// <summary> /// 博客分類 /// </summary> public class Category { /// <summary> /// /// </summary> public int Id { get; set; } /// <summary> /// 分類名稱 /// </summary> public string Name { get; set; } /// <summary> /// /// </summary> public int BlogId { get; set; } /// <summary> /// /// </summary> public Blog Blog { get; set; } }
上述我們聲明了分類和標簽,我們知道博客有分類和標簽,所以博客類中有對分類和標簽的導航屬性(這里我們先不關心關系到底是一對一還是一對多等關系),然后修改博客類,如下:
public class Blog { public int Id { get; set; } public string Name { get; set; } public List<Post> Posts { get; set; } public List<Tag> Tags { get; set; } public List<Category> Categories { get; set; } }
接下來我們再來進行如下查詢:
var context = new EFCoreDbContext(); var blogs = context.Blogs.AsNoTracking().Include(d => d.Posts) .Include(d => d.Tags) .Include(d => d.Categories).FirstOrDefault(d => d.Id == 1);
SELECT [t].[Id], [t].[Name], [p].[Id], [p].[BlogId], [p].[Content], [p].[Title], [t0].[Id], [t0].[BlogId], [t0].[Name], [c].[Id], [c].[BlogId], [c].[Name] FROM ( SELECT TOP(1) [b].[Id], [b].[Name] FROM [Blogs] AS [b] WHERE [b].[Id] = 1 ) AS [t] LEFT JOIN [Posts] AS [p] ON [t].[Id] = [p].[BlogId] LEFT JOIN [Tags] AS [t0] ON [t].[Id] = [t0].[BlogId] LEFT JOIN [Categories] AS [c] ON [t].[Id] = [c].[BlogId] ORDER BY [t].[Id], [p].[Id], [t0].[Id], [c].[Id]
此時和變更追蹤沒有半毛錢關系,我們看看最終生成的SQL語句,是不是很驚訝,假設單個類中對應多個導航屬性,最終生成的SQL語句就是繼續LEFT JOIN和ORDER BY,可想其性能將是多么的低下。那么我們應該如何解決這樣的問題呢?既然是和Include有關系,每增加一個導航屬性即增加一個Include將會增加一個LEFT JOIN和ORDER BY,那么我們何不分開單獨查詢呢,說完就開干。
var context = new EFCoreDbContext(); var blog = context.Blogs.AsNoTracking().FirstOrDefault(d => d.Id == 1);
此時我們進行如上查詢顯然不可取,因為直接就到數據庫進行SQL查詢了,我們需要返回IQueryable才行,同時根據主鍵查詢只能返回一條,所以我們改造成如下查詢:
var context = new EFCoreDbContext(); var blog = context.Blogs.Where(d => d.Id == 1).Take(1);
因為接下來還需要從上下文中加載導航屬性,所以這里我們需要去掉AsNoTracking,通過上下文加載指定實體導航屬性,我們可通過Load方法來加載,如下:
var context = new EFCoreDbContext(); var blog = context.Blogs.Where(d => d.Id == 1).Take(1); blog.Include(p => p.Posts).SelectMany(d => d.Posts).Load(); blog.Include(t => t.Tags).SelectMany(d => d.Tags).Load(); blog.Include(c => c.Categories).SelectMany(d => d.Categories).Load();
SELECT [p].[Id], [p].[BlogId], [p].[Content], [p].[Title] FROM ( SELECT TOP(1) [b].[Id], [b].[Name] FROM [Blogs] AS [b] WHERE [b].[Id] = 1 ) AS [t] INNER JOIN [Posts] AS [p] ON [t].[Id] = [p].[BlogId] SELECT [t0].[Id], [t0].[BlogId], [t0].[Name] FROM ( SELECT TOP(1) [b].[Id], [b].[Name] FROM [Blogs] AS [b] WHERE [b].[Id] = 1 ) AS [t] INNER JOIN [Tags] AS [t0] ON [t].[Id] = [t0].[BlogId] SELECT [c].[Id], [c].[BlogId], [c].[Name] FROM ( SELECT TOP(1) [b].[Id], [b].[Name] FROM [Blogs] AS [b] WHERE [b].[Id] = 1 ) AS [t] INNER JOIN [Categories] AS [c] ON [t].[Id] = [c].[BlogId]
通過上述生成的SQL語句,我們知道這才是我們想要的結果,上述代碼看起來有點不是那么好看,似乎沒有更加優美的寫法了,當然這里我只是在控制台中進行演示,為了吞吐,將上述修改為異步查詢則是最佳可行方式。 比生成一大堆LEFT JOIN和ORDER BY性能好太多太多。
總結
注意:上述博主采用的是穩定版本3.0.1,其他版本未經測試哦。其實對於查詢而言,還是建議采用Dapper或者走底層connection寫原生SQL才是最佳,對於單表,用EF Core無可厚非,對於復雜查詢還是建議不要用EF Core,生成的SQL很不可控,為了圖方便,結果換來的將是CPU飆到飛起。好了,本節我們就到這里,感謝您的閱讀,我們下節見。