Tolsto
Tolsto

Reputation: 1428

How to combine inner join and left outer join in Rails

I have two Models invoice and payments. The relationship between them is invoice has_many payments.

I'm using the following left outer join to return all invoices that have not been paid at all:

result1 = Invoice.includes(:payments).where(:payments => { :id => nil })

I'm also interested in all invoices that have been partially paid. To return those I use an inner join:

result2 = Invoice.joins(:payments).group("transfers.id").having("sum(payments.amount) < invoice.amount")

I would now like to combine the two results, i.e. I want all Invoices that have either not been paid, or not been fully paid. I know that I could just do result = result1 + result2. However, this doesn't return an ActiveRecord object. Is there a way to combine those two queries in a single query?

I use Rails 4.1 and PostgreSQL

Upvotes: 2

Views: 2431

Answers (2)

Tolsto
Tolsto

Reputation: 1428

I ended up doing just a LEFT OUTER JOIN. However, Rails doesn't seem to support group and having with the LEFT OUTER JOIN generated by includes(). Therefore, i had to construct the join manually which actually doesn't feel like "the Rails way".

My query:

    Invoice.joins("LEFT JOIN payments ON payments.transfer_id = invoices.id").group("invoices.id, payments.id").having("((payments.id IS NULL) OR (sum(payments.amount) < invoices.amount))")

Update: This doesn't work as expected. Please see the accepted answer.

Upvotes: 0

Todd Agulnick
Todd Agulnick

Reputation: 1985

I believe you're correct that you can't get ActiveRecord to generate anything other than an inner join without writing it yourself. But I wouldn't use includes in this context, for while it does happen to cause ActiveRecord to generate a different join, that is an "implementation detail" -- it's fundamental purpose is to load the associated models, which is not necessarily what you want.

In your proposed solution, I don't understand why you'd group by both invoices.id and payments.id -- that seems to defeat the purpose of grouping.

You could do something like the following, although I can't say that this seems much more Rails-ish.

Invoice.joins("LEFT JOIN payments ON payments.transfer_id = invoices.id")
  .select("invoices.id, SUM(payments.amount) AS total_paid")
  .group("invoices.id")
  .having("SUM(payments.amount) IS NULL OR SUM(payments.amount) < invoices.amount")

This will return a list of Invoice objects with only the id field and total_paid field set. If you need other fields available, add them to both the select statement and the the group statement.

Upvotes: 3

Related Questions