views:

297

answers:

3

I have a model along the lines of:

class Account < ActiveRecord::Base

  has_many :payments
  has_many :purchases

  def balance
    payments.sum(:dollar_amount) - purchases.map{|p| p.dollar_amount}.sum
  end

end

I want to memoize the balance method and store it in memcached. The problem, of course, is that the cached value needs to get expired any time a payment or purchase is created. I could insert code in the after_save callbacks of payments and purchases to expire the cached balances of their accounts but it seems to me that it would be easier to understand/maintain if I could say something like:

cached_memoize :balance, :depends_on => [:payments, :purchases]

Is there an existing gem/plugin that does this? And before I go off and write my own, is it a good idea? The downside that I see is that it might make it less obvious to somebody who is modifying the dollar_amount method of Purchase that they need to take into account a caching issue (if they unwittingly introduced a dependency on another model, like SubPurchase or something, it would screw things up.) But since this isn't super obvious anyway, I think that having a neat declarative syntax is worth it - at least that way when it breaks, it's clear how to fix it.

Thoughts?

Edit: in response to semanticart's answer I will be more explicit about my problem with the "just put the expires in the relevant callback" approach - the problem is that you end up with expires all over the codebase - it starts with the after_save callback on payment, but maybe it's in a separate observer for purchases, and then you have polymorphic associations, inheritance trees, etc. The syntax I'm proposing forces developers to keep all these cases in a neat list in one place. That way when you get a bug report like "users balances sometimes are out of sync and they aren't quite sure how to replicate the issue" it is a lot easier to figure out what is going on.

+1  A: 

I'd consider another approach: have a balance field on the account. Use callbacks (after_save, etc.) on the Purchase and SubPurchase models to update the balance field on the parent Account. Your balance only changes when the other models are modified and you never have to worry about it being stale.

semanticart
That is pretty much the same thing as doing it in memcached (except more expensive) so I think that my reasoning about the advantages of the centralized declarative syntax would still hold. I didn't really spell it out in the original post but my problem with that type of approach is you end up not knowing where all of those update / expires come from - i'll edit now to be more clear.
Sam
OK sorry I guess really what you were getting it is that if I wanted to update the balance without recalculating it from scratch, the pattern I'm suggesting simply wouldn't work. In this case that's not a concern but point taken.
Sam
It isn't even about avoiding recalculating it from scratch. If you implement callbacks, then you'll never have to worry about that number being stale because the Purchases and SubPurchases will (for all intents and purposes) clear the cached balance by themselves. In this case, the cached value just happens to be a field on Account.A nice side-effect of this is that you can query based on balance. Say you want to sort by highest to lowest balance on the back-end: without a cached column, you have to make sure you first calculate it for each account.
semanticart
I'm not trying to be a jerk and I appreciate that you're taking the time to help me but did you read the question thoroughly?
Sam
A: 

sounds to me like you want to fork cache_fu, and add an option that magically sprinkles the after_saves across the dependent records. I dig having your dependencies in 1 place.

jdwyah
A: 

Not sure this is what you are looking for but it may help.

http://railscasts.com/episodes/137-memoization

gregf