发生这种情况并不是因为 AND 的优先级高于 OR。现实中会发生什么:
var firstFilter = ...; // itemNumber
var secondFilter = ...; // statusA
var firstAndSecondFilter = firstFilter.And(secondFilter); // itemNumber && statusA
var thirdFilter = ...; // statusB
var endFilter = firstAndSecondFilter.Or(thirdFilter) // (itemNumber && statusA) || statusB.
问题 - 错误的控制流程。你必须这样做:
var filterByA = ...;
var filterByB = ...;
var filterByAorB = filterByA.Or(filterByB);
var filterByNumber = ...;
var endFiler = filterByNumber.And(filterByAorB);
而且您的代码很糟糕,不仅因为它工作错误,还因为很难以这种风格编写代码。原因:
- 此代码不遵循DRY 原则。您有两个相同的 lambdas 检查
StatusA
(查看您的三元运算符)和两个相同的 lambdas 检查StatusB
您的三元运算符太长,带有空检查。这很糟糕,因为您看不到一般情况,您的眼睛专注于语法问题。您可以为函数编写和扩展方法AndNullable。像这样:
static Func<T1, TOut> AndNullable<T1, TOut>(this Func<T1, TOut> firstFunc, Func<T1, TOut> secondFunc) {
if (firstFunc != null) {
if (secondFunc != null)
return firstFunc.And(secondFunc);
else
return firstFunc;
}
else {
if (secondFunc != null)
return secondFunc;
else
return null;
}
}
Or 也一样。现在你的代码可以这样写:
Func<Statement, bool> filter = null;
if (request.StatusA)
filter = s => s.StatementStatus == StatementStatusType.StatusA;
if (request.StatusB)
filter = filter.OrNullable(s => s.StatementStatus == StatementStatusType.StatusB);
if (!string.IsNullOrEmpty(request.ItemNumber))
filter = filter.AndNullable(s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber));
越读越好。
您的过滤器是全局过滤器。全局过滤器的编写在过滤条件少且行数较少的情况下更简单,但理解您的过滤器更复杂。以这种方式重写它:
Func<Statement, bool> filterByStatusA = null;
Func<Statement, bool> filterByStatusB = null;
if (request.StatusA)
filterByStatusA = s => s.StatementStatus == StatementStatusType.StatusA;
if (request.StatusB)
filterByStatusB = s => s.StatementStatus == StatementStatusType.StatusB;
Func<Statement, bool> filterByStatuses = filterByStatusA.OrNullable(filterByStatusB);
Func<Statement, bool> filterByItemNumber = null;
if (!string.IsNullOrEmpty(request.ItemNumber))
filterByItemNumber = s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber);
Func<Statement, bool> endFilter = filterByItemNumber.And(filterByStatuses);
好的,我们已经超越了如何通过组合它们来编写过滤器,Func<..>
但我们仍然有问题。
如果结果过滤器为空,我们会遇到什么问题?答:ArgumentNullException
由于文档。我们必须思考这个案例。
使用 simple 还会遇到哪些问题Func<...>
?IEnumerable<T>
那么,你必须知道和IQueryable<T>
接口之间的区别。简而言之,对 IEnumerable 的所有操作都会导致对所有元素的简单迭代(嗯,它很懒,IEnumerable 确实比 IQueryable 慢)。因此,例如,将 Where(filter)、Take(100)、ToList() 组合到具有 10000 个对该过滤器有害的元素和 400 个有益的元素的集合上,将导致迭代超过 10100 个元素。如果您为 IQueryable 编写了类似的代码,则过滤请求将在数据库服务器上发送,并且如果您在数据库上配置了索引,该服务器将仅迭代 ~400(或 1000,但不是 10100)。那么你的代码会发生什么。
var results = ctx.Statements // you are getting DbSet<Statement> that implements interface IQueryable<Statement> (and IQueryable<T> implements IEnumerable<T>)
.Include("StatementDetails") // still IQueryable<Statement>
.Include("StatementDetails.Entry") // still IQueryable<Statement>
.Where(filter) // Cuz your filter is Func<..> and there are no extension methods on IQueryable that accepts Func<...> as parameter, your IQueryable<Statement> casted automatically to IEnumerable<Statement>. Full collection will be loaded in your memory and only then filtered. That's bad
.Take(100) // IEnumerable<Statement>
.Select(s => new StatementSearchResultDTO { .... // IEnumerable<Statement> -> IEnumerable<StatementSearchResultDTO>
}
好的。现在你明白问题所在了。因此,可以这样编写适合您的简单正确代码:
using (var ctx = new MyContext()) {
results = ctx.Statements
.Include("StatementDetails")
.Include("StatementDetails.Entry")
.AsQueryable();
if (!string.IsNullOrEmpty(request.ItemNumber))
results = results.Where(s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber));
if (request.StatusA) {
if (request.StatusB)
results = results.Where(s => s.StatementStatus == StatementStatusType.StatusA ||
s.StatementStatus == StatementStatusType.StatusA);
else
results = results.Where(s => s.StatementStatus == StatementStatusType.StatusA);
}
else {
if (request.StatusB) {
results = results.Where(s => s.StatementStatus == StatementStatusType.StatusB);
}
else {
// do nothing
}
}
results = .Take(100)
.Select(s => new StatementSearchResultDTO{ ....
};
// .. now you can you results.
}
是的,完全丑陋,但现在你的数据库解决了如何找到满足过滤器的语句。因此,这个请求是尽可能快的。现在我们必须了解我在上面编写的代码中发生了什么神奇的事情。让我们比较两个代码示例:
results = results.Where(s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber));
还有这个:
Func<Statement, bool> filter = s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber);
results = results.Where(filter);
有什么区别?为什么首先更快?答:当编译器看到第一个代码时,它会检查results
is的类型IQueryable<T>
,IEnumerable<T>
因此括号内的条件可以有类型Func<Statement, bool>
(编译函数)或Expression<Func<Statement, bool>>
(数据,可以在函数中编译)。编译器选择Expression
(为什么 - 真的不知道,只是选择)。第一个对象查询的请求不是在 C# 语句中编译,而是在 SQL 语句中编译并发送到服务器。由于存在索引,您的 SQL 服务器可以优化请求。
好吧,更好的方法 - 编写自己的表达式。有不同的方法可以编写自己的表达式,但是有一种方法可以用不难看的语法来编写它。您不能只从另一个表达式调用一个表达式的问题 - 实体框架不支持并且另一个 ORM 不支持。因此,我们可以使用 Pete Montgomery 的 PredicateBuilder:链接。然后在适合我们的表达式上写两个简单的扩展。
public static Expression<Func<T, bool>> OrNullable<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
{
if (first != null && second != null)
return first.Compose(second, Expression.OrElse);
if (first != null)
return second;
if (second != null)
}
And 也一样。现在我们可以编写我们的过滤器了:
{
Expression<Func<Statement, bool>> filterByStatusA = null;
Expression<Func<Statement, bool>> filterByStatusB = null;
if (request.StatusA)
filterByStatusA = s => s.StatementStatus == StatementStatusType.StatusA;
if (request.StatusB)
filterByStatusB = s => s.StatementStatus == StatementStatusType.StatusB;
Expression<Func<Statement, bool>> filterByStatuses = filterByStatusA.OrNullable(filterByStatusB);
Expression<Func<Statement, bool>> filterByItemNumber = null;
if (!string.IsNullOrEmpty(request.ItemNumber))
filterByItemNumber = s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber);
Expression<Func<Statement, bool>> endFilter = filterByItemNumber.And(filterByStatuses);
requests = ...;
if (endFilter != null)
requests = requests.Where(endFilter);
}
您可能会遇到问题,因为 .NET < 4.0 中的类ExpressionVisitor
是PredicateBuilder
密封的。您可以编写自己的 ExpressionVisitor 或从本文中复制它。