12

我需要对 ObjectSet 进行一些过滤,以通过执行以下操作获取我需要的实体:

query = this.ObjectSet.Where(x => x.TypeId == 3); // this is just an example;

稍后在代码中(以及在启动延迟执行之前)我再次过滤查询,如下所示:

query = query.Where(<another lambda here ...>);

到目前为止效果很好。

这是我的问题:

这些实体包含一个DateFrom属性和一个DateTo属性,它们都是DataTime类型。它们代表了一段时间

我需要过滤实体以仅获取属于时间段集合的实体。集合中的句点不一定是连续的,因此,检索实体的逻辑如下所示:

entities.Where(x => x.DateFrom >= Period1.DateFrom and x.DateTo <= Period1.DateTo)
||
entities.Where(x => 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(每个时间段的一个过滤器,用于集合中的多个时间段),但是,它转换为 AND 比较而不是 OR 比较,它根本不返回任何实体,因为一个实体显然不能是多个时间段的一部分。

我需要在这里构建某种动态 linq 来聚合周期过滤器。


更新

根据hatten的回答,我添加了以下成员:

private Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> 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<T, bool>>(resultBody, parameter);
}

声明了一个新的 CombineWithOr 表达式:

Expression<Func<DocumentEntry, bool>> resultExpression = n => false;

并在我的周期集合迭代中使用它,如下所示:

foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;
    Expression<Func<DocumentEntry, bool>> expression = de => de.Date >= period.DateFrom && de.Date <= period.DateTo;
    resultExpression = this.CombineWithOr(resultExpression, expression);
}

var documentEntries = query.Where(resultExpression.Compile()).ToList();

我查看了生成的 SQL,就好像表达式根本没有效果。生成的 SQL 返回先前编程的过滤器,但不返回组合的过滤器。为什么 ?


更新 2

我想试试 feO2x 的建议,所以我重写了我的过滤器查询,如下所示:

query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))

如您所见,我添加了AsEnumerable()但编译器给了我一个错误,它无法将 IEnumerable 转换回 IQueryable,因此我ToQueryable()在查询末尾添加了:

query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))
            .ToQueryable();

一切正常。我可以编译代码并启动此查询。但是,它不符合我的需求。

在分析生成的 SQL 时,我可以看到过滤不是 SQL 查询的一部分,因为它会在处理过程中过滤内存中的日期。我想你已经知道了,这就是你打算建议的。

您的建议有效,但是,因为它获取了所有实体(并且有成千上万的实体),所以从数据库中取回大量实体真的很慢。

我真正想要的是将周期过滤作为结果 SQL 查询的一部分发送,因此在完成过滤过程之前它不会返回大量实体。

4

4 回答 4

8

尽管有很好的建议,但我还是选择了LinqKit。原因之一是我将不得不在代码的许多其他地方重复相同类型的谓词聚合。使用 LinqKit 是最简单的一种,更不用说我只需编写几行代码就可以完成。

以下是我使用 LinqKit 解决问题的方法:

var predicate = PredicateBuilder.False<Document>();
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 方面做得很好。

于 2013-04-30T00:11:05.853 回答
4

您可以使用如下方法:

Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> 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<T, bool>>(resultBody, parameter);
}

接着:

Expression<Func<T, bool>> resultExpression = n => false; // Always false, so that it won't affect the OR.
foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;
    Expression<Func<T, bool>> 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<Func<T, bool>>)

http://www.albahari.com/nutshell/predicatebuilder.aspx

编辑:该行Expression<Func<DocumentEntry, bool>> resultExpression = n => false;只是一个占位符。CombineWithOr方法需要两个方法来组合,如果你写Expression<Func<DocumentEntry, bool>> resultExpression;', you can't use it in the call to了CombineWithOr for the first time in yourforeach`循环。就像下面的代码:

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没有任何影响。

于 2013-04-26T17:46:50.960 回答
1

这段代码怎么样:

var targets = query.Where(de => 
    ratePeriods.Any(period => 
        de.Date >= period.DateFrom && de.Date <= period.DateTo));

我使用 LINQAny运算符来确定是否有任何符合de.Date. 虽然我不太确定这是如何通过实体转换为有效的 SQL 语句的。如果您可以发布生成的 SQL,那对我来说会很有趣。

希望这可以帮助。

哈滕回答后更新:

我不认为 hattnn 的解决方案会起作用,因为 Entity Framework 使用 LINQ 表达式来生成针对数据库执行的 SQL 或 DML。因此,Entity Framework 依赖于IQueryable<T>接口而不是IEnumerable<T>. 现在两个接口都实现了默认的 LINQ 运算符(如 Where、Any、OrderBy、FirstOrDefault 等),因此有时很难看出差异。这些接口的主要区别在于,在IEnumerable<T>扩展方法的情况下,返回的枚举会不断更新而没有副作用,而在IQueryable<T>实际表达式的情况下,是重构的,这不是没有副作用的(即您正在更改表达式最终用于创建 SQL 查询的树)。

现在实体框架支持 ca. LINQ 的 50 个标准查询运算符,但是如果您编写自己的方法来操作一个IQueryable<T>(如 hatnn 的方法),这将导致实体框架可能无法解析的表达式树,因为它根本不知道新的扩展方法. 这可能是您在组合过滤器后看不到组合过滤器的原因(尽管我预计会有例外)。

Any 运算符的解决方案何时起作用:

在评论中,您告诉您遇到了System.NotSupportedException无法创建“RatePeriod”类型的常量值。此上下文仅支持原始类型或枚举类型。RatePeriod对象是内存中的对象并且不被实体框架ObjectContextDbContext. 我做了一个小测试解决方案,可以从这里下载:https ://dl.dropboxusercontent.com/u/14810011/LinqToEntitiesOrOperator.zip

我将 Visual Studio 2012 与 LocalDB 和 Entity Framework 5 一起使用。要查看结果,请打开类LinqToEntitiesOrOperatorTest,然后打开测试资源管理器,构建解决方案并运行所有测试。你会认识到那ComplexOrOperatorTestWithInMemoryObjects会失败,所有其他的都应该通过。

我使用的上下文如下所示:

public class DatabaseContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    public DbSet<RatePeriod> 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; }
}

好吧,它就这么简单:-)。在测试项目中,有两种重要的单元测试方法:

    [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<RatePeriod>
            {
                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());
    }

请注意,第一个方法通过,而第二个方法失败,除了上面提到的异常,尽管两种方法做的事情完全相同,除了在第二种情况下,我在内存中创建了DatabaseContext不知道的利率周期对象。

你能做些什么来解决这个问题?

  1. 您的RatePeriod对象是否分别位于相同的ObjectContextDbContext中?然后像我在上面提到的第一个单元测试中那样直接使用它们。

  2. 如果没有,您可以一次加载所有帖子还是会导致OutOfMemoryException?如果没有,您可以使用以下代码。请注意AsEnumerable()导致Where运算符被用于IEnumerable<T>接口而不是IQueryable<T>. 实际上,这会导致所有帖子被加载到内存中然后过滤:

    [TestMethod]
    public void CorrectComplexOrOperatorTestWithInMemoryObjects()
    {
        var inMemoryRatePeriods = new List<RatePeriod>
            {
                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());
    }
    
  3. 如果第二种解决方案不可行,那么我建议您编写一个 TSQL 存储过程,在其中传递您的费率周期并形成正确的 SQL 语句。此解决方案也是性能最高的解决方案。

于 2013-04-26T16:21:59.923 回答
0

无论如何,我认为动态 LINQ 查询的创建并不像我想象的那么简单。尝试使用 Entity SQL,类似于以下方式:

var filters = new List<string>();
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);

这可能不完全正确(我没有尝试过),但它应该与此类似。

于 2013-04-29T21:37:57.417 回答