Как сделать рекурсивную загрузку с помощью Entity Framework? - PullRequest
19 голосов
/ 15 февраля 2010

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

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

Ответы [ 6 ]

13 голосов
/ 27 июня 2013

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



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

4 голосов
/ 15 февраля 2010

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

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

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

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

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

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

Недавно я написал что-то, что 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)

0 голосов
/ 01 августа 2018

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

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

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 в соответствии с вашим вариантом использования.

0 голосов
/ 13 сентября 2017

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

...