如何使用实体框架进行递归加载?

Posted

技术标签:

【中文标题】如何使用实体框架进行递归加载?【英文标题】:How to do recursive load with Entity framework? 【发布时间】:2010-02-15 14:17:39 【问题描述】:

我在 DB 中有一个带有 TreeNodes 表的树结构。该表具有 nodeId、parentId 和 parameterId。在 EF 中,结构类似于 TreeNode.Children,其中每个孩子都是一个 TreeNode... 我还有一个包含 id、name 和 rootNodeId 的 Tree 表。

一天结束时,我想将树加载到 TreeView 中,但我不知道如何一次将其全部加载。 我试过了:

var trees = from t in context.TreeSet.Include("Root").Include("Root.Children").Include("Root.Children.Parameter")
        .Include("Root.Children.Children")
                        where t.ID == id
                        select t;

这将使我获得前 2 代,但不会更多。 如何加载包含所有世代和附加数据的整个树?

【问题讨论】:

如果这对您来说仍然是个问题,我已经提供了另一个答案,它比喋喋不休的数据库调用更好,并且能够填充整个层次结构而无需指定无限包含。 【参考方案1】:

我最近遇到了这个问题,在我想出了一个简单的方法来实现结果之后偶然发现了这个问题。我提供了对 Craig 的答案的编辑,提供了第 4 种方法,但权力决定它应该是另一个答案。这对我来说很好:)

My original question / answer can be found here.

只要您在表中的项目都知道它们属于哪棵树(在您的情况下看起来像:t.ID),这就会起作用。也就是说,目前尚不清楚您真正拥有哪些实体,但即使您拥有多个实体,您也必须在实体 Children 中拥有一个 FK,如果那不是 TreeSet

基本上不要使用Include():

var query = from t in context.TreeSet
            where t.ID == id
            select t;

// if TreeSet.Children is a different entity:
var query = from c in context.TreeSetChildren
            // guessing the FK property TreeSetID
            where c.TreeSetID == id
            select c;

这将带回树的所有项目并将它们全部放在集合的根目录中。此时,您的结果集将如下所示:

-- Item1
   -- Item2
      -- Item3
-- Item4
   -- Item5
-- Item2
-- Item3
-- Item5

由于您可能希望您的实体分层地从 EF 中出来,这不是您想要的,对吧?

.. 然后,排除存在于根级别的后代:

幸运的是,由于您的模型中有导航属性,子实体集合仍将被填充,如上面结果集的插图所示。通过使用foreach() 循环手动迭代结果集,并将这些根项添加到new List<TreeSet>(),您现在将拥有一个包含根元素和所有后代正确嵌套的列表。

如果您的树变大并且性能受到关注,您可以按ParentID(它是Nullable,对吗?)对返回集 ASCENDING 进行排序,以便所有根项都排在第一位。像以前一样迭代和添加,但是一旦你到达一个非空的循环就会中断。

var subset = query
     // execute the query against the DB
     .ToList()
     // filter out non-root-items
     .Where(x => !x.ParentId.HasValue);

现在subset 将如下所示:

-- Item1
   -- Item2
      -- Item3
-- Item4
   -- Item5

关于 Craig 的解决方案:

    真的不想为此使用延迟加载!围绕 n+1 查询的必要性构建的设计将是一个主要的性能吸盘。 *********(好吧,公平地说,如果您要允许用户有选择地向下钻取树,那么它可能是合适的。只是不要使用延迟加载来将它们全部提前!)我从未尝试过嵌套设置东西,我也不建议破解 EF 配置来完成这项工作,因为有一个更简单的解决方案。 Another reasonable suggestion 正在创建一个提供自链接的数据库视图,然后将该视图映射到中间连接/链接/m2m 表。就个人而言,我发现这个解决方案比必要的复杂,但它可能有它的用途。

【讨论】:

JoeBrockhaus,我没有测试您的代码,但有几件事:1)该代码只是将数据作为单个节点而不是作为树节点进行迭代和填充。 2)永远不会添加根节点。也许您应该尝试在 ICollectionable problery 中分配子对象。这样做你会创建一棵树。我不会投反对票,但请改进您的答案。 @GabrielAndrésBrancolini 最初的问题缺乏关于模型的一些细节,但我的回答解决了我提到的两种可能的情况。至于孩子的集合,这些将自动填充,因为(并且只要)为实体定义了导航属性。其中之一将是根节点。最终的变化在于逻辑:不要考虑查询父级并获取其递归子级,而只需查询所有子级并在需要时独立获取父级。 另外,关于否决票的评论是因为原始答案在进行任何其他投票或 cmet 之前被否决了,至少有些人认为这个答案是对该问题的更好答案。跨度> 这个答案对我很有用,尽管在使用延迟加载时需要一个额外的步骤。我将Entity Framework Core 3.1 与延迟加载代理一起使用。因此,即使在填充树时,代理也会在访问 Children 时触发额外的负载。因此,解决方案是为每个返回的节点添加dbContext.Entry(treeNode).Collection(n => n.Children).IsLoaded = true;【参考方案2】:

当您使用Include() 时,您要求实体框架将您的查询转换为 SQL。所以想一想:你将如何编写返回任意深度的树的 SQL 语句?

回答:除非您使用数据库服务器的特定层次结构功能(这些功能不是 SQL 标准,但某些服务器支持,例如 SQL Server 2008,尽管它的实体框架提供程序不支持),否则您不会。在 SQL 中处理任意深度树的常用方法是使用 the nested sets model 而不是父 ID 模型。

因此,您可以使用三种方法来解决此问题:

    使用嵌套集模型。这需要更改您的元数据。 使用 SQL Server 的层次结构功能,并破解实体框架以理解它们(很棘手,但 this technique might work)。同样,您需要更改元数据。i 使用显式加载或 EF 4 的延迟加载而不是急切加载。这将导致很多数据库查询,而不是一个。

【讨论】:

最后,我添加了一个递归调用,该调用在子级和其他引用对象上调用 Load。谢谢 仅供参考...“嵌套集模型”链接无效。 InformationWeek 说“无效的 URL”。【参考方案3】:

我想发布我的答案,因为其他人没有帮助我。

我的数据库有点不同,基本上我的表有一个 ID 和一个 ParentID。该表是递归的。以下代码获取所有子级并将它们嵌套到最终列表中。

public IEnumerable<Models.MCMessageCenterThread> GetAllMessageCenterThreads(int msgCtrId)

    var z = Db.MCMessageThreads.Where(t => t.ID == msgCtrId)
        .Select(t => new MCMessageCenterThread
        
            Id = t.ID,
            ParentId = t.ParentID ?? 0,
            Title = t.Title,
            Body = t.Body
        ).ToList();

    foreach (var t in z)
    
        t.Children = GetChildrenByParentId(t.Id);
    

    return z;


private IEnumerable<MCMessageCenterThread> GetChildrenByParentId(int parentId)

    var children = new List<MCMessageCenterThread>();

    var threads = Db.MCMessageThreads.Where(x => x.ParentID == parentId);

    foreach (var t in threads)
    
        var thread = new MCMessageCenterThread
        
            Id = t.ID,
            ParentId = t.ParentID ?? 0,
            Title = t.Title,
            Body = t.Body,
            Children = GetChildrenByParentId(t.ID)
        ;

        children.Add(thread);
    

    return children;

为了完整起见,这是我的模型:

public class MCMessageCenterThread

    public int Id  get; set; 
    public int ParentId  get; set; 
    public string Title  get; set; 
    public string Body  get; set; 

    public IEnumerable<MCMessageCenterThread> Children  get; set; 

【讨论】:

今天这对我有帮助,谢谢! 这是递归加载方式。它将产生对数据库的 n 个查询。代码存在一些问题:1.GetAllMessageCenterThreads 返回一个列表,但只有一个项目 (.Where(t=&gt;t.ID == msgCtrId))。 2. 你的 ParentID 属性应该可以为空(int?int),并且 ParentId = t.ParentID ?? 0, 将始终返回“0”,因为 ParentID 不能为空(这不能是代码优先?)。 3. 看起来您的模型缺少一个属性:MessageCenterId。所有项目都应该有这个字段,所以你可以通过那个 id 获取所有项目。 如果您在这种情况下的目标是获得对特定线程的所有回复(相对于特定博客的所有线程),那么您应该查看公用表表达式(或 CTE),它们专门对于递归场景。在这种情况下,您将创建一个递归联合+自连接。看到这个答案:***.com/a/19915206/237723 这段代码取自一个数据库优先的例子;其次,如果您有更好的解决方案,也许您应该发布它,嗯?我想抨击别人的工作解决方案总是更有趣;)【参考方案4】:

我最近写了一个 N+1 选择加载整个树的东西,其中 N 是源对象中最深路径的层数。

鉴于以下自引用类,这就是我所做的

public class SomeEntity 

  public int Id  get; set; 
  public int? ParentId  get; set; 
  public string Name  get; set;

我写了以下 DbSet 助手

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;

namespace Microsoft.EntityFrameworkCore

    public static class DbSetExtensions
    
        public static async Task<TEntity[]> FindRecursiveAsync<TEntity, TKey>(
            this DbSet<TEntity> source,
            Expression<Func<TEntity, bool>> rootSelector,
            Func<TEntity, TKey> getEntityKey,
            Func<TEntity, TKey> getChildKeyToParent)
            where TEntity: class
        
            // Keeps a track of already processed, so as not to invoke
            // an infinte recursion
            var alreadyProcessed = new HashSet<TKey>();

            TEntity[] result = await source.Where(rootSelector).ToArrayAsync();

            TEntity[] currentRoots = result;
            while (currentRoots.Length > 0)
            
                TKey[] currentParentKeys = currentRoots.Select(getEntityKey).Except(alreadyProcessed).ToArray();
                alreadyProcessed.AddRange(currentParentKeys);

                Expression<Func<TEntity, bool>> childPredicate = x => currentParentKeys.Contains(getChildKeyToParent(x));
                currentRoots = await source.Where(childPredicate).ToArrayAsync();
            

            return result;
        
    

当你需要加载整个树时,你只需调用这个方法,传入三个东西

    根对象的选择标准 如何获取对象的主键属性(SomeEntity.Id) 如何获取引用其父级 (SomeEntity.ParentId) 的子级属性

例如

SomeEntity[] myEntities = await DataContext.SomeEntity.FindRecursiveAsync(
  rootSelector: x => x.Id = 42,
  getEntityKey: x => x.Id,
  getChildKeyToParent: x => x.ParentId).ToArrayAsync();
);

或者,如果您可以将RootId 列添加到表中,那么对于每个非根条目,您可以将此列设置为树根的 ID。然后,您可以通过一次选择获取所有内容

DataContext.SomeEntity.Where(x =&gt; x.Id == rootId || x.RootId == rootId)

【讨论】:

这对于 RPS 非常低的小型集合来说可能没问题,但如果必须,您确实应该在数据库中使用 CTE 来递归加载整个集合。 DataContext.SomeEntity.Where(x =&gt; x.Id == rootId || x.RootId == rootId)好吗? 如果您有rootId,那么您不需要数据库中的 CTE。 CTE 的价值只是在数据库中执行所有递归迭代,这样您就不会产生连接和往返开销。【参考方案5】:

对于加载子对象的示例,我将给出一个包含注释的 Comment 对象的示例。每个评论都有一个可能的子评论。

private static void LoadComments(<yourObject> q, Context yourContext)

    if(null == q | null == yourContext)
    
        return;
    
    yourContext.Entry(q).Reference(x=> x.Comment).Load();
    Comment curComment = q.Comment;
    while(null != curComment)
    
        curComment = LoadChildComment(curComment, yourContext);
    


private static Comment LoadChildComment(Comment c, Context yourContext)

    if(null == c | null == yourContext)
    
        return null;
    
    yourContext.Entry(c).Reference(x=>x.ChildComment).Load();
    return c.ChildComment;

现在,如果您拥有自己的集合,您将需要使用 Collection 而不是 Reference 并进行相同的潜水。至少这是我在处理实体和 SQLite 时在这种情况下采用的方法。

【讨论】:

【参考方案6】:

这是一个老问题,但其他答案要么有 n+1 个数据库命中,要么他们的模型有利于自下而上(树干到叶子)方法。在这种情况下,标签列表作为树加载,并且标签可以有多个父级。我使用的方法只有两个数据库命中:第一个获取所选文章的标签,然后另一个急切加载连接表。因此,这使用了自上而下(从树干到树干)的方法;如果您的连接表很大,或者如果结果不能真正被缓存以供重用,那么急切加载整个事物就会开始显示这种方法的权衡。

首先,我初始化了两个HashSets:一个保存根节点(结果集),另一个保存对每个被“命中”的节点的引用。

var roots = new HashSet<AncestralTagDto>(); //no parents
var allTags = new HashSet<AncestralTagDto>();

接下来,我抓取客户请求的所有叶子,将它们放入一个包含子集合的对象中(但在此步骤之后该集合将保持为空)。

var startingTags = await _dataContext.ArticlesTags
        .Include(p => p.Tag.Parents)
        .Where(t => t.Article.CategoryId == categoryId)
        .GroupBy(t => t.Tag)
        .ToListAsync()
        .ContinueWith(resultTask => 
             resultTask.Result.Select(
                  grouping => new AncestralTagDto(
                        grouping.Key.Id, 
                        grouping.Key.Name)));

现在,让我们抓取标签自连接表,并将其全部加载到内存中:

var tagRelations = await _dataContext.TagsTags.Include(p => p.ParentTag).ToListAsync();

现在,对于startingTags 中的每个标签,将该标签添加到allTags 集合中,然后沿着树向下递归地获取祖先:

foreach (var tag in startingTags)

    allTags.Add(tag);
    GetParents(tag);

return roots;

最后,这是构建树的嵌套递归方法:

void GetParents(AncestralTagDto tag)

    var parents = tagRelations.Where(c => c.ChildTagId == tag.Id).Select(p => p.ParentTag);
    if (parents.Any()) //then it's not a root tag; keep climbing down
    
        foreach (var parent in parents)
        
            //have we already seen this parent tag before? If not, instantiate the dto.
            var parentDto = allTags.SingleOrDefault(i => i.Id == parent.Id);
            if (parentDto is null)
            
                parentDto = new AncestralTagDto(parent.Id, parent.Name);
                allTags.Add(parentDto);
            

            parentDto.Children.Add(tag);
            GetParents(parentDto);
        
    
    else //the tag is a root tag, and should be in the root collection. If it's not in there, add it.
    
        //this block could be simplified to just roots.Add(tag), but it's left this way for other logic.
        var existingRoot = roots.SingleOrDefault(i => i.Equals(tag));
        if (existingRoot is null)
            roots.Add(tag);
    

在幕后,我依靠HashSet 的属性来防止重复。为此,您使用的中间对象(我在这里使用 AncestralTagDto,它的 Children 集合也是 HashSet)非常重要,根据您的用例覆盖 Equals 和 GetHashCode 方法。

【讨论】:

以上是关于如何使用实体框架进行递归加载?的主要内容,如果未能解决你的问题,请参考以下文章

如何实现递归自连接实体框架?

如何使用代码优先实体框架在 ASP.Net MVC3 中重新加载多对多导航属性

使用实体框架流利语法或内联语法编写递归 CTE

如何使用实体框架进行连接表搜索?

当我不知道记录是不是存在时,如何使用实体框架进行合并?

如何使用实体查询框架对以下查询进行建模