简体   繁体   English

实体框架如何处理递归层次结构? Include() 似乎不适用于它

[英]How does Entity Framework work with recursive hierarchies? Include() seems not to work with it

I have an Item .我有一个Item Item has a Category . Item有一个Category

Category has ID , Name , Parent and Children . CategoryIDNameParentChildren Parent and Children are of Category too. ParentChildren也属于Category

When I do a LINQ to Entities query for a specific Item , it doesn't return the related Category , unless I use the Include("Category") method.当我对特定Item执行 LINQ to Entities 查询时,它不会返回相关的Category ,除非我使用Include("Category")方法。 But it doesn't bring the full category, with its parent and children.但它没有带来完整的类别,包括它的父级和子级。 I could do Include("Category.Parent") , but this object is something like a tree, I have a recursive hierarchy and I don't know where it ends.我可以做Include("Category.Parent") ,但这个对象有点像一棵树,我有一个递归层次结构,我不知道它在哪里结束。

How can I make EF fully load the Category , with parent and children, and the parent with their parent and children, and so on?我怎样才能让 EF 完全加载Category ,带有父级和子级,以及父级和他们的父级和子级等等?

This is not something for the whole application, for performance considerations it would be needed only for this specific entity, the Category.这不是整个应用程序的东西,出于性能考虑,它只需要这个特定实体,即类别。

Instead of using the Include method you could use Load .您可以使用Load代替使用Include方法。

You could then do a for each and loop through all the children, loading their children.然后你可以为每个人做一个循环遍历所有的孩子,加载他们的孩子。 Then do a for each through their children, and so on.然后通过他们的孩子为每个人做一个,依此类推。

The number of levels down you go will be hard coded in the number of for each loops you have.您向下的级别数将被硬编码为您拥有的每个循环的数量。

Here is an example of using Load : http://msdn.microsoft.com/en-us/library/bb896249.aspx这是使用Load的示例: http : //msdn.microsoft.com/en-us/library/bb896249.aspx

If you definitely want the whole hierarchy loaded, then if it was me I'd try writing a stored procedure who's job it is to return all the items in a hierarchy, returning the one you ask for first (and its children subsequently).如果您确实想要加载整个层次结构,那么如果是我,我会尝试编写一个存储过程,它的工作是返回层次结构中的所有项目,返回您首先要求的项目(以及随后的子项目)。

And then let the EF's relationship fixup ensure that they are all hooked up.然后让 EF 的关系修复确保它们都连接起来。

ie something like:即类似:

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

If you've written your stored procedure correctly, materializing all the items in the hierarchy (ie ToList() ) should make EF relationship fixup kicks in.如果您正确编写了存储过程,则将层次结构中的所有项目(即ToList() )具体化应该会使 EF 关系修复开始。

And then the item you want (First()) should have all its children loaded and they should have their children loaded etc. All be populated from that one stored procedure call, so no MARS problems either.然后你想要的项目 (First()) 应该加载它的所有孩子,他们应该加载他们的孩子等等。所有这些都是从那个存储过程调用中填充的,所以也没有 MARS 问题。

Hope this helps希望这可以帮助

Alex亚历克斯

It could be dangerous if you did happen to load all recursive entities, especially on category, you could end up with WAY more than you bargained for:如果您碰巧加载了所有递归实体,尤其是在类别上,这可能会很危险,您最终得到的结果可能比您预想的要多:

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

All of a sudden you've loaded most of your database, you could have also loaded invoices lines, then customers, then all their other invoices.突然之间,您已经加载了大部分数据库,您还可以加载发票行,然后是客户,然后是所有其他发票。

What you should do is something like the following:你应该做的是如下:

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

A better solution however is to construct your query to build an anonymous class with the results so you only need to hit your datastore once.然而,更好的解决方案是构建查询以使用结果构建匿名类,这样您只需访问数据存储一次。

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

This way you could return a dictionary result if required.如果需要,您可以通过这种方式返回字典结果。

Remember, your contexts should be as short lived as possible.请记住,您的上下文应该尽可能短。

Use this extension method which calls the hard-coded version of Include , to achieve a dynamic depth level of inclusion, it works great.使用这种调用硬编码版本Include扩展方法来实现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());
    }
  }
}

Usage:用法:

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

Anyway, meanwhile, join the discussion about it on the EF repo.无论如何,同时,在 EF 回购中加入有关它的讨论

You don't want to do recursive loading of the hierarchy, unless you are allowing a user to iteratively drill down/up the tree: Every level of recursion is another trip to the database.您不想递归加载层次结构,除非您允许用户迭代地向下/向上钻取树:递归的每个级别都是对数据库的另一次访问。 Similarly, you'll want lazy loading off to prevent further DB trips as you're traversing the hierarchy when rendering to a page or sending over a webservice.类似地,您需要延迟加载以防止在呈现到页面或通过 Web 服务发送时遍历层次结构时进一步的 DB 行程。

Instead, flip your query: Get Catalog , and Include the items in it.相反,翻转您的查询:获取Catalog ,并在其中Include项目。 This will get you all items both hierarchically (navigation properties) and flattened, so now you just need to exclude the non-root elements present at the root, which should be pretty trivial.这将使您获得分层(导航属性)和扁平化的所有项目,因此现在您只需要排除根中存在的非根元素,这应该非常简单。

I had this problem and provided a detailed example of this solution to another, here我遇到了这个问题,并在此处为另一个问题提供了此解决方案的详细示例

You chould rather introduce a mapping table that maps each Category a parent and a child, instead of adding the parent and child property to the cargo itself.您最好引入一个映射表,将每个 Category 映射为父级和子级,而不是将父级和子级属性添加到货物本身。

Depending on how often you need that information it can be queried on demand.根据您需要该信息的频率,可以按需查询。 Via unique constraints in the db you can avoid an infinite amount of relationships beeing possible.通过数据库中的唯一约束,您可以避免可能的无限关系。

I found out that if you include "two parent levels", you will get the whole parent hierarchy, like that:我发现如果您包含“两个父级别”,您将获得整个父层次结构,如下所示:

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

And now for a completely different approach to hierarchical data, for example populating a treeview.现在对于分层数据采用完全不同的方法,例如填充树视图。

First, do a flat query for all data, and then build the object graph in memory:首先对所有数据做一个平面查询,然后在内存中构建对象图:

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

Get the root item:获取根项:

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

Now build your graph:现在构建你的图表:

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

Here is a clever recursive function I found here that would work for this:这是我在这里找到的一个聪明的递归函数,它可以解决这个问题:

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

You could also create a tablevalued function in the database and add that to your DBContext.您还可以在数据库中创建一个表值函数并将其添加到您的 DBContext。 Then you can call that from your code.然后你可以从你的代码中调用它。

This example requires that you import EntityFramework.Functions from nuget.此示例要求您从 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;
    }
}

try this尝试这个

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;

@parliament gave me an idea for EF6. @parliament 给了我一个关于 EF6 的想法。 Example for Category with Methods to load all parents up to root node and all children.具有将所有父节点加载到根节点和所有子节点的方法的类别示例。

NOTE: Use this only for non performance critical operation.注意:这仅用于非性能关键操作。 Example with 1000 nodes performance from http://nosalan.blogspot.se/2012/09/hierarchical-data-and-entity-framework-4.html .来自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

Code:代码:

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

My suggestion would be我的建议是

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

return result.FirstOrDefault();

and it means it will load single entity and all this entity.Parent entities recursive .这意味着它将加载单个entity和所有实体。 entity.Parent实体recursive

entity is same as entity.Parent
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);
    }
}

Let me offer my simple solution that fits needs to enable/disable the branch of hierarchical data of the selected department's structure of an organization.让我提供适合启用/禁用组织的所选部门结构的分层数据分支的需求的简单解决方案。

The table Departments looks according this SQL表 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# code provides a very simple approach that work fine for me. C# 代码提供了一种非常简单的方法,对我来说效果很好。 1. It returns the complete table asynchronously. 1. 异步返回完整表。 2. It changes property for the linked rows. 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);
                }
            }

This approach works very fast for relative small amount of records I guess less 100000. Probably for big set of data you have to implement server side stored function.这种方法对于相对少量的记录来说非常快,我猜不到 100000。可能对于大数据集,您必须实现服务器端存储功能。

Enjoy!享受!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM