Как уже упоминалось, LINQ позволяет расширить любой запрос, просто добавив к нему больше критериев.
var query =
from x in xs
where x==1
select x;
if (mustAddCriteria1)
query =
from x in query
where ... // criteria 1
select x;
if (mustAddCriteria2)
query =
from x in query
where ... // criteria 2
select x;
И так далее. Этот подход работает просто отлично. Но, вероятно, вы знаете, что компиляция запросов LINQ довольно дорогая: например, Entity Framework может компилировать около 500 относительно простых запросов в секунду (см., Например, ORMBattle.NET ).
С другой стороны, многие инструменты ORM поддерживают скомпилированные запросы:
- Вы передаете экземпляр
IQueryable
некоторому методу Compile
и получаете делегата, позволяющего выполнить его намного быстрее, потому что в этом случае перекомпиляция не произойдет.
Но если мы попытаемся использовать этот подход здесь, мы сразу заметим, что наш запрос на самом деле динамический: IQueryable
, который мы выполняем каждый раз, может отличаться от предыдущего. Наличие частей запроса там определяется значениями внешних параметров.
Таким образом, мы можем выполнять такие запросы, как скомпилированные, например, без явное кеширование?
DataObjects.Net 4 поддерживает так называемую функцию «логического ветвления». Это подразумевает, что любое константное логическое выражение оценивается во время компиляции запроса, и его фактическое значение вводится в запрос SQL как истинная логическая константа (т.е. не как значение параметра или как выражение, использующее параметры).
Эта функция позволяет с легкостью генерировать различные планы запросов в зависимости от значений таких логических выражений. Например. этот код:
int all = new Random().Next(2);
var query =
from c in Query<Customer>.All
where all!=0 || c.Id=="ALFKI"
select c;
будет выполняться с использованием двух разных запросов SQL и, следовательно, двух разных планов запросов:
- План запроса на основе поиска по индексу (довольно быстро), если все == 0
- План запроса на основе сканирования индекса (довольно медленный), если все! = 0
Случай, когда все == ноль, SQL-запрос:
SELECT
[a].[CustomerId],
111 AS [TypeId] ,
[a].[CompanyName]
FROM
[dbo].[Customers] [a]
WHERE(( CAST( 0 AS bit ) <> 0 ) OR( [a].[CustomerId] = 'ALFKI' ) );
Случай, когда все == ноль, план запроса:
|--Compute Scalar(DEFINE:([Expr1002]=(111)))
|--Clustered Index Seek(OBJECT:([DO40-Tests].[dbo].[Customers].[PK_Customer] AS [a]), SEEK:([a].[CustomerId]=N'ALFKI') ORDERED FORWARD)
Второй случай (когда все! = Ноль), SQL-запрос:
SELECT
[a].[CustomerId],
111 AS [TypeId] ,
[a].[CompanyName]
FROM
[dbo].[Customers] [a]
WHERE(( CAST( 1 AS bit ) <> 0 ) OR( [a].[CustomerId] = 'ALFKI' ) );
-- Notice the ^ value is changed!
Второй случай (когда все! = Ноль), план запроса:
|--Compute Scalar(DEFINE:([Expr1002]=(111)))
|--Clustered Index Scan(OBJECT:([DO40-Tests].[dbo].[Customers].[PK_Customer] AS [a]))
-- There is index scan instead of index seek!
Обратите внимание, что почти любой другой ORM скомпилирует это в запрос с использованием целочисленного параметра:
SELECT
[a].[CustomerId],
111 AS [TypeId] ,
[a].[CompanyName]
FROM
[dbo].[Customers] [a]
WHERE(( @p <> 0 ) OR ( [a].[CustomerId] = 'ALFKI' ) );
-- ^^ parameter is used here
Поскольку SQL Server (как и большинство баз данных) генерирует одну версию плана запроса для конкретного запроса, в этом случае у него есть единственная опция - создать план с проверкой индекса:
|--Compute Scalar(DEFINE:([Expr1002]=(111)))
|--Clustered Index Scan(OBJECT:([DO40-Tests].[dbo].[Customers].[PK_Customer] AS [a]), WHERE:(CONVERT(bit,[@p],0)<>(0) OR [DO40-Tests].[dbo].[Customers].[CustomerId] as [a].[CustomerId]=N'ALFKI'))
Хорошо, это было «быстрое» объяснение полезности этой функции. Вернемся теперь к вашему делу.
Логическое ветвление позволяет реализовать его очень просто:
var categoryId = 1;
var userId = 1;
var query =
from product in Query<Product>.All
let skipCategoryCriteria = !(categoryId > 0)
let skipUserCriteria = !(userId > 0)
where skipCategoryCriteria ? true : product.Category.Id==categoryId
where skipUserCriteria ? true :
(
from order in Query<Order>.All
from detail in order.OrderDetails
where detail.Product==product
select true
).Any()
select product;
Пример отличается от вашего, но иллюстрирует идею. В основном я использовал другую модель, чтобы проверить это (мой пример основан на модели Northwind).
Этот запрос:
- Не динамический запрос, поэтому вы можете безопасно передать его в метод
Query.Execute(...)
, чтобы он выполнялся как скомпилированный запрос.
- Тем не менее каждое его выполнение приведет к тому же результату, как если бы это было сделано с «добавлением» к
IQueryable
.