LINQ — удобная, красивая, но при этом довольно коварная абстракция. Самые неожиданные вещи обычно происходят на стыке какой-либо реализации LINQ и LINQ To Objects. Сегодня на одном примере я рассмотрю совместную работу LINQ To Entities (Entity Framework) и LINQ To Objects.
За основу возьмем метод репозитория, который принимает на вход список идентификаторов клиентов и возвращает сгруппированный по этим идентификаторам набор заказов (таблица Orders содержит поля OrderId, OrderDate и CustomerId):
Минуточку! А как это работает? Ведь при выполнении GROUP BY запроса мы можем выбрать лишь поля, по которым происходит группировка, а также агрегированные значения. Стандартным решением этой проблемы является JOIN данных таблицы и результатов группировки. Примерно так:
Что-то в этом духе и должен сгенерировать EF-провайдер. Давайте убедимся в этом. У меня под рукой был MySQL .NET Connector (официальный ADO.NET-провайдер для MySQL), поэтому я воспользовался им и получил следующий сгенерированный запрос (передав на вход список из идентификаторов от 1 до 5):
Немного хуже ручной реализации, но в целом прослеживается озвученная выше мысль.
Стоп! А зачем мы используем группировку на уровне базы данных? Группировка оправдана в случае использования функций агреграции (как в приведенной выше ручной реализации запроса). В нашем же случае группировка — лишь удобное представление полученных данных. Давайте слегка модифицируем метод репозитория и перенесем процесс группировки на уровень LINQ To Objects:
Для полноты картины посмотрим, какой запрос сгенерирует EF-провайдер:
Определенно этот запрос эффективнее предыдущего.
Вот, собственно, и все. Ничего особенного — лишь хотел заострить ваше внимание на коварности перехода от LINQ To X к LINQ To Objects после того, как сам попал в эту ловушку. Будьте бдительны!
P. S. Несмотря на то, что я использовал MySQL .NET Connector, категорически не рекомендую применять этот провайдер в продакшене: это не провайдер, а коцентрированный сгусток багов, которые не фиксятся годами.
За основу возьмем метод репозитория, который принимает на вход список идентификаторов клиентов и возвращает сгруппированный по этим идентификаторам набор заказов (таблица 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, категорически не рекомендую применять этот провайдер в продакшене: это не провайдер, а коцентрированный сгусток багов, которые не фиксятся годами.
