正如这里已经提到的,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 查询执行,因此 - 两个不同的查询计划:
- 如果 all==0,则基于索引查找的查询计划(相当快)
- 基于索引扫描的查询计划(非常慢),如果全部!=0
all==null 时的情况,SQL 查询:
SELECT
[a].[CustomerId],
111 AS [TypeId] ,
[a].[CompanyName]
FROM
[dbo].[Customers] [a]
WHERE(( CAST( 0 AS bit ) <> 0 ) OR( [a].[CustomerId] = 'ALFKI' ) );
all==null 时的情况,查询计划:
|--Compute Scalar(DEFINE:([Expr1002]=(111)))
|--Clustered Index Seek(OBJECT:([DO40-Tests].[dbo].[Customers].[PK_Customer] AS [a]), SEEK:([a].[CustomerId]=N'ALFKI') ORDERED FORWARD)
第二种情况(当 all!=null 时),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!
第二种情况(当 all!=null 时),查询计划:
|--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;
这个例子与你的不同,但它说明了这个想法。我使用不同的模型主要是为了能够对此进行测试(我的示例是基于 om Northwind 模型)。
这个查询是:
- 不是动态查询,因此您可以安全地将其传递给
Query.Execute(...) 方法,使其作为已编译查询执行。
- 尽管如此,每次执行都会导致与“附加”到
IQueryable 相同的结果。