【问题标题】:Perform a single query based on comparison of sum of child collections基于子集合总和的比较执行单个查询
【发布时间】:2020-02-16 03:11:49
【问题描述】:

我有以下课程

    public class Order
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public DateTime Date { get; set; }
        public decimal TotalPaid { get { return Payments.Sum(x => x.Amount); } }
        public decimal Total { get { return OrderItems.Sum(x => x.TotalPrice); } }
        public bool PaidCompletely { get { return Total == TotalPaid; } }
        public List<OrderItem> OrderItems { get; set; } = new List<OrderItem>();
        public List<Payment> Payments { get; set; } = new List<Payment>();
    }

    public class OrderItem
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public decimal PricePerUnit { get; set; }
        public decimal Quantity { get; set; }
        public decimal TotalPrice { get { return PricePerUnit * Quantity; } }
    }

    public class Payment
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public decimal Amount { get; set; }
        public DateTime Date { get; set; }
    }

我想获得所有未全额付款的订单。

以下不工作

var OrdersNotFullyPaidShort = 
    context
    .Orders
    .Where(order => !order.PaidCompletely)
    .ToList();

给出错误

LINQ 表达式 'Where(source: DbSet, predicate: (o) => !(o.PaidCompletely))' 无法翻译...

以下工作,但我必须在查询中再次重写所有逻辑,而我在类中也定义了逻辑:

var OrdersNotFullyPaidLong =
    context
    .Orders
    .Where(order => order.OrderItems.Sum(orderItem => orderItem.PricePerUnit * orderItem.Quantity) == order.Payments.Sum(payment => payment.Amount))
    .ToList();

并生成这个非常丑陋的查询:

      SELECT [o].[Id], [o].[Date]
      FROM [Orders] AS [o]
      WHERE (((
          SELECT SUM([o0].[PricePerUnit] * [o0].[Quantity])
          FROM [OrderItems] AS [o0]
          WHERE ([o].[Id] = [o0].[OrderId]) AND [o0].[OrderId] IS NOT NULL) = (
          SELECT SUM([p].[Amount])
          FROM [Payments] AS [p]
          WHERE ([o].[Id] = [p].[OrderId]) AND [p].[OrderId] IS NOT NULL)) AND ((
          SELECT SUM([o0].[PricePerUnit] * [o0].[Quantity])
          FROM [OrderItems] AS [o0]
          WHERE ([o].[Id] = [o0].[OrderId]) AND [o0].[OrderId] IS NOT NULL) IS NOT NULL AND (
          SELECT SUM([p].[Amount])
          FROM [Payments] AS [p]
          WHERE ([o].[Id] = [p].[OrderId]) AND [p].[OrderId] IS NOT NULL) IS NOT NULL)) OR ((
          SELECT SUM([o0].[PricePerUnit] * [o0].[Quantity])
          FROM [OrderItems] AS [o0]
          WHERE ([o].[Id] = [o0].[OrderId]) AND [o0].[OrderId] IS NOT NULL) IS NULL AND (
          SELECT SUM([p].[Amount])
          FROM [Payments] AS [p]
          WHERE ([o].[Id] = [p].[OrderId]) AND [p].[OrderId] IS NOT NULL) IS NULL)

有没有办法使用我的第一种查询方式?我做错了什么,我的类没有明确定义吗?

【问题讨论】:

  • 隐藏在属性中的公式通常无法转换为 LINQ。这在技术上是不可能的。只能翻译 where-select 子句中的公式。试着用不同的方式表达。从外部查看您的所有数据。在那里您可以进行连接、求和、分组等操作。您的构造仅适用于 LINQ to 对象,因为不需要将其转换为 SQL。
  • @Holger ,你的意思是我必须在我的 SQL 数据库中创建视图吗?
  • 不,您必须在一个地方、一行中形成一个查询。不是 .Where(order =&gt; !order.PaidCompletely) ,而是 .Where(order =&gt; OrderItems.Sum(x =&gt; x.TotalPrice))-Payments.Sum(x =&gt; x.Amount)&gt;0) 这只是演示方法,它不是最终解决方案。这样一来,LINQ 就有机会,至少可以得到公式。 LINQ 在运行时完成,没有可用的源,只有编译的机器代码。 LINQ 无法将机器代码转换为 SQL。在 Lambda 中,代码不是直接的机器代码,而是 Expession>,这是一种存储的源代码。
  • No, you have to form a Query in One Place, in one line 好吧,严格来说这不是真的。它可以跨越多行代码。但我同意@Holger 的其余评论。
  • @Holger 这仍然是一个相当神秘的答案:-/。我是否应该在模型中定义一个 IQueryable 方法,以便在需要时调用它?

标签: c# sql-server entity-framework-core


【解决方案1】:

我试过了:

    var query = db.Order.Select(x => new { Id = x.Id, Date = x.Date,
                    Payments = x.Payments.Sum(y => y.Amount),
                    ToPay = x.OrderItems.Sum(y => y.PricePerUnit * y.Quantity)})
                   .Where(x => x.Payments < x.ToPay);

它创造了

SELECT 
     [Project2].[Id] AS [Id], 
[Project2].[Date] AS [Date], 
[Project2].[C1] AS [C1], 
[Project2].[C2] AS [C2]
FROM ( SELECT 
    [Project1].[Id] AS [Id], 
    [Project1].[Date] AS [Date], 
    [Project1].[C1] AS [C1], 
    (SELECT 
        SUM([Filter2].[A1_0]) AS [A1]
        FROM ( SELECT 
            [Extent3].[PricePerUnit] * [Extent3].[Quantity] AS [A1_0]
            FROM [dbo].[OrderItems] AS [Extent3]
            WHERE [Project1].[Id] = [Extent3].[Order_Id]
        )  AS [Filter2]) AS [C2]
    FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Date] AS [Date], 
        (SELECT 
            SUM([Extent2].[Amount]) AS [A1]
            FROM [dbo].[Payments] AS [Extent2]
            WHERE [Extent1].[Id] = [Extent2].[Order_Id]) AS [C1]
        FROM [dbo].[Orders] AS [Extent1]
    )  AS [Project1]
)  AS [Project2]
WHERE ([Project2].[C1] < [Project2].[C2]) AND ([Project2].[C1] < [Project2].[C2])

这对我来说看起来很安静 它可能取决于 LINQ 提供程序,即创建的查询。

在关心“丑陋”的查询之前,您应该很高兴可以创建一个有效的查询。 进一步优化 LINQ 表达式的唯一原因是性能。 您无法针对可读性或美观进行优化。

【讨论】:

  • 确实看起来不错。我想知道是否应该改用计算列 (docs.microsoft.com/en-us/ef/core/modeling/relational/…),这样我就不必每次在查询中都写这些总和?或者有没有更好的方法来动态计算它们而不会导致 N+1 或 N*N 查询?
  • 我不知道你所说的 N+1 或 N*N 查询是什么意思,这是 1 个查询,用单词“One”表示。有一个查询是为了确保从客户端到服务器的通信最少,发送一个查询,得到一个结果集。不要被几个选择混淆,这只是语言结构。 SQL-Servers 擅长优化。计算列的优势很小,与在查询中发送公式或在表设计中预先存储公式的区别很小。您应该始终关注 SQL-Server 上的计算与 C# 客户端上的计算。这很关键。
  • 在我的实际应用程序中,我正在尝试获取未全额支付的订单,但我以某种方式最终得到 (N*2)+1 个查询:它首先在一个查询中获取所有订单,然后获取每个订单及其订单项,然后再次获取每个订单及其付款......这种行为可能与这个问题无关,但我没有看到任何其他原因......
  • 我只能为您提供在 1 个查询中结束的代码。不要在属性中使用任何计算!,所有的“Total”和“TotalPaid”和“PAidCompletly”等等,不要使用它。 (如您所见,我的 linq 查询中没有)。这样的事情可能会导致对数据库的多次查询,因为它们无法被 SQL-Server 处理,它们将在客户端上执行。总是。无条件的。你有一个丑陋的查询,我给了你一个替代查询;而且您一直在讲述有多个查询的故事。我很怀疑您是否知道查询是什么。
  • 我会将您的建议视为理所当然,并将删除所有计算的属性以查看是否有任何改变。我确实知道查询是什么,但我很难理解这种 LINQ 魔法是如何工作的,以及如何创建 LINQ 语句,这些语句将通过一次访问数据库而不是这些 N+1 往返来生成单个查询。我知道一个查询中的多项选择本身并没有错...
【解决方案2】:

我认为这篇博文很好地解释了发生了什么以及存在哪些可能的解决方案:

https://daveaglick.com/posts/computed-properties-and-entity-framework

【讨论】:

    猜你喜欢
    • 2014-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-25
    • 1970-01-01
    • 1970-01-01
    • 2023-04-08
    • 2014-06-23
    相关资源
    最近更新 更多