如何高效动态加载项目的所有后代及关联ProjectOwner?
解决EF Core动态加载无限层级树形项目(含关联Owner)的高效方案
我完全懂你这种手动嵌套Include的痛苦——不仅写起来繁琐到爆炸,还根本没法应对不确定的子项目层级。下面给你几个在EF Core里处理这种树形结构的高效方案,都是实际项目中常用的:
方案1:递归CTE+内存组装(推荐,EF Core 5+支持)
这是性能最优的方案,利用数据库的递归CTE一次性查询出所有根节点及其所有后代,再在内存中快速组装树形结构,完美支持任意层级。
步骤1:查询所有需要的节点(含ProjectOwner)
如果你的数据量不是特别大,可以直接拉取所有项目,然后筛选根节点并组装:
// 一次性加载所有项目及其所有者 var allProjects = await db.Projects .Include(p => p.ProjectOwner) .ToListAsync(); // 用字典快速查找父节点,构建树形结构 var projectLookup = allProjects.ToDictionary(p => p.Id); var rootProjects = new List<Project>(); foreach (var project in allProjects) { if (project.ParentId == null) { rootProjects.Add(project); } else { if (projectLookup.TryGetValue(project.ParentId.Value, out var parentProject)) { parentProject.ChildProjects ??= new List<Project>(); parentProject.ChildProjects.Add(project); } } }
如果数据量很大,不想拉取所有项目,可以用SQL递归CTE只查询根节点的后代:
// 用递归CTE查询所有根节点及其子项目 var relevantProjects = await db.Projects .FromSqlRaw(@" WITH RecursiveProjects AS ( -- 起始:所有根节点 SELECT * FROM Projects WHERE ParentId IS NULL UNION ALL -- 递归:子节点关联父节点 SELECT p.* FROM Projects p INNER JOIN RecursiveProjects rp ON p.ParentId = rp.Id ) SELECT * FROM RecursiveProjects ") .Include(p => p.ProjectOwner) .ToListAsync(); // 同样用字典组装树形结构 var projectLookup = relevantProjects.ToDictionary(p => p.Id); var rootProjects = relevantProjects.Where(p => p.ParentId == null).ToList(); foreach (var project in relevantProjects.Where(p => p.ParentId != null)) { projectLookup[project.ParentId.Value].ChildProjects ??= new List<Project>(); projectLookup[project.ParentId.Value].ChildProjects.Add(project); }
这个方案只需要1次数据库查询,内存组装的效率也很高,不管子项目有多少层都能完美处理。
方案2:懒加载(适合小型数据集)
如果你的项目数据量很小,想快速实现功能,可以启用EF Core的懒加载,这样访问子项目时会自动加载对应的ProjectOwner。
步骤1:修改实体类,给关联属性加virtual
public class Project { public int Id { get; set; } public int? ParentId { get; set; } public virtual Member ProjectOwner { get; set; } // 加virtual public virtual List<Project> ChildProjects { get; set; } // 加virtual // ... 更多属性 }
步骤2:在DbContext中启用懒加载代理
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("你的数据库连接字符串") .UseLazyLoadingProxies(); // 启用懒加载 }
步骤3:查询根节点即可
var rootProjects = await db.Projects .Where(p => p.ParentId == null) .Include(p => p.ProjectOwner) // 根节点的Owner提前加载 .ToListAsync();
之后当你访问某个项目的ChildProjects时,EF Core会自动加载对应的子项目及其ProjectOwner。注意:这个方案会产生N+1查询问题,层级多、数据量大时性能会很差,只适合小型项目。
方案3:递归显式加载(可控但查询次数较多)
如果不想用懒加载,又需要控制加载时机,可以手动递归显式加载每个节点的子项目和所有者:
编写递归加载方法
private async Task LoadAllChildProjects(Project parentProject) { // 显式加载当前节点的子项目及其Owner await db.Entry(parentProject) .Collection(p => p.ChildProjects) .Query() .Include(c => c.ProjectOwner) .LoadAsync(); // 递归加载每个子项目的子项目 foreach (var child in parentProject.ChildProjects) { await LoadAllChildProjects(child); } }
使用方法
// 先加载根节点及其Owner var rootProjects = await db.Projects .Where(p => p.ParentId == null) .Include(p => p.ProjectOwner) .ToListAsync(); // 递归加载所有子项目 foreach (var root in rootProjects) { await LoadAllChildProjects(root); }
这个方案的查询次数等于项目的层级数+1,比懒加载的N+1要好,但还是不如递归CTE的一次查询高效,适合需要精准控制加载时机的场景。
内容的提问来源于stack exchange,提问作者Stian
相关产品推荐
相关产品推荐

