Tuesday, March 25, 2008

RoR: Caching Dynamic Association Conditions

The problem which is verbosely described in a previous post on dynamic associations still does not have a clean solution - I've been researching possible workarounds, and there is no definite answer. One feasible workaround is specifying :conditions => 'send(:method)' in single quotes, this way Rails will only eval the conditions when forming the SQL string. This, and what was suggested in my previous post, both would work... Unless you would want to reuse that association with different (or no) conditions later on.

And here comes the trouble - Rails preloads all model classes on startup, and any changes that your controller actions do to the model classes stay cached for all further requests. So on production system, modifying an association in run-time would affect all future references through that association (and that's in fact what has happened to me). We better restore the conditions back after modifying them in with_conditions method. But if your Rails app is anything complicated, it probably uses delayed loading when rendering the views, so you can not reset association conditions right away in the model. Let's delay it until the next request:
class ActiveRecord::Base
 @@saved_conds = Hash.new

 # dynamically modify conditions as there is no other way in Rails 
 # to specify run-time conditions on joins...
 def self.with_conditions(assoc, conditions)
   @@saved_conds[self.to_s] ||= Hash.new
   # only save if that's the first call during this request
   @@saved_conds[self.to_s][assoc] ||=
   reflect_on_association(assoc).options[:conditions] = conditions

 # reset association conditions if any has been modified by our
 # with_conditions calls in the previous request
 def self.reset_conditions
   return if @@saved_conds.empty?
   @@saved_conds.each { |klass,associations|
     associations.each { |assoc,saved|
       model = klass.constantize
       model.reflect_on_association(assoc).options[:conditions] = saved
   @@saved_conds = Hash.new
This solution I came up with is pretty hacky - but it works. If you know of a better way to mark a class for reload in Rails, let me know. This code saves modified conditions in a hash by class and association and then you would need to add code to restore them to a mint condition in a before_ or around_ filter in ApplicationController (call like Magazine.reset_conditions). Using class name as a Hash parameter since class variable behavior in Ruby is strange to say the least - it is shared in all inherited classes and the parent class :)