Easier Association Proxies
A few months ago, I wrote about association extensions and how to create more dynamic finders from your associations. While this method is nice, there are other ways to offer the same/similar functionality while inheriting a bit more functionality.
The association methods (i.e., has_many, belongs_to, etc.) take an argument called :conditions. While this might be seen as only somewhat useful from the outside, when paired with the :class_name argument, it really shines through. Take this association extension for example:
has_many :actions do
find(:all, :conditions => 'status = 1')
end
end
end
The code above provides us with the following functionality, in our controllers and views:
<!– In our view –>
Open Actions
Briefly, the association extension causes Rails to execute the find method inside a with_scope call which puts the database query inside the scope of the "actions" association. In other words, it's joining the actions table with the people table before it issues the query.
While association extensions are great, we can go a bit further when we have an extension like our example, using the :class_name and :conditions arguments I talked about earlier. Our new code looks like this:
has_many :open_actions, :class_name => 'Action', :conditions => 'actions.status = 1'
end
We've now created an entirely new association. The :class_name argument tells Rails that we don't actually want it to try and find an OpenAction class, but that we're instead altering the Action class we have with some new conditions. Our view code now looks like this:
<!– In our view –>
Open Actions
You might wonder why we'd ever want to do something like this. There are a couple reasons:
- Readability. I prefer keeping my code clean, and I opt for this method over association extensions where I can.
- We now have a true association. That might not mean much to you, but I'll show you in a minute why it can be nice to have.
True associations are important–to me–for a few reasons. First, we can now do this when finding people from our application:
@people = Person.find(:all, :include => :open_actions)
end
end
That code wasn't possible using association extensions, because we weren't creating a true association, and as such, we couldn't use it to eager load only our open actions: If we know which items we want from the database, there's no reason to give Rails more than it needs. It takes time to parse all the results returned from the database, so it behooves us to get only what we need when we are eager loading.
A little less important, but still nice, is the functionality we get in testing (and potentially in our code, but moreso in testing). If you're testing something where you need to test the value of an item, then run some code, and test the item again, that item may be cached if it's an item inside an association. Since we're now dealing with a true association, we get the force_reload option for free. With an association extension, you have to manually add that, and it can get bothersome.
One of the downsides of association proxies like this is when you start getting fancy and eager loading at the declaration level, as such:
belongs_to :status
end
has_many :open_actions, :class_name => 'Action', :include => :status, :conditions => "statuses.name = 'Open'"
end
If you try to eager load the open_actions association in a call to Person.find, you'll get an error that Rails can't find the status.id column. This is because the :include argument in the has_many call doesn't get carried over into the scope of the query. If you need to do something like this, consider the following:
has_many :open_actions, :class_name => 'Action', :conditions => 'actions.status_id = 1'
end
It's not very pretty, but it's better than nothing.