За основу возьмем метод репозитория, который принимает на вход список идентификаторов клиентов и возвращает сгруппированный по этим идентификаторам набор заказов (таблица 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, категорически не рекомендую применять этот провайдер в продакшене: это не провайдер, а коцентрированный сгусток багов, которые не фиксятся годами.
PP. S. Кросспост на Хабре: http://habrahabr.ru/blogs/net/119624/
4 комментария:
Меня до сих пор интересует вопрос:
оправдано ли создание контекста каждый раз создании запроса?
Вот в частности за это мне и нравится NHibernate. За то, что он даёт удобный способ написания старых-добрых SQL-запросов, а не предлагает некую "магию". Таким образом, когда пишешь сложный запрос к БД, задумываешься о том, как он будет выглядеть на SQL.
А в случае простых запросов - ну он такой же, как ЕФ :)
На NH пример выше выглядел бы как:
session.QueryOver()
.WhereRestrictionOn(x => x.Customer.Id).IsIn( customersIds)
.List()
.GroupBy(x => x.Customer.Id)
.ToDictionary(x => x.Key, x => x.ToList());
@yuriy: в NHibernate точно не оправдано, за ЕФ пока не скажу :)
как при этом будет выглядеть SQL-запрос, думаю, очевидно.
SELECT * FROM orders WHERE order.customerId IN (1, 2, 3)
что, имхо, еще и чуть-чуть-чуть оптимальнее, чем через OR :)
yuriy, не могу дать однозначного ответа. Быть может, в ближайшем будущем сформулирую свои мысли на этот счет в виде поста.
Shaddix, интересное разделение синтаксиса. Но в общем случае, думаю, я бы предпочел подход EF: LINQ прекрасен своим единообразием - не хочу изучать отдельный синтаксис LINQ под каждый ORM.
Во что сконвертить Contains - задача провайдера. Так что использование OR vs. In не является преимуществом той или иной ORM.
Отправить комментарий