Как Entity Framework работает с рекурсивными иерархиями? Include(), похоже, не работает с ним

У меня есть Item, Item имеет Category,

Category имеет ID, Name, Parent а также Children, Parent а также Children являются из Category тоже.

Когда я делаю запрос LINQ to Entities для конкретного Item, не возвращает связанный Category, если я не использую Include("Category") метод. Но это не приносит полную категорию, с ее родителями и детьми. я мог бы сделать Include("Category.Parent"), но этот объект похож на дерево, у меня рекурсивная иерархия, и я не знаю, где он заканчивается.

Как я могу сделать EF полностью загрузить Category, с родителями и детьми, и родителями с их родителями и детьми, и так далее?

Это не что-то для всего приложения, для соображений производительности это будет необходимо только для этой конкретной сущности, категории.

12 ответов

Решение

Вместо использования Include метод, который вы могли бы использовать Load,

Затем вы могли бы сделать для каждого и пройтись по всем детям, загружая их детей. Затем сделайте для каждого через своих детей и так далее.

Количество уровней, на которые вы пойдете, будет жестко задано в количестве для каждого цикла, который у вас есть.

Вот пример использования Load: http://msdn.microsoft.com/en-us/library/bb896249.aspx

Если вы определенно хотите, чтобы вся иерархия была загружена, то, если бы это был я, я бы попытался написать хранимую процедуру, которая должна возвращать все элементы в иерархии, возвращая тот, который вы запрашивали первым (и его потомками впоследствии).

И затем пусть исправление отношений EF гарантирует, что они все подключены.

то есть что-то вроде:

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

Если вы написали свою хранимую процедуру правильно, материализовав все элементы в иерархии (т.е. ToList()) должен сделать исправления отношений EF.

И затем у элемента, который вы хотите (First()), должны быть загружены все его дочерние элементы, и у них должны быть загружены их дочерние элементы и т. Д. Все они заполняются из этого одного вызова хранимой процедуры, поэтому проблем MARS также нет.

Надеюсь это поможет

Alex

Вы не хотите выполнять рекурсивную загрузку иерархии, если только вы не позволяете пользователю итеративно детализировать / вверх по дереву: каждый уровень рекурсии - это еще одна поездка в базу данных. Точно так же вам потребуется ленивая загрузка для предотвращения дальнейших обращений к БД, когда вы пересекаете иерархию при рендеринге на страницу или при отправке через веб-сервис.

Вместо этого переверните ваш запрос: Get Catalog, а также 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());
    }
  }
}

Использование:

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

В любом случае, присоединяйтесь к дискуссии об этом на репозитории EF.

Это может быть опасно, если вам случится загрузить все рекурсивные сущности, особенно в категории, вы можете получить 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
                    };

Таким образом, вы можете вернуть результат словаря, если требуется.

Помните, что ваши контексты должны быть максимально короткими.

Вы должны вместо этого добавить таблицу сопоставления, которая отображает каждую категорию родительский и дочерний, а не добавлять родительские и дочерние свойства в сам груз.

В зависимости от того, как часто вам нужна эта информация, она может быть запрошена по запросу. Благодаря уникальным ограничениям в БД вы можете избежать бесконечного количества возможных отношений.

Я обнаружил, что если вы включите "два родительских уровня", вы получите всю родительскую иерархию, например:

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

А теперь для совершенно другого подхода к иерархическим данным, например, для заполнения древовидной структуры.

Сначала выполните плоский запрос для всех данных, а затем создайте граф объектов в памяти:

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

Вы также можете создать табличную функцию в базе данных и добавить ее в свой 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;
    }
}

Вот умная рекурсивная функция, которую я нашел здесь, которая будет работать для этого:

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

@par Parliament дал мне идею для EF6. Пример для категории с методами для загрузки всех родителей до корневого узла и всех дочерних элементов.

ПРИМЕЧАНИЕ. Используйте это только для не критичных к производительности операций. Пример с производительностью 1000 узлов из http://nosalan.blogspot.se/2012/09/hierarchical-data-and-entity-framework-4.html.

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

Попробуй это

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;

Позвольте предложить свое простое решение, которое подходит для включения / выключения ветки иерархических данных выбранной структуры отдела организации.

Таблица 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. Вероятно, для большого набора данных вам нужно реализовать хранимую функцию на стороне сервера.

Наслаждайтесь!

Мое предложение будет

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
Другие вопросы по тегам