Как 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, с родителем и детьми, и родитель со своим родителем и детьми, и так далее?



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

1077   12  

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 отношения fixup пинает.

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

надеюсь, что это помогает

Алекс

это может быть опасно, если вы случайно загрузите все рекурсивные сущности, особенно в категории, вы можете получить гораздо больше, чем вы ожидали:

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

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

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

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

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

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

вместо этого переверните свой запрос: 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 РЕПО.

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

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

вы также можете создать табличную функцию в базе данных и добавить ее в свой DBContext. Тогда вы можете вызвать это из своего кода.

в этом примере требуется импортировать EntityFramework.Функции из 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;
    }
}

попробуй такое

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;

@парламент дал мне идею для 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;
        }
    }   
}

мое предложение было бы

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

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

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

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

Comments

    Ничего не найдено.