Extending the Magazines example, consider that you have a third model, Reader, which has a many-to-many relationship with the Magazine model through a Subscription model:
Class Reader < ActiveRecord::Base has_many :subscriptions has_many :magazines, :through => :subscriptions end Class Subscription < ActiveRecord::Base belongs_to :magazine belongs_to :reader end Class Magazine < ActiveRecord::Base has_many :subscriptions has_many :readers, :through => :subscriptions belongs_to :publisher end Class Publisher < ActiveRecord::Base has_many :magazines endAll nice and clean so far. But imagine you want to do something like output a list of publisher.magazines for a specified reader. For this, you would need to add conditions to an association. Condition for association is simply an SQL snippet which is applied to the LEFT OUTER JOIN (a database way of saying "I need all matching rows from the first table with rows from the second table if they exist") when you are doing find's on the model with :include => <association>,
# Adding condition here applies it to # the WHERE clause of your SELECT # So this line selects publisher's magazines # that specified reader is subscribed to publisher.magazines.find(:all, :include => :subscriptions, :conditions => ["subscriptions.reader_id = ?", reader.id]) # Adding a condition on association applies it # to the left outer join # So this code selects all magazines for the # publisher, including only expired subscriptions Class Magazine < ActiveRecord::Base has_many :expired_subscriptions, :conditions => "subscriptions.expiration <= now()" end publisher.magazines.find(:all, :include => :expired_subscriptions)
However, sometimes this is not sufficient, too :) While you can dynamically specify WHERE conditions, there is no straightforward way to dynamically specify conditions for the LEFT OUTER JOIN (association conditions). So, you can't select all publisher.magazines while only loading subscriptions for a selected reader. This is needed, for example, if you want to show publisher.magazines which the current reader is subscribed to while also showing all the other magazines from this publisher with no subscription details.
There are two solutions, one is non-Railsy and involves writing find_by_sql with a full SQL query, which in this particular example would be very cumbersome. The other solution is to override the association object's conditions dynamically, shown below:
Class Magazine < ActiveRecord::Base # The method which dynamically modifies association's # condition. Can also be added as ActiveRecord::Base # extension, then it will be available for all models def self.with_conditions(assoc, conditions) options = reflect_on_association(assoc).options options[:conditions] = conditions yield end # Finds all magazines sorting reader's magazines # by reader's subscription date def self.find_for_reader(reader) with_conditions(:subscriptions, ["subscriptions.reader_id = ?", reader.id]) do find(:all, :include => :subscriptions, :order => "subscriptions.expiration") end end end # And finally, this call somewhere in controller # achieves our goal of displaying *all* publisher # magazines sorted by selected reader's subscription publisher.magazines.find_for_reader(current_reader)
self.with_conditions basically just adds passed conditions to the specified association, the same thing that specifying :conditions => {...} for the association does, but not limited to static predefined values. The trick is possible with reflect_on_association, a Reflection method which allows us to modify associations in runtime (as any other ruby entity could be).
NOTE: I did not find a way to save and restore association conditions back after the yield call, as restoring options[:conditions] yields the block with the last assigned conditions. So keep in mind that any association used in with_conditions call will have it's conditions changed for the duration of the current request. Fixes accepted :)
This solution is working well in production on Rails 1.2.3-1.2.6