Как сделать рекурсивную загрузку с помощью инфраструктуры Entity?

У меня есть древовидная структура в БД с таблицей TreeNodes. таблица имеет nodeId, parentId и parameterId. в EF, структура похожа на TreeNode.Children, где каждый ребенок является TreeNode... У меня также есть таблица деревьев с указанием id, name и rootNodeId.

В конце дня я хотел бы загрузить дерево в TreeView, но я не могу понять, как загрузить его все сразу. Я пробовал:

var trees = from t in context.TreeSet.Include("Root").Include("Root.Children").Include("Root.Children.Parameter")
        .Include("Root.Children.Children")
                        where t.ID == id
                        select t;

Это даст мне первые 2 поколения, но не больше. Как загрузить все дерево со всеми поколениями и дополнительные данные?

Ответ 1

У меня была эта проблема недавно и наткнулся на этот вопрос после того, как я понял простой способ добиться результатов. Я предоставил отредактировать ответ Крейга, предоставив 4-й метод, но полномочия, которые будут определены, должны быть другим ответом. Это хорошо со мной:)

Мой оригинальный вопрос/ответ можно найти здесь.

Это работает, пока ваши элементы в таблице все знают, к какому дереву они принадлежат (что в вашем случае выглядит так: t.ID). Тем не менее, неясно, какие объекты вы действительно имеете в игре, но даже если у вас их несколько, у вас должен быть FK в сущности Children, если это не TreeSet

В принципе, просто не используйте Include():

var query = from t in context.TreeSet
            where t.ID == id
            select t;

// if TreeSet.Children is a different entity:
var query = from c in context.TreeSetChildren
            // guessing the FK property TreeSetID
            where c.TreeSetID == id
            select c;

Это вернет ВСЕ элементы для дерева и поместит их все в корень коллекции. На этом этапе ваш результирующий набор будет выглядеть следующим образом:

-- Item1
   -- Item2
      -- Item3
-- Item4
   -- Item5
-- Item2
-- Item3
-- Item5

Поскольку вы, вероятно, хотите, чтобы ваши объекты выходили из EF только иерархически, это не то, что вы хотите, не так ли?

.. затем исключить потомки, присутствующие на корневом уровне:

К счастью, поскольку у вас есть свойства навигации в вашей модели, коллекции дочерних сущностей будут по-прежнему заполняться, как вы можете видеть на иллюстрации приведенного выше результата. Путем ручного повторения набора результатов с помощью цикла foreach() и добавления этих корневых элементов в new List<TreeSet>() теперь у вас будет список с корневыми элементами и все потомки должным образом вложены.

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

var subset = query
     // execute the query against the DB
     .ToList()
     // filter out non-root-items
     .Where(x => !x.ParentId.HasValue);

И теперь subset будет выглядеть так:

-- Item1
   -- Item2
      -- Item3
-- Item4
   -- Item5



О решениях Craig:

  1. Вы действительно не хотите использовать ленивую загрузку для этого! Дизайн, основанный на необходимости опроса n + 1, станет основным производительным партнером. ********* (Ну, если честно, если вы позволите пользователю выборочно просверлить вниз по дереву, тогда это может быть уместно. Просто не используйте ленивую загрузку, чтобы получить их все впереди!)

  2. Я никогда не пробовал вложенные вещи, и я бы не предложил взломать EF-конфигурацию, чтобы выполнить эту работу, учитывая, что решение намного проще.
  3. Еще одно разумное предложение - это создание представления базы данных, которое обеспечивает самосвязность, а затем сопоставляет это представление с промежуточная таблица join/link/m2m. Лично я нашел это решение более сложным, чем необходимо, но оно, вероятно, имеет свои применения.

Ответ 2

Когда вы используете Include(), вы просите Entity Framework перевести ваш запрос в SQL. Итак, подумайте: как бы вы напили инструкцию SQL, которая возвращает дерево произвольной глубины?

Ответ. Если вы не используете определенные функции иерархии вашего сервера базы данных (которые не являются стандартом SQL, но поддерживаются некоторыми серверами, такими как SQL Server 2008, хотя и не его провайдером Entity Framework), вы бы этого не сделали. Обычный способ обработки деревьев произвольной глубины в SQL - это использовать модель вложенных множеств, а не модель родительского идентификатора.

Поэтому для решения этой проблемы можно использовать три способа:

  • Используйте модель вложенных наборов. Это требует изменения ваших метаданных.
  • Используйте функции иерархии SQL Server и взломайте Entity Framework для их понимания (сложно, но этот метод может работать). Опять же, вам нужно будет изменить свои метаданные .i
  • Используйте явную загрузку или EF 4 ленивую загрузку вместо активной загрузки. Это приведет к появлению многих запросов к базе данных вместо одного.

Ответ 3

Я хотел опубликовать свой ответ, поскольку другие не помогли мне.

Моя база данных немного отличается, в основном моя таблица имеет идентификатор и ParentID. Таблица рекурсивна. Следующий код получает всех детей и вставляет их в окончательный список.

public IEnumerable<Models.MCMessageCenterThread> GetAllMessageCenterThreads(int msgCtrId)
{
    var z = Db.MCMessageThreads.Where(t => t.ID == msgCtrId)
        .Select(t => new MCMessageCenterThread
        {
            Id = t.ID,
            ParentId = t.ParentID ?? 0,
            Title = t.Title,
            Body = t.Body
        }).ToList();

    foreach (var t in z)
    {
        t.Children = GetChildrenByParentId(t.Id);
    }

    return z;
}

private IEnumerable<MCMessageCenterThread> GetChildrenByParentId(int parentId)
{
    var children = new List<MCMessageCenterThread>();

    var threads = Db.MCMessageThreads.Where(x => x.ParentID == parentId);

    foreach (var t in threads)
    {
        var thread = new MCMessageCenterThread
        {
            Id = t.ID,
            ParentId = t.ParentID ?? 0,
            Title = t.Title,
            Body = t.Body,
            Children = GetChildrenByParentId(t.ID)
        };

        children.Add(thread);
    }

    return children;
}

Для полноты, здесь моя модель:

public class MCMessageCenterThread
{
    public int Id { get; set; }
    public int ParentId { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }

    public IEnumerable<MCMessageCenterThread> Children { get; set; }
}

Ответ 4

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

private static void LoadComments(<yourObject> q, Context yourContext)
{
    if(null == q | null == yourContext)
    {
        return;
    }
    yourContext.Entry(q).Reference(x=> x.Comment).Load();
    Comment curComment = q.Comment;
    while(null != curComment)
    {
        curComment = LoadChildComment(curComment, yourContext);
    }
}

private static Comment LoadChildComment(Comment c, Context yourContext)
{
    if(null == c | null == yourContext)
    {
        return null;
    }
    yourContext.Entry(c).Reference(x=>x.ChildComment).Load();
    return c.ChildComment;
}

Теперь, если у вас есть что-то, что имеет собственные коллекции, вам нужно будет использовать Collection вместо Reference и делать то же самое с погружением вниз. По крайней мере, тот подход, который я использовал в этом сценарии, поскольку мы имели дело с Entity и SQLite.

Ответ 5

Я недавно что-то написал, что N + 1 выбирает загрузку всего дерева, где N - количество уровней вашего самого глубокого пути в исходном объекте.

Это то, что я сделал, учитывая следующий класс саморегуляции

public class SomeEntity 
{
  public int Id { get; set; }
  public int? ParentId { get; set; }
  public string Name { get; set;
}

Я написал следующий помощник DbSet

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;

namespace Microsoft.EntityFrameworkCore
{
    public static class DbSetExtensions
    {
        public static async Task<TEntity[]> FindRecursiveAsync<TEntity, TKey>(
            this DbSet<TEntity> source,
            Expression<Func<TEntity, bool>> rootSelector,
            Func<TEntity, TKey> getEntityKey,
            Func<TEntity, TKey> getChildKeyToParent)
            where TEntity: class
        {
            // Keeps a track of already processed, so as not to invoke
            // an infinte recursion
            var alreadyProcessed = new HashSet<TKey>();

            TEntity[] result = await source.Where(rootSelector).ToArrayAsync();

            TEntity[] currentRoots = result;
            while (currentRoots.Length > 0)
            {
                TKey[] currentParentKeys = currentRoots.Select(getEntityKey).Except(alreadyProcessed).ToArray();
                alreadyProcessed.AddRange(currentParentKeys);

                Expression<Func<TEntity, bool>> childPredicate = x => currentParentKeys.Contains(getChildKeyToParent(x));
                currentRoots = await source.Where(childPredicate).ToArrayAsync();
            }

            return result;
        }
    }
}

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

  • Критерии выбора для ваших корневых объектов
  • Как получить свойство для первичного ключа объекта (SomeEntity.Id)
  • Как получить дочернее свойство, которое ссылается на его родительский элемент (SomeEntity.ParentId)

Например

SomeEntity[] myEntities = await DataContext.SomeEntity.FindRecursiveAsync(
  rootSelector: x => x.Id = 42,
  getEntityKey: x => x.Id,
  getChildKeyToParent: x => x.ParentId).ToArrayAsync();
);

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

DataContext.SomeEntity.Where(x => x.Id == rootId || x.RootId == rootId)