Pull to refresh

Linq To Entities vs. Linq To Objects на примере группировки

Reading time3 min
Views6.2K
LINQ — удобная, красивая, но при этом довольно коварная абстракция. Самые неожиданные вещи обычно происходят на стыке какой-либо реализации LINQ и LINQ To Objects. Сегодня на одном примере я рассмотрю совместную работу LINQ To Entities (Entity Framework) и LINQ To Objects.



За основу возьмем метод репозитория, который принимает на вход список идентификаторов клиентов и возвращает сгруппированный по этим идентификаторам набор заказов (таблица Orders содержит поля OrderId, OrderDate и CustomerId):
public IDictionary<long, List<Order>> GetOrdersByCustomersIds(IList<long> customersIds)
{
  using (var ctx = new RepositoryContext())
  {
    return ctx.Orders.
      Where(o => customersIds.Contains(o.Id)).
      GroupBy(o => o.CustomerId).
      ToDictionary(o => o.Key, o => o.ToList());
  }
}


Минуточку! А как это работает? Ведь при выполнении GROUP BY запроса мы можем выбрать лишь поля, по которым происходит группировка, а также агрегированные значения. Стандартным решением этой проблемы является JOIN данных таблицы и результатов группировки. Примерно так:
  SELECT o1.*, MinTotal
  FROM Orders as o1
  INNER JOIN 
    (SELECT o2.CustomerId, 
                  Min(o2.Total) as MinTotal
     FROM Orders o2
     GROUP BY o2.CustomerId) as o3 
  ON o1.CustomerId = o3.CustomerId
  Where o1.CustomerId in (1, 2, 3, 4, 5)

Что-то в этом духе и должен сгенерировать EF-провайдер. Давайте убедимся в этом. У меня под рукой был MySQL .NET Connector (официальный ADO.NET-провайдер для MySQL), поэтому я воспользовался им и получил следующий сгенерированный запрос (передав на вход список из идентификаторов от 1 до 5):
SELECT `Project2`.`C1`, 
       `Project2`.`CustomerId`, 
       `Project2`.`C2`, 
       `Project2`.`CustomerId1`, 
       `Project2`.`Id`, 
       `Project2`.`OrderDate`
FROM  
  (SELECT `Distinct1`.`CustomerId`, 
          1 AS `C1`, 
          `Extent2`.`CustomerId` AS `CustomerId1`, 
          `Extent2`.`Id`, 
          `Extent2`.`OrderDate`, 
   CASE WHEN (`Extent2`.`CustomerId` IS  NULL) THEN (NULL)  ELSE (1) END AS `C2`
   FROM 
     (SELECT DISTINCT `Extent1`.`CustomerId`
      FROM `orders` AS `Extent1`
      WHERE ((1 = `Extent1`.`Id`) OR (2 = `Extent1`.`Id`)) OR (((3 = `Extent1`.`Id`) OR (4 = `Extent1`.`Id`)) OR (5 = `Extent1`.`Id`))) AS `Distinct1` 
      LEFT OUTER JOIN `orders` AS `Extent2` 
      ON (((1 = `Extent2`.`Id`) OR (2 = `Extent2`.`Id`)) OR (((3 = `Extent2`.`Id`) OR (4 = `Extent2`.`Id`)) OR (5 = `Extent2`.`Id`))) AND (`Distinct1`.`CustomerId` = `Extent2`.`CustomerId`)) AS `Project2`
   ORDER BY `CustomerId` ASC, `C2` ASC

Немного хуже ручной реализации, но в целом прослеживается озвученная выше мысль.

Стоп! А зачем мы используем группировку на уровне базы данных? Группировка оправдана в случае использования функций агреграции (как в приведенной выше ручной реализации запроса). В нашем же случае группировка — лишь удобное представление полученных данных. Давайте слегка модифицируем метод репозитория и перенесем процесс группировки на уровень LINQ To Objects:
public IDictionary<long, List<Order>> GetOrdersByCustomersIds(IList<long> customersIds)
{
  using (var ctx = new RepositoryContext())
  {
    return ctx.Orders.
      Where(o => customersIds.Contains(o.Id)).
      AsEnumerable().
      GroupBy(o => o.CustomerId).
      ToDictionary(o => o.Key, o => o.ToList());
  }
}

Для полноты картины посмотрим, какой запрос сгенерирует EF-провайдер:
SELECT `Extent1`.`CustomerId`, 
       `Extent1`.`Id`, 
       `Extent1`.`OrderDate`
FROM `orders` AS `Extent1`
WHERE ((1 = `Extent1`.`Id`) OR (2 = `Extent1`.`Id`)) OR (((3 = `Extent1`.`Id`) OR (4 = `Extent1`.`Id`)) OR (5 = `Extent1`.`Id`))

Определенно этот запрос эффективнее предыдущего.

Вот, собственно, и все. Ничего особенного — лишь хотел заострить ваше внимание на коварности перехода от LINQ To X к LINQ To Objects после того, как сам попал в эту ловушку. Будьте бдительны!

P. S. Несмотря на то, что я использовал MySQL .NET Connector, категорически не рекомендую применять этот провайдер в продакшене: это не провайдер, а коцентрированный сгусток багов, которые не фиксятся годами.
Tags:
Hubs:
+23
Comments23

Articles