Сравнение с несколькими таблицами SUM

Скажем, у меня есть 3 таблицы в приложении rails:

счета

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

Но... теперь мне нужно получить это в рельсы (возможно, как область). Я могу использовать 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, поэтому я структурировал ее так, как я.

Любые мысли здесь?

Ответ 1

Вы можете получить свои результаты, используя группу и having MySQL как:

Чистый 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 - менеджер SQL AST для Ruby. Это

  • упрощает создание сложных SQL-запросов и
  • адаптируется к различным РСУБД.

Вот пример того, как реализация 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 довольно гибкий, поэтому вы можете использовать его в качестве базы и уточнить условия запроса на основе более конкретных требований, которые могут возникнуть у вас.

Я настоятельно рекомендую вам рассмотреть возможность создания столбцов кэша (total, amount_paid) в таблице счетов и их поддержки, чтобы избежать этого сложного запроса. По крайней мере, дополнительный столбец total был бы довольно простым для создания и заполнения данных.