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

如何在Entity Framework中递归Include依赖项,高效构建PTO计划依赖图?

递归加载EF依赖的可行方案

我之前也处理过类似的多层自引用依赖场景——既要构建完整的依赖图,又不想拉取全量数据拖慢性能,确实得找个平衡的方案。EF(尤其是EF Core)本身没有原生的IncludeWhile方法,但我们可以通过自定义扩展方法或者数据库端递归查询(CTE)来实现需求,下面给你两种实用方案:

方案一:自定义递归Include扩展方法

你提到的IncludeWhile思路很合理,我们可以自己实现一个支持递归加载的扩展方法,同时加入循环依赖检测,避免无限递归或重复加载。

带循环检测的递归扩展方法

public static IQueryable<T> IncludeRecursive<T, TProperty>(
    this IQueryable<T> query,
    Expression<Func<T, TProperty>> navigationPath,
    Func<T, int> getIdFunc,
    HashSet<int> loadedIds = null)
    where T : class
    where TProperty : class
{
    loadedIds ??= new HashSet<int>();

    // 先加载当前层级的导航属性
    var includedQuery = query.Include(navigationPath);
    
    // 获取导航属性的访问逻辑
    var propertyAccessor = navigationPath.Compile();

    // 过滤出未加载过的依赖项,避免循环
    var filteredQuery = includedQuery.Where(x => 
    {
        var dependent = propertyAccessor(x);
        return dependent != null && loadedIds.Add(getIdFunc(x));
    });

    // 递归加载下一层依赖
    return filteredQuery.ThenInclude(x => propertyAccessor(x))
        .IncludeRecursive(navigationPath, getIdFunc, loadedIds);
}

你的业务代码适配示例

public Dictionary<int, List<int>> GetPTOPlanDependencyGraph(List<int> ptoPlanIds)
{
    var trackedIds = new HashSet<int>(ptoPlanIds);

    var rawPlans = context.PTOPlans
        .Where(x => ptoPlanIds.Contains(x.Id))
        // 递归加载DependentPTO
        .IncludeRecursive(x => x.DependentPTO, x => x.Id, trackedIds)
        // 递归加载RolloverPlan
        .IncludeRecursive(x => x.RolloverPlan, x => x.Id, trackedIds)
        // 加载PTOPlanRules及其依赖
        .Include(x => x.PTOPlanRules)
            .ThenInclude(r => r.DependentPTO)
            .ThenInclude(dp => dp.RolloverPlan)
        .AsNoTracking(); // 无需修改数据时开启,提升性能

    return BuildDependencyGraph(rawPlans);
}

方案二:数据库CTE批量获取依赖ID(大数据量首选)

如果你的计划数量特别多,数据库端的递归查询(CTE)会更高效——它能在数据库层面一次性找出所有依赖链上的ID,再批量加载数据,避免多次数据库往返。

第一步:用CTE获取所有关联ID

public List<int> GetAllRelevantPlanIds(List<int> initialIds)
{
    // 构建CTE递归查询,覆盖PTOPlan的Dependent和Rollover依赖,以及Rules的依赖
    var cteSql = @"
        WITH RecursivePTO AS (
            -- 初始节点
            SELECT Id, DependentPTOId, RolloverPlanId FROM PTOPlans WHERE Id IN ({0})
            UNION ALL
            -- 递归加载PTOPlan的依赖
            SELECT p.Id, p.DependentPTOId, p.RolloverPlanId 
            FROM PTOPlans p
            INNER JOIN RecursivePTO rp ON p.Id = rp.DependentPTOId OR p.Id = rp.RolloverPlanId
            WHERE p.Id NOT IN ({0})
            UNION ALL
            -- 加载PTOPlanRules关联的依赖计划
            SELECT p.Id, p.DependentPTOId, p.RolloverPlanId
            FROM PTOPlans p
            INNER JOIN PTOPlanRules r ON p.Id = r.DependentPTOId OR p.Id = r.RolloverPlanId
            INNER JOIN RecursivePTO rp ON r.ParentPTOPlanId = rp.Id
        )
        SELECT DISTINCT Id FROM RecursivePTO";

    // 替换参数占位符
    var paramPlaceholders = string.Join(",", initialIds.Select((_, idx) => $"@p{idx}"));
    var finalSql = string.Format(cteSql, paramPlaceholders);

    // 执行原生SQL获取所有关联ID
    var allIds = context.PTOPlans
        .FromSqlRaw(finalSql, initialIds.Cast<object>().ToArray())
        .Select(x => x.Id)
        .ToList();

    // 确保初始ID被包含
    allIds.AddRange(initialIds.Except(allIds));
    return allIds.Distinct().ToList();
}

第二步:批量加载数据

public Dictionary<int, List<int>> GetPTOPlanDependencyGraph(List<int> ptoPlanIds)
{
    var allRelevantIds = GetAllRelevantPlanIds(ptoPlanIds);

    var rawPlans = context.PTOPlans
        .Where(x => allRelevantIds.Contains(x.Id))
        .Include(x => x.DependentPTO)
        .Include(x => x.RolloverPlan)
        .Include(x => x.PTOPlanRules)
            .ThenInclude(r => r.DependentPTO)
            .ThenInclude(r => r.RolloverPlan)
        .AsNoTracking()
        .ToList();

    return BuildDependencyGraph(rawPlans);
}

关键注意事项

  1. 实体类修正提示:你的PTOPlan和PTOPlanRule中外键属性名可能有笔误(比如PTOPlan里的public int DependentPTO { get; set; }应该是DependentPTOId),EF默认约定是导航属性名加Id作为外键,建议修正避免映射错误。
  2. 循环依赖处理:两种方案都加入了ID追踪逻辑,一定要保留,否则会出现无限递归或重复加载的问题。
  3. 性能优化:无需修改数据时务必使用AsNoTracking(),能大幅降低EF的追踪开销;批量操作永远比循环单条查询高效。

内容的提问来源于stack exchange,提问作者johnny 5

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 07:46:12