实体框架如何与递归层次结构一起使用?Include()似乎不适合


73

我有一个ItemItem有一个Category

CategoryIDNameParentChildrenParentChildrenCategory太。

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

如何让EF完全加载Category,带有父级和子级的,以及父级及其父级和子级的加载,依此类推?

对于整个应用程序而言,这不是什么,出于性能方面的考虑,仅对于此特定实体(类别)才需要。


2
如果是递归的,那么它可以(轻松地)有一个循环。您必须选择最大深度。完成之后,您可以为其编写查询。
2009年

2
不,不允许循环。业务层将其视为一棵树,父级没有机会将子级添加为父级。
维克多·罗德里格斯

如果不允许循环,那么最好的关系模型可能是嵌套集。但是,取决于您的应用程序。NS适用于大多数查询,FK适用于大多数插入/更新/删除。但是映射将完全不同。
2009年

2
我不知道您需要更改模型。但是,在进行主要选择时,嵌套集要快得多。RDBMS通常要么(1)根本不支持层次结构(例如SQL 2005),要么(2)在某种程度上拥有它,但是不将其公开给其EF提供程序(例如SQL 2008)。您是在要求EF支持数据库在直接SQL中无法完成的工作!(认为​​:哪种SQL语句将返回具有无限深度的JOINed行?)通过更改SQL语句,可以在EF中轻松加载。哪种SQL语句将生成所需的结果集?
2009年

2
仅仅因为您的实现不允许循环,所以您正在使用的数据结构可以。因此,EF无法让您热切地加载树。相反,您需要在需要相关项目时显式调用Load。或者,您可以将表作为平面集合加载并手动重新构建树。尽管这会向数据库发出一个读取请求,但管理更新变得更加棘手。
Jim Wooley,2009年

Answers:


25

Include可以使用代替使用该方法Load

然后,您可以为每个子对象执行一个操作,并遍历所有子对象,加载他们的子对象。然后为每个通过孩子的孩子做一个,依此类推。

下降的级别数将硬编码为每个循环的数量。

这是使用示例Loadhttp : //msdn.microsoft.com/zh-cn/library/bb896249.aspx


我尝试了Load,但抛出了SqlDataReader异常(即使启用MultipleActiveResultSets时也是如此)
Victor Rodrigues

问题是SQL Server2000。它不支持MARS。
维克多·罗德里格斯

37
出于性能方面的考虑,这是一个非常糟糕的解决方案:每个级别都是数据库的另一趟旅程,如果启用了延迟加载,则情况甚至更糟。此处的更多信息:stackoverflow.com/a/22024714/237723
JoeBrockhaus 2014年

@reggaeguitar这个问题充斥着不太好的答案。从更彻底的答案中复制并粘贴相同的代码会浪费很多时间。也18> 5 stackoverflow.com/questions/2266473/...
JoeBrockhaus

1
@JoeBrockhaus我指的是msdn链接
reggaeguitar

14

如果您确实要加载整个层次结构,那么如果是我,我将尝试编写一个存储过程,由谁来做,即返回层次结构中的所有项目,并返回您首先请求的项目(然后返回其子项)。

然后让EF的关系修复程序确保它们都已连接。

即类似:

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

如果您正确地编写了存储过程,则实现层次结构中的所有项目(即ToList())将使EF关系修正生效。

然后,您想要的项目(First())应该加载其所有子项,并且应该加载其子项,等等。所有这些都从该存储过程调用中填充,因此也没有MARS问题。

希望这可以帮助

亚历克斯


1
仅当您的SPROC映射到“函数导入”属性窗口中的“类别实体”时才有效,并且仅返回该数据。即:如果您的SPROC与EntityTableA和执行连接,并且&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&EntityTableB以具有EntityTableA导航属性的实体集合为EntityTableB实体,则可以使用该方法。取而代之的是,您必须对扁平化的数据集进行相应的分组,然后转换为相应的实体。
JoeBrockhaus 2014年

1
如果使用SQL Server并使用SP解决方案,则可以使用CTE在一个查询中获取层次结构。请参阅:technet.microsoft.com/zh-CN/library/ms186243
v

6

如果您确实碰巧加载了所有递归实体,则可能很危险,尤其是在类别上,最终可能获得比您讨价还价更多的WAY:

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
                    };

这样,您可以根据需要返回字典结果。

请记住,您的环境应尽可能短。


1
这导致数据库往返很多,而这种紧急加载是要避免的。
Asad Saeeduddin 2014年

2
这就是为什么我说“但是,更好的解决方案是构造查询以使用结果构建匿名类,因此您只需单击数据存储一次”,正如我指出的那样,急于加载并不总是一个好的解决方案。另请注意,您的参考数据将仅加载一次,从而在上下文生存期内减少命中。
Brett Ryan

1
我知道了 不好意思。我刚刚看到“这是您应该做的”部分,然后是我认为OP不应该做的事情。如果您编辑答案(也许限定为“这是您应该做什么”?),我将可以推翻我的投票。
Asad Saeeduddin

5

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

而是翻转查询:GetCatalogInclude其中的项目。这将使您获得所有项目的层次结构(导航属性)和展平的信息,因此现在您只需要排除存在于根目录中的非根目录元素,这应该是微不足道的。

我遇到了这个问题,并在此处向其他人提供了此解决方案的详细示例


5

使用此扩展方法调用的硬编码版本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存储库中加入有关它的讨论


这很棒。而且讨论很棒。但是,我不确定在这种情况下如何使用您的表达式。即使在您的示例中,如果您使用Include(4,m => m.Referred),那也行不通吗?如果您的Queryable是IQueryable <Affiliate>,那么您的Func返回值将是一个数组?
jsgoupil 2015年

@jsgoupil,不。如您所见,返回值的类型与输入参数的类型完全相同IQueryable<TEntity>,并且适用于延迟执行。您可以Inclde在单个查询上多次使用它作为流利的API查询和任何扩展方法,以在返回数据中包含各种分支。
Shimmy Weitzhandler

对不起,我有点茫然。您能否在Affiliate类示例中提供有关如何不久使用它的示例?
jsgoupil 2015年

1
@jsgoupil我已根据您的要求更新了答案。有什么不清楚的地方要和我谈谈。参数3levelIndex参数指出我们要加载其子级的深度级别。
Shimmy Weitzhandler,2015年

谢谢,这很有意义。之前我很困惑,因为其他答案中的示例都在谈论“孩子”,而不是您示例中的“孩子”。感谢您将用法放在这里。
jsgoupil 2015年

3

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

根据您需要该信息的频率,可以根据需要对其进行查询。通过数据库中的唯一约束,可以避免无数种可能的关系。


2

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

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

  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);
        }
    }

2

我发现,如果包括“两个父级”,则将获得整个父级,如下所示:

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

1

这是我在这里找到的一个聪明的递归函数,可以用于此目的:

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;
            }
    }
}

1
这对于内存中集合非常有用,但是我不建议您从数据库中主动检索结果。n + 1个FTL stackoverflow.com/questions/97197/what-is-the-n1-selects-issue
JoeBrockhaus

1

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

此示例要求您从nuget导入EntityFramework.Functions。

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;
    }
}

0

尝试这个

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;

0

@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;
        }
    }   
}

0

我的建议是

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

0
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 => i.YourIncludeProperty, context)
雨果·布鲁霍尔

0

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

表部门根据此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),此方法非常有效。可能对于实现服务器端存储功能的大量数据而言。

请享用!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.