Entity Framework: запрос дочерних объектов - PullRequest
44 голосов
/ 13 октября 2011

В настоящее время я изучаю Entity Framework, и у меня возникают проблемы !!

Может кто-нибудь уточнить, правильно ли я считаю, что не могу получить родителя и подмножество его детей из БД?

Например ...

db.Parents
.Include(p => p.Children)
.Where(p => p.Children.Any(c => c.Age >= 5))

Это вернет всех родителей, у которых есть ребенок в возрасте от 5 лет, но если я переберу коллекцию Parents.Children, все дети будут присутствовать (не только те, кто старше 5 лет).

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

Вопросы:

  1. Правильно ли то, что я сказал?
  2. Возможно ли получить родителей и просто подмножество из БД, не совершая загрузок в БД?
  3. Я далеко от цели? (Не в первый раз) !!!!

Я нашел несколько блогов и ТАКИХ постов, которые касаются этой темы, но ничего такого, что достаточно хорошо объясняет мой маленький мозг.

EDIT

Прочитав этот блог (спасибо Daz Lewis) ....... Я до сих пор не понимаю !!!

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

Как я могу получить IEnumerable, в котором у каждого из родителей есть отфильтрованная коллекция Children (Age> = 5)?

Дополнительные уточнения:

В ответ на комментарий ДонАндре, я после: а) Список родителей, у которых есть ребенок старше 5 лет (включая только этих детей).

Любая помощь приветствуется,

Спасибо.

Ответы [ 3 ]

46 голосов
/ 14 октября 2011

Единственный способ получить коллекцию родителей с отфильтрованной коллекцией детей в одной базе данных - это проекция.Невозможно использовать энергичную загрузку (Include), поскольку она не поддерживает фильтрацию, Include всегда загружает всю коллекцию.Способ явной загрузки, показанный @Daz, требует одну поездку туда и обратно на родительский объект.

Пример:

var result = db.Parents
    .Select(p => new
    {
        Parent = p,
        Children = p.Children.Where(c => c.Age >= 5)
    })
    .ToList();

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

Контекст EF также автоматически заполнит коллекцию Children Parentесли вы не отключите отслеживание изменений (например, с помощью AsNoTracking()).В этом случае вы можете затем спроецировать родителя из анонимного типа результата (происходит в памяти, без запроса к БД):

var parents = result.Select(a => a.Parent).ToList();

parents[i].Children будет содержать отфильтрованные дочерние элементы для каждого Parent.

Редактировать до последнего Редактировать в вопросе:

Я после a) Список родителей, у которых есть ребенок старше 5 лет (включая только этих детей).

Приведенный выше код вернет всех родителей и будет включать только детей с Age> = 5, поэтому потенциально также родители с пустой коллекцией детей, если есть только детис Age <5. Вы можете отфильтровать их, используя дополнительное условие <code>Where для родителей, чтобы получить только родителей, у которых хотя бы один (Any) ребенок с Age> =5:

var result = db.Parents
    .Where(p => p.Children.Any(c => c.Age >= 5))
    .Select(p => new
    {
        Parent = p,
        Children = p.Children.Where(c => c.Age >= 5)
    })
    .ToList();
3 голосов
/ 13 октября 2011

Если взять ваш пример, следующее должно делать то, что вам нужно. Посмотрите здесь для получения дополнительной информации.

db.Entry(Parents)
.Collection("Children")
.Query().Cast<Child>()
.Where(c => c.Age >= 5))
.Load();
2 голосов
/ 14 октября 2011

Я думаю, что родители и ребенок не очень хорошо подходят как отдельные сущности.Ребенок всегда может быть также родителем, и обычно у него есть два родителя (отец и мать), так что это не самый простой контекст.Но я предполагаю, что у вас просто простое отношение 1: n, как в следующей модели «ведущий-ведомый», которую я использовал.

Вам нужно сделать левое внешнее соединение (этот ответпривело меня на правильный путь).Такое объединение немного сложнее, но вот код

var query = from m in ctx.Masters
            join s in ctx.Slaves
              on m.MasterId equals s.MasterId into masterSlaves
            from ms in masterSlaves.Where(x => x.Age > 5).DefaultIfEmpty()
            select new {
              Master = m,
              Slave = ms
            };

foreach (var item in query) {
  if (item.Slave == null) Console.WriteLine("{0} owns nobody.", item.Master.Name);
  else Console.WriteLine("{0} owns {1} at age {2}.", item.Master.Name, item.Slave.Name, item.Slave.Age);
}

Это будет переводиться в следующий оператор SQL с EF 4.1

SELECT 
[Extent1].[MasterId] AS [MasterId], 
[Extent1].[Name] AS [Name], 
[Extent2].[SlaveId] AS [SlaveId], 
[Extent2].[MasterId] AS [MasterId1], 
[Extent2].[Name] AS [Name1], 
[Extent2].[Age] AS [Age]
FROM  [dbo].[Master] AS [Extent1]
LEFT OUTER JOIN [dbo].[Slave] AS [Extent2]
ON ([Extent1].[MasterId] = [Extent2].[MasterId]) AND ([Extent2].[Age] > 5)

Обратите внимание, что важно выполнитьдополнительное условие where на возраст объединенной коллекции, а не между from и select.

EDIT:

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

var hierarchical = from line in query
                   group line by line.Master into grouped
                   select new { Master = grouped.Key, Slaves = grouped.Select(x => x.Slave).Where(x => x != null) };

foreach (var elem in hierarchical) {
   Master master = elem.Master;
   Console.WriteLine("{0}:", master.Name);
   foreach (var s in elem.Slaves) // note that it says elem.Slaves not master.Slaves here!
     Console.WriteLine("{0} at {1}", s.Name, s.Age);
}

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

class FilteredResult {
  public Master Master { get; set; }
  public IEnumerable<Slave> Slaves { get; set; }
}

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

...