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, категорически не рекомендую применять этот провайдер в продакшене: это не провайдер, а коцентрированный сгусток багов, которые не фиксятся годами.