You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:51:37