【问题标题】:Multi-Table Invoice SUM Comparison多表发票总和比较
【发布时间】:2017-10-24 20:57:15
【问题描述】:

假设我在 Rails 应用中有 3 个表:

发票

id  | customer_id  | employee_id  | notes
---------------------------------------------------------------
1   | 1            | 5            | An order with 2 items.
2   | 12           | 5            | An order with 1 item.
3   | 17           | 12           | An empty order.
4   | 17           | 12           | A brand new order.

invoice_items

id  | invoice_id  | price  | name
---------------------------------------------------------
1   | 1           | 5.35   | widget
2   | 1           | 7.25   | thingy
3   | 2           | 1.25   | smaller thingy
4   | 2           | 1.25   | another smaller thingy

invoice_payments

id  | invoice_id  | amount  | method      | notes
---------------------------------------------------------
1   | 1           | 4.85    | credit card | Not enough 
2   | 1           | 1.25    | credit card | Still not enough
3   | 2           | 1.25    | check       | Paid in full

这代表 4 个订单:

第一个有 2 个项目,总共 12.60。它有两次付款,总支付金额为 6.10。此订单已部分支付。

第二个只有一件物品,一件付款,合计1.25。此订单已全额付款。

第三个订单没有商品或付款。这对我们很重要,有时我们会使用这种情况。它也被视为全额支付。

最终订单又多了一件,总共 1.25,但目前还没有付款。

现在我需要一个查询:

显示所有尚未全额付款的订单;也就是说,所有订单的项目总和大于付款的总和。

我可以用纯 sql 来做:

SELECT      invoices.*,
            invoice_payment_amounts.amount_paid AS amount_paid,
            invoice_item_amounts.total_amount AS total_amount
FROM        invoices
LEFT JOIN   (
                SELECT      invoices.id AS invoice_id,
                            COALESCE(SUM(invoice_payments.amount), 0) AS amount_paid
                FROM        invoices
                LEFT JOIN   invoice_payments
                ON          invoices.id = invoice_payments.invoice_id
                GROUP BY    invoices.id
            ) AS invoice_payment_amounts
ON          invoices.id = invoice_payment_amounts.invoice_id
LEFT JOIN   (
                SELECT      invoices.id AS invoice_id,
                            COALESCE(SUM(invoice_items.item_price), 0) AS total_amount
                FROM        invoices
                LEFT JOIN   invoice_items
                ON          invoices.id = invoice_items.invoice_id
                GROUP BY    invoices.id
            ) AS invoice_item_amounts
ON          invoices.id = invoice_item_amounts.invoice_id
WHERE       amount_paid < total_amount

但是...现在我需要将其放入 Rails(可能作为范围)。我可以使用 find_by_sql,但它会返回一个数组,而不是 ActiveRecord::Relation,这不是我需要的,因为我想将它与其他范围链接(例如,一个过期的范围,它使用这个)等。

所以原始 SQL 可能不是正确的方法......但是什么是?我无法使用 activerecord 的查询语言执行此操作。

到目前为止,我得到的最接近的是:

Invoice.select('invoices.*, SUM(invoice_items.price) AS total, SUM(invoice_payments.amount) AS amount_paid').
  joins(:invoice_payments, :invoice_items).
  group('invoices.id').
  where('amount_paid < total')

但这失败了,因为对于像 #1 这样的订单,有多次付款,它错误地将订单价格翻了一番(由于多次加入),显示为仍未付款。我在 SQL 中遇到了同样的问题,这就是我按照我的方式构建它的原因。

这里有什么想法吗?

【问题讨论】:

  • 您是否考虑在invoices 表中添加一些额外的列?也许您可以添加 payment_totaltotal 列,这将使这些查询变得轻而易举

标签: sql ruby-on-rails ruby-on-rails-4


【解决方案1】:

您可以使用 MySQL 的 group byhaving 子句获取结果:

纯 MySQL 查询:

  SELECT `invoices`.* FROM `invoices` 
  INNER JOIN `invoice_items` ON 
    `invoice_items`.`invoice_id` = `invoices`.`id` 
  INNER JOIN `invoice_payments` ON 
    `invoice_payments`.`invoice_id` = `invoices`.`id` 
  GROUP BY invoices.id 
    HAVING sum(invoice_items.price) < sum(invoice_payments.amount) 

ActiveRecord 查询:

Invoice.joins(:invoice_items, :invoice_payments).group("invoices.id").having("sum(invoice_items.price) < sum(:invoice_payments.amount)")

【讨论】:

    【解决方案2】:

    在 Rails 中构建更复杂的查询时,Arel Really Exasperates Logicians 通常会派上用场

    Arel 是 Ruby 的 SQL AST 管理器。它

    1. 简化了复杂 SQL 查询的生成,并且
    2. 适应各种 RDBMS。

    这是一个基于需求的 Arel 实现示例

    invoice_table = Invoice.arel_table
    
    # Define invoice_payment_amounts
    payment_arel_table = InvoicePayment.arel_table
    invoice_payment_amounts = Arel::Table.new(:invoice_payment_amounts)
    payment_cte = Arel::Nodes::As.new(
      invoice_payment_amounts,
      payment_arel_table
        .project(payment_arel_table[:invoice_id],
                 payment_arel_table[:amount].sum.as("amount_paid"))
        .group(payment_arel_table[:invoice_id])
    )
    
    # Define invoice_item_amounts
    item_arel_table = InvoiceItem.arel_table
    invoice_item_amounts =  Arel::Table.new(:invoice_item_amounts)
    item_cte = Arel::Nodes::As.new(
      invoice_item_amounts,
      item_arel_table
        .project(item_arel_table[:invoice_id],
                 item_arel_table[:price].sum.as("total"))
        .group(item_arel_table[:invoice_id])
    )
    
    # Define main query
    query = invoice_table
              .project(
                invoice_table[Arel.sql('*')],
                invoice_payment_amounts[:amount_paid],
                invoice_item_amounts[:total]
              )
              .join(invoice_payment_amounts).on(
                invoice_table[:id].eq(invoice_payment_amounts[:invoice_id])
              )
              .join(invoice_item_amounts).on(
                invoice_table[:id].eq(invoice_item_amounts[:invoice_id])
              )
              .where(invoice_item_amounts[:total].gt(invoice_payment_amounts[:amount_paid]))
              .with(payment_cte, item_cte)
    
    
    res = Invoice.find_by_sql(query.to_sql)
    for r in res do
      puts "---- Invoice #{r.id} -----"
      p r
      puts "total: #{r[:total]}"
      puts "amount_paid: #{r[:amount_paid]}"
      puts "----"
    end
    

    这将使用您提供给问题的示例数据返回与您的 SQL 查询相同的输出。 输出:

     <Invoice id: 2, notes: "An order with 1 items.", created_at: "2017-12-18 21:15:47", updated_at: "2017-12-18 21:15:47">
     total: 2.5
     amount_paid: 1.25
     ----
     ---- Invoice 1 -----
     <Invoice id: 1, notes: "An order with 2 items.", created_at: "2017-12-18 21:15:47", updated_at: "2017-12-18 21:15:47">
     total: 12.6
     amount_paid: 6.1
     ----
    

    Arel 非常灵活,因此您可以以此为基础,根据您可能有的更具体的要求来优化查询条件。

    我强烈建议您考虑在 Invoice 表中创建缓存列(total、amount_paid)并对其进行维护,这样您就可以避免这种复杂的查询。至少total 附加列的创建和填充数据非常简单。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多