Как 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 с родительским и дочерним, а родительский с родителями и дочерними элементами и т.д.?

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

Ответ 1

Вместо использования метода Include вы можете использовать Load.

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

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

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

Ответ 2

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

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

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

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

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

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

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

Алекс

Ответ 3

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

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

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

Ответ 4

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

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

Ответ 5

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

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

У меня была эта проблема, и я привел подробный пример этого решения другому, здесь

Ответ 6

Используйте этот метод расширения, который вызывает жестко запрограммированную версию 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.

Ответ 7

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

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

Ответ 8

Вы также можете создать функцию tablevalued в базе данных и добавить ее в свой DBC-текст. Затем вы можете вызвать это из своего кода.

В этом примере вам нужно импортировать EntityFramework.Functions from 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;
    }
}

Ответ 10

попробуйте это

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;

Ответ 11

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

ПРИМЕЧАНИЕ. Используйте это только для критической работы без производительности. Пример с производительностью 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;
        }
    }   
}