Как Entity Framework работает с рекурсивными иерархиями? Включить (), кажется, не работает с ним - PullRequest
67 голосов
/ 20 августа 2009

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

Category имеет ID, Name, Parent и Children. Parent и Children тоже относятся к Category.

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

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

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

Ответы [ 12 ]

21 голосов
/ 30 августа 2009

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

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

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

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

14 голосов
/ 14 декабря 2009

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

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

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

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

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

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

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

Alex

6 голосов
/ 31 августа 2009

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

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

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

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

3 голосов
/ 15 октября 2015

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

3 голосов
/ 27 августа 2009

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

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

1 голос
/ 11 января 2017

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

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

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;
            }
    }
}
0 голосов
/ 20 мая 2018

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

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

  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);
        }
    }
0 голосов
/ 26 апреля 2018

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

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