Получить детей, общих для всех родителей
У меня есть следующие объекты, использующие Entity Framework Core:
public class Parent {
public Int32 ParentId { get; set; }
public virtual Collection<ParentChildren> ParentChildrens { get; set; }
}
public class ParentChildren {
public Int32 ParentId { get; set; }
public Int32 ChildrenId { get; set; }
public virtual Parent Parent { get; set; }
public virtual Children Children { get; set; }
}
public class Children {
public Int32 ChildrenId { get; set; }
public virtual Collection<ParentChildren> ParentChildrens { get; set; }
public virtual Collection<ChildrenLocalization> ChildrenLocalizations { get; set; }
}
public class ChildrenLocalization {
public Int32 ChildrenId { get; set; }
public String Language { get; set; }
public String Name { get; set; }
public virtual Children Children { get; set; }
}
Учитывая IQueryable<Parent>
Мне нужно, используя Linq to Entities лямбда-выражения:
- Сделайте детей общими для всех родителей;
- Для каждого
Children
получить свое имя отChildrenLocalization
сLanguage="en"
,
Поэтому я попробовал следующее:
var result = context.Parents
.SelectMany(y => y.ParentChildrens)
.GroupBy(y => y.ParentId)
.Where(y =>
context.Parents
.SelectMany(y => y.ParentChildrens)
.Select(z => z.ChildrenId)
.Distinct()
.All(z => y.Any(w => w.ChildrenId == z)))
.SelectMany(y => y)
.Select(y => new {
Id = y.ChildrenId,
Name = y.Children.ChildrenLocalizations.Where(z => z.Language == "en").Select(z => z.Name).FirstOrDefault()
})
.GroupBy(x => x.Id)
.Select(x => x.FirstOrDefault())
.ToList();
Этот запрос дает ожидаемый результат, но он кажется слишком сложным.
Я не смог улучшить его, и, например, мне нужно было добавить последний GroupBy, чтобы он работал.
Как я могу сделать мой запрос проще?
5 ответов
Поскольку у вас есть отношение многие ко многим, лучше основывать (запускать) запрос на получающемся объекте (Children
), что позволяет избежать необходимости GroupBy
/Distinct
если начать с другого конца (Parent
).
Так дано
IQueryable<Parent> parents
и при условии, что у вас есть доступ к контексту, запрос может быть записан следующим образом:
var query = context.Set<Children>()
.Where(c => parents.All(p => p.ParentChildrens.Select(pc => pc.ChildrenId).Contains(c.ChildrenId)))
.Select(c => new
{
Id = c.ChildrenId,
Name = c.ChildrenLocalizations.Where(cl => cl.Language == "en").Select(cl => cl.Name).FirstOrDefault()
});
что приятно переводит на один SQL.
Вы начинаете с уникального Children
, Для требования (2) вы просто используете свойство навигации. Требование (1) является более сложным (все всегда труднее выполнить, чем любое), но я думаю, что критерии
parents.All(p => p.ParentChildrens.Select(pc => pc.ChildrenId).Contains(c.ChildrenId))
довольно интуитивно представляет ребенка, общего для всех родителей.
Если я правильно понял, это может сработать. Это будет один запрос.
var result =
(from parent in context.Parents
from pToC in parent.ParentChildrens
where pToC.Children.ParentChildrens.Select(pc => pc.ParentId).Distinct().Count() == context.Parents.Count()
from childLocation in pToC.Children.ChildrenLocalizations
where childLocation.Language == "en"
select new { pToC.Children.ChildrenId, childLocation.Name }).Distinct();
Дано IQueryable<Parent> parents
parents
.SelectMany(p => p.ParentChildrens)
.Select(pc => pc.Children)
.Where(c => c.ParentChildrens
.Select(pc => pc.ParentId)
.OrderBy(i => i)
.SequenceEqual(parents.Select(p => p.ParentId).OrderBy(i => i)))
.Select(c => new
{
Id = c.ChildrenId,
c.ChildrenLocalizations.FirstOrDefault(cl => cl.Language == "en").Name
})
Предположим, у вас есть 3 родителя с Id 10, 11, 12 Предположим, у вас есть 3 ребенка с Id 20, 21, 22
Таблица ParentChildrens:
ChildId | ParentId
20 10
20 11
20 12
21 10
21 11
22 10
22 12
Итак, у Ребенка 20 есть Родители 10/11/12; Ребенок 21 имеет Родителя 10/11; У ребенка 22 есть родители 10/12.
"Сделайте детей общими для всех родителей"; Если это означает: получить детей, у которых есть каждый доступный родитель в своей коллекции родителей, то легко увидеть, что вы хотите только ребенка 20, и вы хотите этого ребенка только один раз
Поскольку все отношения между родителями и детьми уникальны, мы знаем, что если есть X родителей, мы хотим, чтобы у детей было ровно X родителей.
Вам не нужны все свойства этих Children, вы хотите только "получить его имя от ChildrenLocalization with Language =" en", всегда ли есть ноль или одно такое имя? Если есть еще какие из них мы должны взять? Любое имя или все имена?
Поскольку нам нужно ограничить себя всеми детьми, у которых ParentCount равен числу родителей, нам также необходимо рассчитать количество родителей на одного ребенка.
var childrenWithParentCount = dbContext.Children.Select(child => new
{
// "get its name from ChildrenLocalization with Language="en"
LocalizationName = child.ChildrenLocalizations
.Where(localization => localization.Language == "en")
.Select(localization => localizaition.Name)
.FirstOrDefault();
// or if you want all names:
LocalizationNames = child.ChildrenLocalizations
.Where(localization => localization.Language == "en")
.Select(localization => localizaition.Name)
.ToList;
ParentCount = child.ParentChildren
.Select(parentChild => parentChild.ParentId)
.Count();
});
Теперь мы не хотим всех этих детей, мы хотим только тех детей, у которых ParentCount равен числу родителей
var childrenWithAllParents = childrenWithParentCount
.Where(child => !child.ParentCount == dbContext.Parents.Count());
Вы заметили, что я только создал объекты IQueryable, я еще не выполнил ни одного из запросов. Чтобы выполнить запрос:
var result = childrenWithAllParents.ToList();
Некоторые люди любят удивлять других одним большим заявлением LINQ; ну вот оно:
var result = dbContext.Children.Select(child => new
{
LocalizationName = child.ChildrenLocalizations
.Where(localization => localization.Language == "en")
.Select(localization => localizaition.Name)
.FirstOrDefault();
ParentCount = child.ParentChildren
.Select(parentChild => parentChild.ParentId)
.Count();
})
.Where(child => !child.ParentCount == dbContext.Parents.Count())
.ToList();
К счастью, ваша система управления базами данных достаточно умна, чтобы запоминать количество родителей, вместо того, чтобы рассчитывать ее снова один раз на ребенка.
Вам нужно отделить свои звонки от группировки.
List<Parent> result = context.Parents
.Include(i => i.ParentChildrens)
.ThenInclude(i => i.Children)
.ThenInclude(i => i.ChildrenLocalizations)
.ToList();
var finalResult = result.SelectMany(c => c.ParentChildrens, (o, j) =>
{
return new
{
Id = j.ChildrenId,
Parent = o.ParentId,
Name = j.Children.ChildrenLocalizations.First(c => c.Language == "en").Name
};
});