Skip to content

Instantly share code, notes, and snippets.

@latentflip
Created November 18, 2011 13:19
Show Gist options
  • Save latentflip/1376443 to your computer and use it in GitHub Desktop.
Save latentflip/1376443 to your computer and use it in GitHub Desktop.
Before save and active record association aggregations

So today I learned that if you have:

class Invoice < ActiveRecord::Base
  has_many :transactions
  before_save :cache_transaction_total

  def cache_transaction_total
    self.transaction_total = transactions.sum(:total) || 0
  end
end

class Transaction < ActiveRecord::Base
  belongs_to :invoice
end

And you open a rails console and do something innocuous like:

$ i = Invoice.create

The query that aggregates the transactions in the before_save callback is something like:

SELECT SUM("transactions"."total") AS sum_id 
FROM "transactions" WHERE "transactions"."invoice_id" IS NULL

Which rather than aggregating all the (non-existant) transactions associated to the new invoice, aggregates all the transactions which aren't associated to an invoice at all - which gives a very different result. In my case, most transactions are not listed to an invoice so instead of caching 0 it caches something more like 1,000,000.

I guess the solution is to run the update in an after_save callback instead -- but does this behaviour not seem completely different to what we would expect?

Thoughts?

@lenary
Copy link

lenary commented Nov 18, 2011

The invoice won't have an id or be in the db at all until after it's saved... hence you're going to have to be a bit cleverer about how you do it, or stick it in after_save (with the fun that you're going to need to save in after_save and make sure you don't go all inception-mode on the code).

Sam

@latentflip
Copy link
Author

Yeah I guess I see why it's happening I just thought that AR would catch that the id was null and not try and do the aggregation. After all, if you do

$ i = Invoice.new
$ i.transactions

it doesn't go ahead and

SELECT * FROM transactions WHERE transactions.invoice_id IS NULL

@lenary
Copy link

lenary commented Nov 18, 2011

actually on second thoughts, how about just have it in before_update... that would make more sense... (and stops you having to think about inception-callbacks). S

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment