Entity Framework: запрос дочерних объектов
Я изучаю Entity Framework в mo, и у меня проблемы!
Может ли кто-нибудь уточнить, правильно ли я полагаю, что я не могу получить родителя и его подмножество из db?
Например...
db.Parents
.Include(p => p.Children)
.Where(p => p.Children.Any(c => c.Age >= 5))
Это приведет к возвращению всех родителей, у которых есть ребенок в возрасте 5+, но если я буду проходить через коллекцию Parents.Children, все дети будут присутствовать (а не только те, кто старше 5 лет).
Теперь запрос имеет смысл для меня (я попросил включить детей, и у меня их есть!), но могу предположить, что я хотел бы, чтобы в некоторых сценариях было применено предложение where к дочерней коллекции.
Вопросы:
- Это то, что я сказал правильно?
- Можно ли получить родителей и просто подмножество из db без загрузки вызовов в db?
- Неужели я не знаком? (Не было бы в первый раз)!!!!
Я нашел несколько блогов и сообщений SO, которые касаются темы, но ничего, что объясняет это достаточно хорошо для моего маленького мозга.
ИЗМЕНИТЬ
Прочитав это blog (спасибо Daz Lewis)....... Я все еще не понимаю!!!
В примере, приведенном в блоге, я вижу, как я могу добиться его от одного экземпляра родителя, но я изо всех сил пытаюсь понять, как я могу сделать это с помощью коллекции.
Как я могу получить IEnumerable, в котором каждый из родителей имеет фильтрованную коллекцию Children (Age >= 5)?
Дальнейшие разъяснения:
В ответ на комментарий DonAndre, я за a) Список родителей, у которых есть ребенок старше 5 (и включают только этих детей).
Любая помощь оценивается,
Спасибо.
Ответы
Ответ 1
Единственный способ получить коллекцию родителей с отфильтрованной дочерней коллекцией в одной броузере базы данных - это использовать проекцию. Нельзя использовать загружаемую загрузку (Include
), поскольку она не поддерживает фильтрацию, Include
всегда загружает всю коллекцию. Способ загрузки explicite, показанный @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. Вы можете отфильтровать их с помощью дополнительного предложения 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();
Ответ 2
Взяв ваш пример, вы должны сделать то, что вам нужно. Взгляните здесь для получения дополнительной информации.
db.Entry(Parents)
.Collection("Children")
.Query().Cast<Child>()
.Where(c => c.Age >= 5))
.Load();
Ответ 3
Я думаю, что родители и ребенок не очень хорошо подходят как отдельные сущности. Ребенок всегда может быть родителем, и обычно у ребенка есть два родителя (отец и мать), поэтому это не самый простой контекст. Но я предполагаю, что у вас просто простая связь 1: n, как в следующей модели master-slave, которую я использовал.
Что вам нужно сделать, это сделать левое внешнее соединение (этот ответ привел меня к правильному пути). Такое соединение немного сложно сделать, но вот код
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; }
}
а затем проецируйте группу в экземпляры этого класса. Это облегчает вам передачу этих результатов другим методам.