【问题标题】:How does Entity Framework work with recursive hierarchies? Include() seems not to work with it实体框架如何处理递归层次结构? Include() 似乎不适用于它
【发布时间】:2010-11-21 10:40:14
【问题描述】:

我有一个ItemItem 有一个 Category

CategoryIDNameParentChildrenParentChildren 也属于 Category

当我对特定的Item 执行 LINQ to Entities 查询时,它不会返回相关的Category,除非我使用Include("Category") 方法。但它并没有带来完整的类别,包括它的父母和孩子。我可以做Include("Category.Parent"),但是这个对象有点像一棵树,我有一个递归层次结构,我不知道它在哪里结束。

如何让 EF 完全加载 Category,带有父子和子,以及父子及其父子,等等?

这不是整个应用程序的东西,出于性能考虑,它只需要这个特定实体,即类别。

【问题讨论】:

  • 如果它是递归的,那么它可以(很容易)有一个循环。您必须选择最大深度。完成后,您可以为其编写查询。
  • 不,不允许循环。业务层将此视为一棵树,父级没有机会将子级添加为父级。
  • 如果不允许循环,那么最好的关系模型可能是嵌套集。不过,这取决于您的应用程序。 NS 主要用于查询,FK 主要用于插入/更新/删除。但映射会完全不同。
  • 不知道需要换机型。但是在进行主要选择时,嵌套集的速度要快得多。 RDBMS 通常要么 (1) 根本不支持层次结构(例如,SQL 2005),要么 (2) 在某种程度上具有它,但不在其 EF 提供程序中公开它(例如,SQL 2008)。您要求 EF 支持 DB 在直接 SQL 中无法执行的操作! (想一想:什么 SQL 语句会返回无限深度的 JOINed 行?) EF 中的急切加载通过更改 SQL 语句来工作。什么样的 SQL 语句会产生你想要的结果集?
  • 仅仅因为你的实现不允许循环,你正在使用的数据结构可以。因此 EF 不能让您急切地加载树。相反,您需要在需要相关项目时显式调用 Load。或者,您可以将表加载为平面集合并手动重新构建树。虽然这会向数据库发出单个读取请求,但管理更新变得更加棘手。

标签: c# visual-studio-2008 entity-framework


【解决方案1】:

您可以使用Load,而不是使用Include 方法。

然后你可以做一个 for each 并循环遍历所有的孩子,加载他们的孩子。然后通过他们的孩子为每个人做一个,依此类推。

您所走的关卡数量将被硬编码为您拥有的每个循环的数量。

这里是一个使用Load的例子:http://msdn.microsoft.com/en-us/library/bb896249.aspx

【讨论】:

  • 我试过 Load,但抛出了 SqlDataReader 异常(即使启用 MultipleActiveResultSets 时)
  • 问题是 SQL Server 2000。它不支持 MARS。
  • 出于性能原因,这是一个非常糟糕的解决方案:每个级别都是对数据库的另一次访问,如果启用了延迟加载,情况会变得更糟。更多信息:stackoverflow.com/a/22024714/237723
  • @reggaeguitar 这个问题充斥着不太好的答案。从另一个问题的更彻底的答案中复制和粘贴相同的代码将是浪费的。还有 18 > 5 stackoverflow.com/questions/2266473/…
  • @JoeBrockhaus 我指的是 msdn 链接
【解决方案2】:

如果您确实希望加载整个层次结构,那么如果是我,我会尝试编写一个存储过程,它的工作是返回层次结构中的所有项目,返回您首先要求的项目(以及随后的子项目) )。

然后让 EF 的关系修复确保它们都连接起来。

即类似:

// the GetCategoryAndHierarchyById method is an enum
Category c = ctx.GetCategoryAndHierarchyById(1).ToList().First();

如果您正确编写了存储过程,实现层次结构中的所有项目(即ToList())应该会启动 EF 关系修复。

然后你想要的项目 (First()) 应该加载它的所有子项,并且他们应该加载他们的子项等。所有这些都是从那个存储过程调用中填充的,所以也没有 MARS 问题。

希望对你有帮助

亚历克斯

【讨论】:

  • 这仅在您的 SPROC 映射到函数导入属性窗口中的类别实体时才有效,并且仅返回该数据。即:如果您的 SPROC 执行与 EntityTableAEntityTableB 的连接,并且您的目标是获得具有导航属性的 EntityTableA 实体的结果集合到 EntityTableB 实体,您将无法使用此方法。相反,您必须相应地对扁平化的数据集进行分组并转换为相应的实体。
  • 如果您使用 SQL Server 并选择 SP 解决方案,您可以使用 CTE 在一个查询中获取层次结构。见:technet.microsoft.com/en-us/library/ms186243(v=sql.105).aspx
【解决方案3】:

如果你碰巧加载了所有递归实体,尤其是在类别上,这可能会很危险,你最终可能会得到比你预想的要多得多的结果:

Category > Item > OrderLine > Item
                  OrderHeader > OrderLine > Item
         > Item > ...

突然间,您已经加载了大部分数据库,您还可以加载发票行,然后是客户,然后是所有其他发票。

你应该做如下的事情:

var qryCategories = from q in ctx.Categories
                    where q.Status == "Open"
                    select q;

foreach (Category cat in qryCategories) {
    if (!cat.Items.IsLoaded)
        cat.Items.Load();
    // This will only load product groups "once" if need be.
    if (!cat.ProductGroupReference.IsLoaded)
        cat.ProductGroupReference.Load();
    foreach (Item item in cat.Items) {
        // product group and items are guaranteed
        // to be loaded if you use them here.
    }
}

然而,一个更好的解决方案是构建您的查询以使用结果构建一个匿名类,因此您只需访问您的数据存储区一次。

var qryCategories = from q in ctx.Categories
                    where q.Status == "Open"
                    select new {
                        Category = q,
                        ProductGroup = q.ProductGroup,
                        Items = q.Items
                    };

如果需要,您可以通过这种方式返回字典结果。

请记住,您的上下文应尽可能短。

【讨论】:

  • 这会导致数据库的大量往返,而急切加载是为了避免这种情况。
  • 这正是为什么我说“更好的解决方案是构建查询以使用结果构建匿名类,因此您只需要访问数据存储区一次”,正如我指出的那样,急切加载不是总是一个很好的解决方案。另请注意,您的参考数据只会加载一次,从而导致上下文生命周期内的点击次数减少。
  • 啊,我明白了;对当时的反对票感到抱歉。我刚刚看到“这是你应该做的”部分,然后是我认为 OP 不应该做的事情。如果您编辑答案(也许限定“这是您应该做的”?)我将能够推翻我的投票。
【解决方案4】:

使用这种调用硬编码版本Include的扩展方法,实现动态深度包含,效果很好。

namespace System.Data.Entity
{
  using Linq;
  using Linq.Expressions;
  using Text;

  public static class QueryableExtensions
  {
    public static IQueryable<TEntity> Include<TEntity>(this IQueryable<TEntity> source,
      int levelIndex, Expression<Func<TEntity, TEntity>> expression)
    {
      if (levelIndex < 0)
        throw new ArgumentOutOfRangeException(nameof(levelIndex));
      var member = (MemberExpression)expression.Body;
      var property = member.Member.Name;
      var sb = new StringBuilder();
      for (int i = 0; i < levelIndex; i++)
      {
        if (i > 0)
          sb.Append(Type.Delimiter);
        sb.Append(property);
      }
      return source.Include(sb.ToString());
    }
  }
}

用法:

var affiliate = await DbContext.Affiliates
  .Include(3, a => a.Referrer)
  .SingleOrDefaultAsync(a => a.Id == affiliateId);

不管怎样,同时,在 EF repo 上加入 discussion 关于它。

【讨论】:

  • 这很棒。而且讨论很棒。但是,我不确定在这种情况下如何使用您的表达式。即使在您的示例中,如果您使用 Include(4, m => m.Referred),那也行不通?如果您的 Queryable 是 IQueryable,那么您的 Func 返回值将是一个数组?
  • @jsgoupil,没有。如您所见,返回值的类型与输入参数IQueryable&lt;TEntity&gt; 完全相同,并适用于延迟执行。您可以在单个查询中多次将其用作流畅的 API 查询和任何 Inclde 扩展方法,以在返回数据中包含各种分支。
  • 对不起,我有点不知所措。您能否提供一个示例,说明如何在 Affiliate 类示例中快速使用它?
  • @jsgoupil 我已根据您的要求更新了我的答案。有什么不清楚的可以跟我说。 levelIndex 参数的参数 3 说明了我们要加载其子级的深度级别。
  • 谢谢,这是有道理的。我早些时候感到困惑,因为其他答案中的示例是在谈论“儿童”,而不是像您的示例中那样谈论“儿童”。感谢您在这里使用。
【解决方案5】:

您不想递归加载层次结构,除非您允许用户迭代地向下/向上钻取树:每一级递归都是对数据库的另一次访问。同样,您需要延迟加载以防止在呈现到页面或通过 Web 服务发送时遍历层次结构时进一步的 DB 行程。

相反,翻转您的查询:获取CatalogInclude 中的项目。这将使您获得分层(导航属性)和扁平化的所有项目,因此现在您只需要排除根中存在的非根元素,这应该很简单。

我遇到了这个问题,并将此解决方案的详细示例提供给另一个 here

【讨论】:

    【解决方案6】:

    您应该引入一个映射表,将每个类别映射为父级和子级,而不是将父级和子级属性添加到货物本身。

    根据您需要该信息的频率,可以按需查询。通过数据库中的独特约束,您可以避免无限量的关系。

    【讨论】:

      【解决方案7】:

      我发现如果你包含“两个父级别”,你会得到整个父层次结构,像这样:

      var query = Context.Items
                  .Include(i => i.Category)
                  .Include(i => i.Category.Parent.Parent)
      

      【讨论】:

      • 我怀疑这行得通,因为每个类别都与一个项目相关联,或者有一个孙子项目与一个项目相关联。当您将所有项目加载到上下文中时,您会获得所有类别,并且上下文中的关系魔法将它们全部连接起来。当我试图只获得一个“项目”时,它对我不起作用,所以这是上述答案的一个特例,它依赖于“首先将整个层次结构加载到内存中”,它有效,但对于真正的东西有点痛苦一个非常简单的递归 sql 查询。 EF 应该有这个内置的。
      【解决方案8】:

      现在是一种完全不同的分层数据方法,例如填充树视图。

      首先,对所有数据进行平面查询,然后在内存中构建对象图:

        var items = this.DbContext.Items.Where(i=> i.EntityStatusId == entityStatusId).Select(a=> new ItemInfo() { 
                  Id = a.Id,
                  ParentId = a.ParentId,
                  Name = a.Name,
                  ItemTypeId = a.ItemTypeId
                  }).ToList();
      

      获取根项:

       parent = items.FirstOrDefault(a => a.ItemTypeId == (int)Enums.ItemTypes.Root);
      

      现在构建您的图表:

       this.GetDecendantsFromList(parent, items);
      
      
       private void GetDecendantsFromList(ItemInfo parent, List<ItemInfo> items)
          {
              parent.Children = items.Where(a => a.ParentId == parent.Id).ToList();
              foreach (var child in parent.Children)
              {
                  this.GetDecendantsFromList(child,items);
              }
          }
      

      【讨论】:

        【解决方案9】:

        这是我发现的一个聪明的递归函数here,它适用于这个:

        public partial class Category
        {
            public IEnumerable<Category> AllSubcategories()
            {
                yield return this;
                foreach (var directSubcategory in Subcategories)
                    foreach (var subcategory in directSubcategory.AllSubcategories())
                    {
                        yield return subcategory;
                    }
            }
        }
        

        【讨论】:

        【解决方案10】:

        您还可以在数据库中创建一个表值函数并将其添加到您的 DBContext。然后你可以从你的代码中调用它。

        此示例要求您从以下位置导入 EntityFramework.Functions nuget。

        public class FunctionReturnType
        {
            public Guid Id { get; set; } 
        
            public Guid AnchorId { get; set; } //the zeroPoint for the recursion
        
            // Add other fields as you want (add them to your tablevalued function also). 
            // I noticed that nextParentId and depth are useful
        }
        
        public class _YourDatabaseContextName_ : DbContext
        {
            [TableValuedFunction("RecursiveQueryFunction", "_YourDatabaseContextName_")]
            public IQueryable<FunctionReturnType> RecursiveQueryFunction(
                [Parameter(DbType = "boolean")] bool param1 = true
            )
            {
                //Example how to add parameters to your function
                //TODO: Ask how to make recursive queries with SQL 
                var param1 = new ObjectParameter("param1", param1);
                return this.ObjectContext().CreateQuery<FunctionReturnType>(
                    $"RecursiveQueryFunction(@{nameof(param1)})", param1);
            }
        
            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                //add both (Function returntype and the actual function) to your modelbuilder. 
                modelBuilder.ComplexType<FunctionReturnType>();
                modelBuilder.AddFunctions(typeof(_YourDatabaseContextName_), false);
        
                base.OnModelCreating(modelBuilder);
            }
        
            public IEnumerable<Category> GetParents(Guid id)
            {
                //this = dbContext
                return from hierarchyRow in this.RecursiveQueryFunction(true)
                    join yourClass from this.Set<YourClassThatHasHierarchy>()
                    on hierarchyRow.Id equals yourClass.Id
                    where hierarchyRow.AnchorId == id
                    select yourClass;
            }
        }
        

        【讨论】:

          【解决方案11】:

          试试这个

          List<SiteActionMap> list = this.GetQuery<SiteActionMap>()
                          .Where(m => m.Parent == null && m.Active == true)
                          .Include(m => m.Action)
                          .Include(m => m.Parent).ToList();    
          
          if (list == null)
              return null;
          
          this.GetQuery<SiteActionMap>()
              .OrderBy(m => m.SortOrder)
              .Where(m => m.Active == true)
              .Include(m => m.Action)
              .Include(m => m.Parent)
              .ToList();
          
          return list;
          

          【讨论】:

            【解决方案12】:

            @parliament 给了我一个关于 EF6 的想法。具有将所有父节点加载到根节点和所有子节点的方法的类别示例。

            注意:仅将其用于非性能关键操作。来自http://nosalan.blogspot.se/2012/09/hierarchical-data-and-entity-framework-4.html 的 1000 个节点性能示例。

            Loading 1000 cat. with navigation properties took 15259 ms 
            Loading 1000 cat. with stored procedure took 169 ms
            

            代码:

            public class Category 
            {
                [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
                public int Id { get; set; }
            
                public string Name { get; set; }
            
                public int? ParentId { get; set; }
            
                public virtual Category Parent { get; set; }
            
                public virtual ICollection<Category> Children { get; set; }
            
                private IList<Category> allParentsList = new List<Category>();
            
                public IEnumerable<Category> AllParents()
                {
                    var parent = Parent;
                    while (!(parent is null))
                    {
                        allParentsList.Add(parent);
                        parent = parent.Parent;
                    }
                    return allParentsList;
                }
            
                public IEnumerable<Category> AllChildren()
                {
                    yield return this;
                    foreach (var child in Children)
                    foreach (var granChild in child.AllChildren())
                    {
                        yield return granChild;
                    }
                }   
            }
            

            【讨论】:

              【解决方案13】:

              我的建议是

              var query = CreateQuery()
                  .Where(entity => entity.Id == Id)
                  .Include(entity => entity.Parent);
              var result = await FindAsync(query);
              
              return result.FirstOrDefault();
              

              这意味着它将加载单个entity 和所有这些entity.Parent 实体recursive

              entity is same as entity.Parent
              

              【讨论】:

                【解决方案14】:
                public static class EntityFrameworkExtensions
                {
                    public static ObjectContext GetObjectContext(this DbContext context) 
                    {
                        ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
                
                        return objectContext;
                    }
                
                    public static string GetTableName<T>(this ObjectSet<T> objectSet) 
                        where T : class
                    {
                        string sql = objectSet.ToTraceString();
                        Regex regex = new Regex("FROM (?<table>.*) AS");
                        Match match = regex.Match(sql);
                
                        string table = match.Groups["table"].Value;
                        return table;
                    }
                
                    public static IQueryable<T> RecursiveInclude<T>(this IQueryable<T> query, Expression<Func<T, T>> navigationPropertyExpression, DbContext context)
                        where T : class
                    {
                        var objectContext = context.GetObjectContext();
                
                        var entityObjectSet = objectContext.CreateObjectSet<T>();
                        var entityTableName = entityObjectSet.GetTableName();
                        var navigationPropertyName = ((MemberExpression)navigationPropertyExpression.Body).Member.Name;
                
                        var navigationProperty = entityObjectSet
                            .EntitySet
                            .ElementType
                            .DeclaredNavigationProperties
                            .Where(w => w.Name.Equals(navigationPropertyName))
                            .FirstOrDefault();
                
                        var association = objectContext.MetadataWorkspace
                            .GetItems<AssociationType>(DataSpace.SSpace)
                            .Single(a => a.Name == navigationProperty.RelationshipType.Name);
                
                        var pkName = association.ReferentialConstraints[0].FromProperties[0].Name;
                        var fkName = association.ReferentialConstraints[0].ToProperties[0].Name;
                
                        var sqlQuery = @"
                                EXEC ('
                                    ;WITH CTE AS
                                    (
                                        SELECT 
                                            [cte1].' + @TABLE_PK + '
                                            , Level = 1
                                        FROM ' + @TABLE_NAME + ' [cte1]
                                        WHERE [cte1].' + @TABLE_FK + ' IS NULL
                
                                        UNION ALL
                
                                        SELECT 
                                            [cte2].' + @TABLE_PK + '
                                            , Level = CTE.Level + 1
                                        FROM ' + @TABLE_NAME + ' [cte2]
                                            INNER JOIN CTE ON CTE.' + @TABLE_PK + ' = [cte2].' + @TABLE_FK + '
                                    )
                                    SELECT 
                                        MAX(CTE.Level)
                                    FROM CTE 
                                ')
                            ";
                
                        var rawSqlQuery = context.Database.SqlQuery<int>(sqlQuery, new SqlParameter[]
                            {
                                new SqlParameter("TABLE_NAME", entityTableName),
                                new SqlParameter("TABLE_PK", pkName),
                                new SqlParameter("TABLE_FK", fkName)
                            });
                
                        var includeCount = rawSqlQuery.FirstOrDefault();
                
                        var include = string.Empty;
                
                        for (var i = 0; i < (includeCount - 1); i++)
                        {
                            if (i > 0)
                                include += ".";
                
                            include += navigationPropertyName;
                        }
                
                        return query.Include(include);
                    }
                }
                

                【讨论】:

                • context.YourEntitySet.RecursiveInclude(i =&gt; i.YourIncludeProperty, context)
                【解决方案15】:

                让我提供我的简单解决方案,以满足启用/禁用组织所选部门结构的分层数据分支的需求。

                Departments 表根据这条 SQL 查找

                CREATE TABLE [dbo].[Departments](
                    [ID] [int] IDENTITY(1,1) NOT NULL,
                    [Name] [nvarchar](1000) NOT NULL,
                    [OrganizationID] [int] NOT NULL,
                    [ParentID] [int] NULL,
                    [IsEnabled] [bit] NOT NULL, 
                 CONSTRAINT [PK_Departments] PRIMARY KEY CLUSTERED 
                (
                    [ID] ASC
                )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
                ) ON [PRIMARY]
                GO
                

                C# 代码提供了一种非常简单的方法,对我来说效果很好。 1. 它异步返回完整的表。 2. 它改变了链接行的属性。

                public async Task<bool> RemoveDepartmentAsync(int orgID, int depID)
                            {
                                try
                                {
                                    using (var db = new GJobEntities())
                                    {
                                        var org = await db.Organizations.FirstOrDefaultAsync(x => x.ID == orgID); // Check if  the organization exists
                                        if (org != null)
                                        {
                                            var allDepartments = await db.Departments.ToListAsync(); // get all table items
                                            var isExisting = allDepartments.FirstOrDefault(x => x.OrganizationID == orgID && x.ID == depID);
                                            if (isExisting != null) // Check if the department exists
                                            {
                                                isExisting.IsEnabled = false; // Change the property of visibility of the department
                                                var all = allDepartments.Where(x => x.OrganizationID == orgID && x.ID == isExisting.ID).ToList();
                                                foreach (var item in all)
                                                {
                                                    item.IsEnabled = false;
                                                    RecursiveRemoveDepartment(orgID, item.ID, ref allDepartments); // Loop over table data set to change property of the linked items
                                                }
                                                await db.SaveChangesAsync();
                                            }
                                            return true;
                                        }
                                    }
                                }
                                catch (Exception ex)
                                {
                                    logger.Error(ex);
                                }
                
                                return false;
                            }
                
                            private void RecursiveRemoveDepartment(int orgID, int? parentID, ref List<Department> items)
                            {
                                var all = items.Where(x => x.OrganizationID == orgID && x.ParentID == parentID);
                                foreach (var item in all)
                                {
                                    item.IsEnabled = false;
                                    RecursiveRemoveDepartment(orgID, item.ID, ref items);
                                }
                            }
                

                这种方法对于我猜少于 100000 的相对少量记录非常有效。 可能对于大量数据,您必须实现服务器端存储功能。

                享受吧!

                【讨论】:

                  猜你喜欢
                  • 2013-04-16
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2023-03-24
                  • 2017-05-25
                  • 2011-03-31
                  • 1970-01-01
                  相关资源
                  最近更新 更多