Using set_association_target to Avoid Reloading ActiveRecord Associations

June 12, 2007     

Being smart with ActiveRecord means making sure your code is at least passably efficient with the database. Consider the following code:

class Thing < ActiveRecord::Base
  has_many :photos

class Photo < ActiveRecord::Base
  belongs_to :thing
  def public_filename

@thing = Thing.find(:first, :include => :photos)
@thing.photos[0].public_filename #results in an extra query

You might assume that all of of @@thing@’s photos would automatically have their belongs_to association set when they are accessed through @thing.photos. After all, @thing is right there, there wouldn’t be any extra cost. Unfortunately that does not happen. I’ve dug through the ActiveRecord association code a bit to understand this issue. I’m thinking a fix for this at the core level might be computationally costly, which would explain this behavior. However I’m not yet familiar enough with the ActiveRecord code to quickly analyze the whole situation, much less create a patch.

set_association_target to the rescue

Fortunately I did discover an undocumented public method that allows you to solve the problem easily—if not prettily—in practice. The method is set_association_target where association is replaced by the name of your association. It’s actually pretty similar to association= except that it doesn’t modify anything, it just attaches the object directly to the association, which means that you better know what you are doing when you use it. But since when has Ruby ever assumed you don’t know what you’re doing?

So the fix is:

@thing = Thing.find(:first, :include => :photos)
@thing.photos[0].public_filename #no extra query.

It seems like ActiveRecord ought to be able to figure this out for itself, but for whatever reason, it doesn’t. Sure you can work around the limitation by defining the public_filename method on Thing instead of Photo, but that feels like it negatively affects the whole architecture rather than just uglifying a small snippet. At least that’s my opinion.