I am building an Invoicing application where an Invoice has many Items and Payments.
In my index view I am showing a list of all Invoices including two virtual attributes:
def total
items.sum { |item| item.total }
end
def balance
self.payments.sum(:amount) - self.total
end
I noticed that an aweful lot of SQL is needed to display the index view. Would it be advisable to create two more table columns instead? So far, I chose not to because I don’t like having too much redundant data.
This is my controller:
def index
result = current_user.invoices.includes(:items, :payments)
@invoices = paginate(result)
end
index.html.erb:
<table id="index">
<thead>
<tr>
<th>Number</th>
<th>Date</th>
<th>Total</th>
<th>Balance</th>
<th></th>
</tr>
</thead>
<tbody>
<%= render @invoices %>
</tbody>
</table>
<%= will_paginate @invoices %>
_invoice.html.erb:
<tr>
<td>
<%= link_to invoice.number, invoice_path(invoice) %>
</td>
<td>
<%= l invoice.date %>
</td>
<td>
<%= number_to_currency(invoice.total) %>
</td>
<td>
<%= number_to_currency(invoice.balance) %>
</td>
<td>
<%= destroy_link(invoice) %>
</td>
</tr>
For each invoice on the index view these four SQL queries are being generated:
(0.1ms) SELECT SUM("payments"."amount") AS sum_id FROM "payments" WHERE "payments"."invoice_id" = 19
CACHE (0.0ms) SELECT "items".* FROM "items" WHERE "items"."invoice_id" = 19
CACHE (0.0ms) SELECT SUM("payments"."amount") AS sum_id FROM "payments" WHERE "payments"."invoice_id" = 19
CACHE (0.0ms) SELECT "items".* FROM "items" WHERE "items"."invoice_id" = 19
(That makes 40 SQL queries per index page.)
I must admit that I am relatively new to Rails. So I wonder if there’s a Best Practice to follow?
Probably the simplest improvement you could use here is eager loading.
When you load your
Invoiceobjects, if you eager load the associated items and payments, you’ll do 3 queries instead of, well, lots.So, if you have a controller action that does something like this:
you could change it to:
This change should speed things up quite a bit, and doesn’t require that you change your
totalorbalancemethods – they still do the same thing, but fetch all the objects they need at once, instead of a few at a time.Now, this is still (potentially, at least) loading rather a lot of objects into memory. If you’re going to be displaying all sorts of data in that view related to the individual Items and Payments, there’s probably nothing to be done. But if all the view needs are the total and balance values, you can make your database do this for you and skip instantiating the objects, like this:
When you use a custom select clause like this, the extra column get grafted onto the returned objects as attributes with the name of the column, so you can do:
I’ve used different names for these columns so that they don’t overlap with your existing balance and total methods – you may or may not still need those, as these values only exist when the objects they’re called on use the custom select described. If they were loaded without it, trying to call
.remaining_balancewould produce a NoMethodError.Caveats: I’m using PostgreSQL 9.1.3, if you’re not, the above may need to be modified slightly. Also, the adapter has an odd tendency to return these values as strings, so you may need to call
.to_for something similar on them.