Получить детей, общих для всех родителей - PullRequest
1 голос
/ 03 апреля 2019

У меня есть следующие сущности, использующие ядро ​​Entity Framework:

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:

  1. Получите детей, общих для всех родителей ;
  2. Для каждого 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 ]

3 голосов
/ 03 апреля 2019

Поскольку у вас есть отношение «многие ко многим», лучше основывать (запускать) запрос на результирующей сущности (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))

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

1 голос
/ 03 апреля 2019

Предположим, у вас есть 3 родителя с идентификатором 10, 11, 12. Предположим, у вас есть 3 ребенка с идентификатором 20, 21, 22 * ​​1001 *

Таблица 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 родителей.

Вам не нужны все свойства этих детей, вы хотите только" получить свое имя от 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();

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

1 голос
/ 03 апреля 2019

Если я правильно понял, это может сработать. Это будет один запрос.

    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();
1 голос
/ 03 апреля 2019

Дано 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
})
0 голосов
/ 03 апреля 2019

Вам нужно отделить свои звонки от группировки.

  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
                };
            });
...