如何使用Entity Framework扁平化自引用实体数据图谱?
如何在Entity Framework中实现自引用实体的扁平化(避免加载全部数据)
首先得说,你现有的代码确实能实现扁平化,但加载全表数据的问题在数据量大的时候会很影响性能,而且你的递归方法还藏着一个小bug——循环里的return会导致第一个子节点处理完就直接退出,其他子节点根本不会被加入结果列表。咱们一步步来解决这两个问题:
先修复你现有代码的递归bug
把递归方法里的return去掉,改成直接调用递归函数,这样所有子节点都会被遍历到:
private IList<OrganizationHierarchyItem> FlattenOrganizationGraph(OrganizationHierarchyItem passedItem, List<OrganizationHierarchyItem> organizationHierarchyItems, IList<OrganizationHierarchyItem> result=null) { if (result == null) result = new List<OrganizationHierarchyItem>(); result.Add(passedItem); foreach (var item in organizationHierarchyItems.Where(x => x.ParentId == passedItem.Id).ToList()) { // 去掉return,让每个子节点都能触发递归 FlattenOrganizationGraph(item, organizationHierarchyItems, result); } return result; }
优化数据加载:只加载需要的层级(不加载全表)
加载全表是没必要的,咱们可以通过EF的特性或者数据库层面的查询,只获取目标根节点及其所有后代:
方案1:用数据库CTE递归查询(推荐,性能最优)
直接在数据库层面用CTE(公共表表达式)完成递归查询,返回已经扁平化的结果,这是效率最高的方式,因为所有逻辑都在数据库里完成,不需要加载多余数据,也不用在内存里递归处理:
var targetId = id; // 你的目标根节点ID var flattenHierarchyList = dbContext.OrganizationHierarchyItems .FromSqlRaw(@" WITH RecursiveHierarchy AS ( -- 起始节点:目标根节点 SELECT * FROM OrganizationHierarchyItems WHERE Id = {0} UNION ALL -- 递归遍历所有子节点 SELECT o.* FROM OrganizationHierarchyItems o INNER JOIN RecursiveHierarchy rh ON o.ParentId = rh.Id ) SELECT * FROM RecursiveHierarchy", targetId) .ToList();
方案2:EF Core 5+ 递归Include(适合层级固定或需要实体导航属性的场景)
如果你需要保留实体的导航属性(比如Parent/SubOrganizationHierarchyItems),可以用EF Core 5及以上支持的递归Include,但手动写ThenInclude只能支持固定层级,要是层级不确定,可以自己写一个扩展方法来实现无限层级Include:
// 先获取根节点,递归加载所有子节点 var rootItem = await dbContext.OrganizationHierarchyItems .IncludeRecursive(x => x.SubOrganizationHierarchyItems) .FirstOrDefaultAsync(x => x.Id == id); // 然后再扁平化这个层级结构(用你修复后的递归方法) var flattenList = FlattenOrganizationGraph(rootItem, new List<OrganizationHierarchyItem> { rootItem });
这里的IncludeRecursive是自定义扩展方法,你可以自己实现:
public static IQueryable<T> IncludeRecursive<T, TProperty>(this IQueryable<T> query, Expression<Func<T, IEnumerable<TProperty>>> navigationProperty) where T : class where TProperty : class { var parameter = Expression.Parameter(typeof(T), "x"); var property = Expression.Property(parameter, ((MemberExpression)navigationProperty.Body).Member.Name); var includeExpression = Expression.Lambda<Func<T, IEnumerable<TProperty>>>(property, parameter); return query.Include(navigationProperty) .ThenInclude(includeExpression) .IncludeRecursive(includeExpression); }
不过这个方法如果层级太深,可能会生成复杂的SQL,不如CTE高效。
方案3:避免N+1的内存加载(适合必须在内存处理的场景)
如果你一定要在内存里处理扁平化,可以先一次性加载所有相关节点,避免多次查询数据库:
var targetId = id; // 先查询根节点 var root = await dbContext.OrganizationHierarchyItems.FirstOrDefaultAsync(x => x.Id == targetId); if (root == null) return new List<OrganizationHierarchyItem>(); // 用CTE查询所有相关节点(和方案1一样),或者用EF的查询获取所有后代 var allRelatedItems = await dbContext.OrganizationHierarchyItems .FromSqlRaw(@" WITH RecursiveHierarchy AS ( SELECT * FROM OrganizationHierarchyItems WHERE Id = {0} UNION ALL SELECT o.* FROM OrganizationHierarchyItems o INNER JOIN RecursiveHierarchy rh ON o.ParentId = rh.Id ) SELECT * FROM RecursiveHierarchy", targetId) .ToList(); // 然后用你修复后的扁平化方法处理 var flattenList = FlattenOrganizationGraph(root, allRelatedItems);
总结
- 优先用CTE方案:性能最优,不需要加载全表,也不用处理内存递归的问题
- 如果你需要保留实体导航属性,考虑用EF Core的递归Include扩展
- 一定要修复递归方法里的
returnbug,否则只能得到部分节点
内容的提问来源于stack exchange,提问作者Simple Code
相关产品推荐
相关产品推荐

