Entity Framework递归Include实体嵌套层级问题求助
Entity Framework递归Include实体嵌套层级问题求助
嘿,我来帮你排查下这个递归加载的问题!首先得说你的思路是对的,但大概率是踩了EF实体追踪或者集合加载的小坑,咱们一步步捋。
先分析你现有代码的潜在问题:你在调用递归Load前已经用Include(x => x.Pages)加载了第一层子页面,这时候父页面的Pages集合已经被EF追踪了。但你实体里的Pages用了init只读设置器还初始化了空集合,EF加载关联集合时会往已有集合里塞数据,但如果递归时的判断逻辑或者加载时机没处理好,就可能导致深层级加载失败。
给你几个可行的解决方案,你可以根据自己的场景选:
方案一:修复现有递归Load方法
你的递归逻辑框架没问题,只要加个小判断确保只在需要时加载集合就行:
private static void Load(ICollection<CatalogPage> pages, DbContext db) { foreach (var p in pages) { // 先判断集合是否已经被EF加载,没加载才去查数据库 if (!db.Entry(p).Collection(x => x.Pages).IsLoaded) { db.Entry(p).Collection(x => x.Pages).Load(); } // 不用判断Count,空集合递归进去也会直接跳过循环,代码更简洁 Load(p.Pages, db); } }
加IsLoaded判断是为了避免重复触发数据库查询——EF已经加载过的集合,再调用Load()不会有任何操作,这个判断能确保我们只在真正需要的时候去拉取数据。
方案二:开启延迟加载(适合数据量小的场景)
如果你的项目能接受延迟加载的N+1查询问题,这是最省事的办法:
- 把实体里的
Pages改成virtual属性:
public virtual ICollection<CatalogPage> Pages { get; init; } = [];
- 确保你的DbContext开启了延迟加载(EF Core需要先装
Microsoft.EntityFrameworkCore.Proxies包,然后在OnConfiguring里加optionsBuilder.UseLazyLoadingProxies();)
之后你根本不用写递归Load,只要访问某个页面的Pages集合,EF会自动去数据库拉取对应的子页面,递归访问深层级也会自动加载。
方案三:用递归CTE一次性拉取所有数据(性能最优)
如果数据量不小,递归Load会触发大量数据库查询,这时候用SQL递归CTE一次性把所有层级的数据拉回来,再在内存里构建嵌套结构是最优解:
// 用CTE一次性查询所有层级的页面 var allPages = await dbContext.Set<CatalogPage>() .FromSqlRaw(@" WITH RecursiveCatalogPages AS ( SELECT * FROM CatalogPages WHERE CatalogPageId = 0 -- 这里替换成你无父级页面的判断条件 UNION ALL SELECT cp.* FROM CatalogPages cp JOIN RecursiveCatalogPages rcp ON cp.CatalogPageId = rcp.Id ) SELECT * FROM RecursiveCatalogPages ") .Include(x => x.Items) // 这里正常关联你需要的其他实体 .ToListAsync(); // 内存里用字典快速构建嵌套结构 var pageLookup = allPages.ToDictionary(p => p.Id); var parentlessPages = new List<CatalogPage>(); foreach (var page in allPages) { if (page.CatalogPageId == 0) { parentlessPages.Add(page); } else { if (pageLookup.TryGetValue(page.CatalogPageId, out var parent)) { parent.Pages.Add(page); } } }
这个方式只需要一次数据库查询,性能比递归Load好太多,数据量越大优势越明显。
备注:内容来源于stack exchange,提问作者user24419920
相关产品推荐
相关产品推荐

