EF Core 8如何递归加载目录页面的所有子页面?
EF Core 8递归加载多级目录页面解决方案
我正在使用EF Core 8开发目录系统,系统中的页面可包含子页面及条目,需要递归加载所有层级的子页面。当前代码仅能加载一级子页面,无法递归到无后续子页面的层级。
当前代码
var page = await dbContext .Set<CatalogPage>() .Where(x => x.Id == eventParser.PageId) .Include(c => c.Items) .ThenInclude(x => x.FurnitureItems) .Include(c => c.Pages) .ThenInclude(x => x.Pages) .FirstOrDefaultAsync();
实体类定义
public class CatalogPage { [Key] public int Id { get; init; } public string? Name { get; init; } public string? Caption { get; init; } public string? Layout { get; init; } public int? RoleId { get; init; } public int CatalogPageId { get; init; } public int OrderId { get; init; } public int IconId { get; init; } public ICollection<CatalogPage> Pages { get; init; } = []; public ICollection<CatalogItem> Items { get; init; } = []; } public class CatalogItem { public int Id { get; init; } public string? Name { get; init; } public int CostCredits { get; init; } public int CostPoints { get; init; } public ICollection<FurnitureItem> FurnitureItems { get; init; } = []; public string? MetaData { get; init; } public int Amount { get; init; } public int SellLimit { get; init; } }
解决方案
方法1:启用延迟加载(快速实现)
EF Core的延迟加载可以自动递归加载子页面,但需满足两个条件:
- 将导航属性标记为
virtual - 确保DbContext启用延迟加载(默认已开启,若手动关闭需重新开启)
修改实体类导航属性:
public class CatalogPage { // ...其他属性 public virtual ICollection<CatalogPage> Pages { get; init; } = []; public virtual ICollection<CatalogItem> Items { get; init; } = []; } public class CatalogItem { // ...其他属性 public virtual ICollection<FurnitureItem> FurnitureItems { get; init; } = []; }
修改后的查询代码(无需多层ThenInclude,访问子页面时会自动加载):
var page = await dbContext .Set<CatalogPage>() .Where(x => x.Id == eventParser.PageId) .Include(c => c.Items) .ThenInclude(x => x.FurnitureItems) .FirstOrDefaultAsync();
注意:延迟加载可能导致N+1查询问题,数据量较大时不推荐使用。
方法2:递归CTE+内存构建树(性能最优)
通过SQL递归CTE一次性获取根节点及所有后代页面,再在内存中构建层级结构,避免N+1查询。
// 1. 执行递归CTE查询所有相关页面 var allPages = await dbContext.Set<CatalogPage>() .Include(c => c.Items) .ThenInclude(x => x.FurnitureItems) .FromSqlRaw(@" WITH RecursivePages AS ( SELECT * FROM CatalogPages WHERE Id = {0} UNION ALL SELECT cp.* FROM CatalogPages cp JOIN RecursivePages rp ON cp.CatalogPageId = rp.Id ) SELECT * FROM RecursivePages", eventParser.PageId) .ToListAsync(); // 2. 在内存中构建页面树 var rootPage = allPages.FirstOrDefault(x => x.Id == eventParser.PageId); if (rootPage != null) { BuildPageTree(rootPage, allPages); } // 递归构建子页面的辅助方法 void BuildPageTree(CatalogPage parent, List<CatalogPage> allPages) { parent.Pages = allPages.Where(x => x.CatalogPageId == parent.Id).ToList(); foreach (var childPage in parent.Pages) { BuildPageTree(childPage, allPages); } }
优势:仅执行一次数据库查询,性能高效,适合数据量大的场景。
方法3:显式递归加载(手动控制)
先加载根节点,再递归查询并填充子页面,适合需要精细控制加载逻辑的场景。
var rootPage = await dbContext.Set<CatalogPage>() .Include(c => c.Items) .ThenInclude(x => x.FurnitureItems) .FirstOrDefaultAsync(x => x.Id == eventParser.PageId); if (rootPage != null) { await LoadChildPagesAsync(rootPage); } // 递归加载子页面的辅助方法 async Task LoadChildPagesAsync(CatalogPage parent) { await dbContext.Entry(parent) .Collection(p => p.Pages) .Query() .Include(p => p.Items) .ThenInclude(x => x.FurnitureItems) .LoadAsync(); foreach (var childPage in parent.Pages) { await LoadChildPagesAsync(childPage); } }
注意:每一层子页面都会触发一次数据库查询,数据层级较深时会产生多次查询。
内容的提问来源于stack exchange,提问作者user24419920
相关产品推荐
相关产品推荐

