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

У меня есть древовидная структура в БД с таблицей TreeNodes. таблица имеет nodeId, parentId и parameterId. в EF структура похожа на TreeNode.Children, где каждый дочерний элемент является TreeNode... У меня также есть таблица Tree, содержащая 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 поколения, но не больше. Как загрузить все дерево со всеми поколениями и дополнительными данными?

6 ответов

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



О решениях Крейга:

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

  3. Я никогда не пробовал вещи с вложенными наборами, и я бы не советовал взламывать конфигурацию EF, чтобы сделать эту работу, поскольку есть гораздо более простое решение.

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

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

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

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

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

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

Моя база данных немного отличается, в основном у моей таблицы есть ID и 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; }
}

Недавно я написал что-то, что выбирает 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;
        }
    }
}

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

  1. Критерии выбора для ваших корневых объектов
  2. Как получить свойство для первичного ключа объекта (SomeEntity.Id)
  3. Как получить свойство ребенка, которое ссылается на его родителя (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)

Для примера загрузки в дочерние объекты я приведу пример объекта 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.

Это старый вопрос, но другие ответы либо имели n+1 совпадений с базой данных, либо их модели способствовали подходам снизу вверх (магистраль к листьям). В этом случае список тегов загружается в виде дерева, и у тега может быть несколько родителей. Подход, который я использую, имеет только два попадания в базу данных: сначала для получения тегов для выбранных статей, а затем для другого, который активно загружает таблицу соединений. Таким образом, это использует нисходящий подход (листья к стволу); если ваша таблица соединения велика или если результат не может быть действительно кэширован для повторного использования, то полная загрузка всего этого начинает показывать компромиссы с этим подходом.

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

var roots = new HashSet<AncestralTagDto>(); //no parents
var allTags = new HashSet<AncestralTagDto>();

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

var startingTags = await _dataContext.ArticlesTags
        .Include(p => p.Tag.Parents)
        .Where(t => t.Article.CategoryId == categoryId)
        .GroupBy(t => t.Tag)
        .ToListAsync()
        .ContinueWith(resultTask => 
             resultTask.Result.Select(
                  grouping => new AncestralTagDto(
                        grouping.Key.Id, 
                        grouping.Key.Name)));

Теперь давайте возьмем таблицу самосоединения тегов и загрузим все это в память:

var tagRelations = await _dataContext.TagsTags.Include(p => p.ParentTag).ToListAsync();

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

foreach (var tag in startingTags)
{
    allTags.Add(tag);
    GetParents(tag);
}
return roots;

И наконец, вот вложенный рекурсивный метод, который строит дерево:

void GetParents(AncestralTagDto tag)
{
    var parents = tagRelations.Where(c => c.ChildTagId == tag.Id).Select(p => p.ParentTag);
    if (parents.Any()) //then it's not a root tag; keep climbing down
    {
        foreach (var parent in parents)
        {
            //have we already seen this parent tag before? If not, instantiate the dto.
            var parentDto = allTags.SingleOrDefault(i => i.Id == parent.Id);
            if (parentDto is null)
            {
                parentDto = new AncestralTagDto(parent.Id, parent.Name);
                allTags.Add(parentDto);
            }

            parentDto.Children.Add(tag);
            GetParents(parentDto);
        }
    }
    else //the tag is a root tag, and should be in the root collection. If it's not in there, add it.
    {
        //this block could be simplified to just roots.Add(tag), but it's left this way for other logic.
        var existingRoot = roots.SingleOrDefault(i => i.Equals(tag));
        if (existingRoot is null)
            roots.Add(tag);
    }
}

Под одеялом я полагаюсь на свойства HashSet для предотвращения дубликатов. Для этого важно, чтобы промежуточный объект, который вы используете (я использовал здесь AncestralTagDto, и его коллекция Children также HashSet), переопределите методы Equals и GetHashCode в соответствии с вашим вариантом использования.

Другие вопросы по тегам