如何在使用 PredicateBuilder 时防止多个联接到同一个表

本文关键字:同一个 PredicateBuilder | 更新日期: 2023-09-27 18:31:03

在这个例子中,我使用的是MS提供的NorthWind数据库。

我正在尝试使用谓词生成器来组装多个

Expression<Func<T,bool>> 

到单个表达式树中。在我看来,它大多工作得很好,但有一个我似乎无法解决的主要缺陷。

我目前有两个表达式定义为:

Expression<Func<Customer, bool>> UnitPriceLessThan2 = c => 
        c.Orders.Any(o => o.Order_Details.Any(d => d.Product.UnitPrice <= 2));
Expression<Func<Customer, bool>> UnitPriceGreaterThan5 = c => 
        c.Orders.Any(o => o.Order_Details.Any(d => d.Product.UnitPrice >= 5));

我使用皮特·蒙哥马利(Pete Montgomery)的通用谓词生成器来或将这两个放在一起,如下所示:

Expression<Func<Customer,bool>> PriceMoreThan5orLessThan2 = 
         UnitPriceLessThan2.Or(UnitPriceGreaterThan5);

这两个表达式都需要通过相同的路径导航到 Product 实体,因此对这两个条件重用相同的子查询是有意义的。如果我只是手动编写条件,它看起来像这样:

Expression<Func<Customer,bool>> PriceMoreThan5orLessThan2 = c => 
        c.Orders.Any(o => 
            o.Order_Details.Any(d => d.Product.UnitPrice >= 5 || 
                                d.Product.UnitPrice <= 2));

但是,由于动态构建这些谓词的要求,我不能这样做,因为会有数百种可能的组合......或更多。

所以我的问题是我如何防止 LINQ to 实体创建这样的查询:

SELECT 
/*all the customer columns*/
FROM [dbo].[Customers] AS [Extent1]
WHERE ( EXISTS (SELECT 
    1 AS [C1]
    FROM ( SELECT 
        [Extent2].[OrderID] AS [OrderID]
        FROM [dbo].[Orders] AS [Extent2]
        WHERE [Extent1].[CustomerID] = [Extent2].[CustomerID]
    )  AS [Project1]
    WHERE  EXISTS (SELECT 
        1 AS [C1]
        FROM  [dbo].[Order Details] AS [Extent3]
        INNER JOIN [dbo].[Products] AS [Extent4] ON [Extent3].[ProductID] = [Extent4].[ProductID]
        WHERE ([Project1].[OrderID] = [Extent3].[OrderID]) AND ([Extent4].[UnitPrice] <= cast(2 as decimal(18)))
    )
)) OR ( EXISTS (SELECT 
    1 AS [C1]
    FROM ( SELECT 
        [Extent5].[OrderID] AS [OrderID]
        FROM [dbo].[Orders] AS [Extent5]
        WHERE [Extent1].[CustomerID] = [Extent5].[CustomerID]
    )  AS [Project4]
    WHERE  EXISTS (SELECT 
        1 AS [C1]
        FROM  [dbo].[Order Details] AS [Extent6]
        INNER JOIN [dbo].[Products] AS [Extent7] ON [Extent6].[ProductID] = [Extent7].[ProductID]
        WHERE ([Project4].[OrderID] = [Extent6].[OrderID]) AND (([Extent7].[UnitPrice] >= cast(5 as decimal(18)))))));

问题是,当我们实际上只需要一个子查询时,我们创建了两个 EXISTS 子查询。

相反,我希望查询看起来像这样:

SELECT 
/*all the customer columns*/
FROM [dbo].[Customers] AS [Extent1]
WHERE( EXISTS (SELECT 
    1 AS [C1]
    FROM ( SELECT 
        [Extent5].[OrderID] AS [OrderID]
        FROM [dbo].[Orders] AS [Extent5]
        WHERE [Extent1].[CustomerID] = [Extent5].[CustomerID]
    )  AS [Project4]
    WHERE  EXISTS (SELECT 
        1 AS [C1]
        FROM  [dbo].[Order Details] AS [Extent6]
        INNER JOIN [dbo].[Products] AS [Extent7] ON [Extent6].[ProductID] = [Extent7].[ProductID]
        WHERE ([Project4].[OrderID] = [Extent6].[OrderID]) AND (([Extent7].[UnitPrice] >= cast(5 as decimal(18))) OR ([Extent7].[UnitPrice] <= cast(2 as decimal(18))))
    )
))

我是否可以以某种方式存储导航路径并将其重用为表达式,然后将这两个条件及其适当的用户提供的运算符和值注入其中?

或者使用一些表达式访问者实现来...我不知道到底是什么,找到并替换?

感谢您阅读我相当冗长的问题:)

如何在使用 PredicateBuilder 时防止多个联接到同一个表

首先,一个

提高可读性的小技巧,将它们添加到您的命名空间中,这不是工作所必需的,但我发现它使代码更易于阅读:

namespace YourNameSpaceHere
{
    using DetailPredicate = Expression<Func<Order_Details, bool>>;
    using CustomerPredicate = Expression<Func<Customer, bool>>;
其次,创建一个订单详细信息

谓词列表,当添加了所有可能的订单详细信息谓词时,我们聚合它们并应用它们:

public void foo()
{
  // List of predicates for order detail
  var predicates = new List<DetailPredicate>();
  // Logic to determine what predicates get added to list
  if(somelogic)
    predicates.Add(d => d.Product.UnitPrice >= 5);
  if(somethingelse)    
    predicates.Add(d => d.Product.UnitPrice <= 2);
  // Default to true
  var whereDetails = PredicateBuilder.True<Order_Details>();
  if (predicates.Any())
  {
     // Aggregate predicates with OR in between
     whereDetails = predicates.Aggregate(PredicateBuilder.Or);
  }
  // Apply aggregate
  CustomerPredicate PriceMoreThan5orLessThan2 = c => 
      c.Orders.Any(o => 
          o.Order_Details.Any(whereDetails);