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

如何在LINQ查询中获取EF Core的多层嵌套子问题

要实现单次SQL查询获取多层嵌套的子问题,最优方案肯定是利用递归CTE(Common Table Expression)——不管是通过EF Core的LINQ递归查询(EF Core 5及以上支持),还是直接写原生SQL,都能高效完成单次数据库交互,避免N+1查询的性能问题。

先给你梳理下具体的实现步骤,顺便优化下你的实体模型,让关联更清晰:

第一步:优化实体与关联配置

你的实体模型可以补充双向导航属性,让EF Core更好地理解层级关系:

QuestionDataModel 补充属性:

public class QuestionDataModel : IDataModel<Guid>
{
    public QuestionDataModel()
    {
        ChildQuestionsInHierarchy = new HashSet<QuestionHierarchyDataModel>();
        ParentQuestionsInHierarchy = new HashSet<QuestionHierarchyDataModel>();
    }
    public Guid Id { get; set; }
    public bool IsSubQuestion { get; set; } // 你查询里用到了这个属性,实体里记得补上

    public virtual ICollection<QuestionHierarchyDataModel> ChildQuestionsInHierarchy { get; set; }
    public virtual ICollection<QuestionHierarchyDataModel> ParentQuestionsInHierarchy { get; set; }
}

QuestionHierarchyDataModel 补充父问题导航:

public class QuestionHierarchyDataModel 
{
    public Guid Id { get; set; }
    public Guid ParentQuestionId { get; set; }
    public Guid ChildQuestionId { get; set; }
    public virtual QuestionDataModel ParentQuestion { get; set; } // 新增父问题导航
    public virtual QuestionDataModel ChildQuestion { get; set; }
}

然后在DbContext的OnModelCreating里配置关联关系,避免歧义:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 父问题 -> 子层级关联
    modelBuilder.Entity<QuestionHierarchyDataModel>()
        .HasOne(h => h.ParentQuestion)
        .WithMany(q => q.ChildQuestionsInHierarchy)
        .HasForeignKey(h => h.ParentQuestionId)
        .OnDelete(DeleteBehavior.Restrict);

    // 子问题 -> 父层级关联
    modelBuilder.Entity<QuestionHierarchyDataModel>()
        .HasOne(h => h.ChildQuestion)
        .WithMany(q => q.ParentQuestionsInHierarchy)
        .HasForeignKey(h => h.ChildQuestionId)
        .OnDelete(DeleteBehavior.Restrict);
}

第二步:实现多层嵌套查询

这里给你两种方案,根据你的EF Core版本和需求选择:

方案1:EF Core 5+ 用LINQ递归查询(类型安全)

EF Core 5及以上支持LINQ递归查询,会自动生成递归CTE的SQL,完全不用写原生SQL:

首先定义一个嵌套模型来承载结果:

public class QuestionWithHierarchy
{
    public QuestionDataModel Question { get; set; }
    public List<QuestionWithHierarchy> Children { get; set; } = new List<QuestionWithHierarchy>();
}

然后编写递归LINQ查询:

// 定义递归方法(方法语法更直观)
IQueryable<QuestionWithHierarchy> GetHierarchy(Guid? parentId = null)
{
    var query = from q in context.Questions
                where (parentId == null && !q.IsSubQuestion) || 
                      (parentId != null && q.ParentQuestionsInHierarchy.Any(h => h.ParentQuestionId == parentId))
                select new QuestionWithHierarchy
                {
                    Question = q,
                    Children = GetHierarchy(q.Id).ToList()
                };
    return query;
}

// 获取所有根节点及其多层子节点
var fullHierarchy = GetHierarchy().ToList();

这个查询会生成单次递归CTE的SQL,直接从数据库获取所有层级数据,然后自动映射成嵌套结构。

方案2:原生SQL递归CTE(兼容低版本EF Core)

如果你的EF Core版本低于5,或者需要更复杂的自定义逻辑,可以直接写递归CTE的SQL,然后在内存中构建嵌套结构:

首先编写SQL:

WITH RecursiveQuestions AS (
    -- 锚点:根节点(非子问题)
    SELECT 
        q.Id AS QuestionId,
        q.*,
        h.ParentQuestionId,
        1 AS HierarchyLevel
    FROM Questions q
    LEFT JOIN QuestionHierarchies h ON q.Id = h.ChildQuestionId
    WHERE q.IsSubQuestion = 0

    UNION ALL

    -- 递归:获取子节点
    SELECT 
        q.Id AS QuestionId,
        q.*,
        h.ParentQuestionId,
        rq.HierarchyLevel + 1 AS HierarchyLevel
    FROM Questions q
    JOIN QuestionHierarchies h ON q.Id = h.ChildQuestionId
    JOIN RecursiveQuestions rq ON h.ParentQuestionId = rq.QuestionId
)
SELECT * FROM RecursiveQuestions
ORDER BY HierarchyLevel, ParentQuestionId

然后在EF Core中执行并构建嵌套结构:

// 执行SQL获取扁平化的层级数据
var flatData = context.Questions
    .FromSqlRaw(@"
        WITH RecursiveQuestions AS (
            SELECT 
                q.Id AS QuestionId,
                q.*,
                h.ParentQuestionId,
                1 AS HierarchyLevel
            FROM Questions q
            LEFT JOIN QuestionHierarchies h ON q.Id = h.ChildQuestionId
            WHERE q.IsSubQuestion = 0

            UNION ALL

            SELECT 
                q.Id AS QuestionId,
                q.*,
                h.ParentQuestionId,
                rq.HierarchyLevel + 1 AS HierarchyLevel
            FROM Questions q
            JOIN QuestionHierarchies h ON q.Id = h.ChildQuestionId
            JOIN RecursiveQuestions rq ON h.ParentQuestionId = rq.QuestionId
        )
        SELECT * FROM RecursiveQuestions
        ORDER BY HierarchyLevel, ParentQuestionId
    ")
    .Select(q => new 
    {
        Question = q,
        ParentQuestionId = (Guid?)q.ParentQuestionsInHierarchy.FirstOrDefault()?.ParentQuestionId
    })
    .ToList();

// 用字典快速查找节点,构建嵌套结构
var nodeDict = new Dictionary<Guid, QuestionWithHierarchy>();
var rootNodes = new List<QuestionWithHierarchy>();

foreach (var item in flatData)
{
    var node = new QuestionWithHierarchy { Question = item.Question };
    nodeDict[item.Question.Id] = node;

    if (!item.ParentQuestionId.HasValue || !nodeDict.ContainsKey(item.ParentQuestionId.Value))
    {
        rootNodes.Add(node);
    }
    else
    {
        nodeDict[item.ParentQuestionId.Value].Children.Add(node);
    }
}

// rootNodes就是包含所有层级的嵌套结构

为什么这是最优方案?

  • 单次SQL查询:不管哪种方案,都是一次数据库交互,避免了多次查询的性能开销;
  • 数据库层面递归:递归CTE是数据库原生支持的高效递归方式,比在应用层多次查询快得多;
  • 灵活性:两种方案分别适配了类型安全和自定义需求,覆盖不同场景。

对比你原来的查询,这种方式能一次性获取所有层级的子问题,完美解决你的需求。

内容的提问来源于stack exchange,提问作者K.Z

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 10:25:31