动态添加新的lambda表达式以创建filter
我需要对ObjectSet进行一些过滤,以通过这样做获取我需要的实体:
query = this.ObjectSet.Where(x => x.TypeId == 3); // this is just an example;
稍后在代码中(以及在启动延迟执行之前)我再次过滤查询,如下所示:
query = query.Where();
到目前为止,这非常有效。
这是我的问题:
实体包含DateFrom属性和DateTo属性,它们都是DataTime类型。 它们代表了一段时间 。
我需要过滤实体,只获得那些属于时间段 集合的实体。 集合中的句点不一定是连续的 ,因此,检索实体的逻辑看起来像:
entities.Where(x => x.DateFrom >= Period1.DateFrom and x.DateTo x.DateFrom >= Period2.DateFrom and x.DateTo <= Period2.DateTo) ||
……以及集合中所有时期的不断变化。
我试过这样做:
foreach (var ratePeriod in ratePeriods) { var period = ratePeriod; query = query.Where(de => de.Date >= period.DateFrom && de.Date <= period.DateTo); }
但是一旦我启动了延迟执行,它就像我想要的那样将它转换为SQL(对于集合中的多个句点,每个时间段都有一个filter),但是,它转换为AND比较而不是OR比较,根本不返回任何实体,因为一个实体显然不能超过一个时期的一部分。
我需要在这里构建一些动态linq来聚合周期filter。
更新
根据hatten的回答,我添加了以下成员:
private Expression<Func> CombineWithOr(Expression<Func> firstExpression, Expression<Func> secondExpression) { // Create a parameter to use for both of the expression bodies. var parameter = Expression.Parameter(typeof(T), "x"); // Invoke each expression with the new parameter, and combine the expression bodies with OR. var resultBody = Expression.Or(Expression.Invoke(firstExpression, parameter), Expression.Invoke(secondExpression, parameter)); // Combine the parameter with the resulting expression body to create a new lambda expression. return Expression.Lambda<Func>(resultBody, parameter); }
声明了一个新的CombineWithOr表达式:
Expression<Func> resultExpression = n => false;
并在我的期间集合迭代中使用它,如下所示:
foreach (var ratePeriod in ratePeriods) { var period = ratePeriod; Expression<Func> expression = de => de.Date >= period.DateFrom && de.Date <= period.DateTo; resultExpression = this.CombineWithOr(resultExpression, expression); } var documentEntries = query.Where(resultExpression.Compile()).ToList();
我查看了生成的SQL,就像Expression完全没有效果一样。 生成的SQL返回先前编程的filter,但不返回组合filter。 为什么?
更新2
我想尝试一下feO2x的建议,所以我重写了我的filter查询,如下所示:
query = query.AsEnumerable() .Where(de => ratePeriods .Any(rp => rp.DateFrom = de.Date))
正如你所看到的,我添加了AsEnumerable()
但编译器给了我一个错误,它无法将IEnumerable转换回IQueryable,所以我在查询结束时添加了ToQueryable()
:
query = query.AsEnumerable() .Where(de => ratePeriods .Any(rp => rp.DateFrom = de.Date)) .ToQueryable();
一切正常。 我可以编译代码并启动此查询。 但是,它不符合我的需要。
在分析生成的SQL时,我可以看到过滤不是SQL查询的一部分,因为它在过程中过滤了内存中的日期。 我想你已经知道了,这就是你打算建议的。
你的建议是可行的,但是,因为它在内存中过滤它们之前从数据库中获取所有实体 (并且有成千上万的实体),从数据库中获取大量数据真的很慢。
我真正想要的是将周期过滤作为结果SQL查询的一部分发送,因此在完成过滤过程之前它不会返回大量实体。
尽管有很好的建议,但我不得不选择LinqKit 。 其中一个原因是我将不得不在代码中的许多其他地方重复相同类型的谓词聚合。 使用LinqKit是最简单的,更不用说我只需编写几行代码即可完成。
以下是我使用LinqKit解决问题的方法:
var predicate = PredicateBuilder.False(); foreach (var submittedPeriod in submittedPeriods) { var period = period; predicate = predicate.Or(d => d.Date >= period.DateFrom && d.Date <= period.DateTo); }
我启动了延迟执行(注意我之前调用了AsExpandable()
):
var documents = this.ObjectSet.AsExpandable().Where(predicate).ToList();
我查看了生成的SQL,它在将谓词转换为SQL方面做得很好。
您可以使用以下方法:
Expression> CombineWithOr(Expression> firstExpression, Expression> secondExpression) { // Create a parameter to use for both of the expression bodies. var parameter = Expression.Parameter(typeof(T), "x"); // Invoke each expression with the new parameter, and combine the expression bodies with OR. var resultBody = Expression.Or(Expression.Invoke(firstExpression, parameter), Expression.Invoke(secondExpression, parameter)); // Combine the parameter with the resulting expression body to create a new lambda expression. return Expression.Lambda>(resultBody, parameter); }
然后:
Expression> resultExpression = n => false; // Always false, so that it won't affect the OR. foreach (var ratePeriod in ratePeriods) { var period = ratePeriod; Expression> expression = (de => de.Date >= period.DateFrom && de.Date <= period.DateTo); resultExpression = CombineWithOr(resultExpression, expression); } // Don't forget to compile the expression in the end. query = query.Where(resultExpression.Compile());
有关更多信息,您可能需要查看以下内容:
组合两个表达式(Expression
http://www.albahari.com/nutshell/predicatebuilder.aspx
编辑:行Expression
只是一个占位符。 CombineWithOr
方法需要两种方法来组合,如果你编写Expression
for the first time in your
foreach`循环中for the first time in your
Expression
CombineWithOr时Expression
。 它就像下面的代码:
int resultOfMultiplications = 1; for (int i = 0; i < 10; i++) resultOfMultiplications = resultOfMultiplications * i;
如果resultOfMultiplications
没有任何内容,则不能在循环中使用它。
至于为什么lambda是n => false
。 因为它在OR
语句中没有任何影响。 例如, false OR someExpression OR someExpression
等于someExpression OR someExpression
。 那个false
没有任何影响。
这段代码怎么样:
var targets = query.Where(de => ratePeriods.Any(period => de.Date >= period.DateFrom && de.Date <= period.DateTo));
我使用LINQ Any
运算符来确定是否存在符合de.Date
任何速率周期。 虽然我不太确定如何将它转换为实体的高效SQL语句。 如果您可以发布生成的SQL,那对我来说非常有趣。
希望这可以帮助。
在hattenn回答之后更新:
我认为hattenn的解决方案不会起作用,因为Entity Framework使用LINQ表达式来生成针对数据库执行的SQL或DML。 因此,Entity Framework依赖于IQueryable
接口而不是IEnumerable
。 现在默认的LINQ运算符(如Where,Any,OrderBy,FirstOrDefault等)都在两个接口上实现,因此有时很难看到差异。 这些接口的主要区别在于,在IEnumerable
扩展方法的情况下,返回的枚举不断更新而没有副作用,而在IQueryable
的情况下,实际的表达式被重新组合,这是不自由的效果(即您正在更改最终用于创建SQL查询的表达式树)。
现在entity framework支持ca. LINQ的50个标准查询运算符,但如果你编写自己的方法来操作IQueryable
(如hatenn的方法),这将导致entity framework可能无法解析的表达式树,因为它根本不知道新的扩展方法。 这可能是您在编写组合filter后无法看到组合filter的原因(尽管我希望例外)。
Any运算符的解决方案何时起作用:
在评论中,您告诉您遇到了System.NotSupportedException
: 无法创建“RatePeriod”类型的常量值。 在此上下文中仅支持原始类型或枚举类型。 当RatePeriod
对象是内存中对象而不是由Entity Framework ObjectContext
或DbContext
跟踪时,就是这种情况。 我做了一个小测试解决方案,可以从这里下载: https : //dl.dropboxusercontent.com/u/14810011/LinqToEntitiesOrOperator.zip
我将Visual Studio 2012与LocalDB和Entity Framework 5一起使用。要查看结果,请打开LinqToEntitiesOrOperatorTest
类,然后打开Test Explorer,构建解决方案并运行所有测试。 您将认识到ComplexOrOperatorTestWithInMemoryObjects
将失败,所有其他人都应该通过。
我使用的上下文如下所示:
public class DatabaseContext : DbContext { public DbSet Posts { get; set; } public DbSet RatePeriods { get; set; } } public class Post { public int ID { get; set; } public DateTime PostDate { get; set; } } public class RatePeriod { public int ID { get; set; } public DateTime From { get; set; } public DateTime To { get; set; } }
嗯,它很简单:-)。 在测试项目中,有两个重要的unit testing方法:
[TestMethod] public void ComplexOrOperatorDBTest() { var allAffectedPosts = DatabaseContext.Posts.Where( post => DatabaseContext.RatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate)); Assert.AreEqual(3, allAffectedPosts.Count()); } [TestMethod] public void ComplexOrOperatorTestWithInMemoryObjects() { var inMemoryRatePeriods = new List { new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)}, new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)} }; var allAffectedPosts = DatabaseContext.Posts.Where( post => inMemoryRatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate)); Assert.AreEqual(3, allAffectedPosts.Count()); }
请注意,第一个方法通过,而第二个方法因上述exception而失败,尽管两种方法完全相同,只是在第二种情况下我在MemoryContext不知道的内存中创建了速率周期对象。
你能做些什么来解决这个问题?
-
您的
RatePeriod
对象分别驻留在相同的ObjectContext
或DbContext
吗? 然后就像我在上面提到的第一个unit testing中那样使用它们。 -
如果没有,您可以一次加载所有post,还是会导致
OutOfMemoryException
? 如果没有,您可以使用以下代码。 注意AsEnumerable()
调用导致Where
操作符用于IEnumerable
接口而不是IQueryable
。 实际上,这会导致所有post被加载到内存中然后被过滤:[TestMethod] public void CorrectComplexOrOperatorTestWithInMemoryObjects() { var inMemoryRatePeriods = new List
{ new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)}, new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)} }; var allAffectedPosts = DatabaseContext.Posts.AsEnumerable() .Where( post => inMemoryRatePeriods.Any( period => period.From < post.PostDate && period.To > post.PostDate)); Assert.AreEqual(3, allAffectedPosts.Count()); } -
如果第二个解决方案不可行,那么我建议编写一个TSQL存储过程,在其中传入速率周期并形成正确的SQL语句。 该解决方案也是性能最高的解决方案。
无论如何,我认为动态LINQ查询创建并不像我想象的那么简单。 尝试使用Entity SQL,类似于以下方式:
var filters = new List(); foreach (var ratePeriod in ratePeriods) { filters.Add(string.Format("(it.Date >= {0} AND it.Date <= {1})", ratePeriod.DateFrom, ratePeriod.DateTo)); } var filter = string.Join(" OR ", filters); var result = query.Where(filter);
这可能不完全正确(我还没有尝试过),但它应该与此类似。