Rails 2.1 has_many :through Conditions Regression
Pulled our application up to Rails 2.1 yesterday. This is the first major release since we moved off of edge as of Rails 2.0, so the set of major features is pretty exciting this time around. One less visible change is a bunch of work optimizing the the
:include option, best explained by Fabio.
The first thing I want to say about this is that it’s awesome. The performance problems with either multiple includes or the infamous n + 1 problem were both potentially severe and fairly intractable. The workarounds were not pretty. That said, I think a fair number of people are going to experience breakage due to the fact that what used to be one query is now potentially multiple queries, and there’s no good way for ActiveRecord to detect or fix what broke between 2.0 and 2.1.
In our case we had one broken query which was remedied with the following advice. I’m addressing the specific case here, but it’s just one very tiny edge case among a large set of tiny edge cases.
Don’t Put Conditions Affecting the Join Tabled into has_many :through
Here’s something which used to work that doesn’t anymore:
class Film < ActiveRecord::Base has_many :memberships has_many :directors, :through => :memberships, :source => :cast_member, :conditions => "memberships.role = 'Director'" end Film.find(:all, :include => :directors)
The problem here is that the memberships table is no longer available when it is loading the cast_members (directors), so the specified conditions throw a mysql error.
I gave some thought to fixing this, but the solution would be worse than the problem. Solving just the simple case would require passing around a lot of extra parameters and lists of tables. Start considering compound conditions, variations in SQL, and proving correctness of the whole mess and you get some sense of why ORM is best kept simple and
find_by_sql is your friend.
So instead I just did this:
has_many :directorships, :class_name => "Membership", :conditions => "role = 'Director'" has_many :directors, :through => :directorships, :source => :cast_member
The lesson here is that association conditions should only reference fields from the target table. Maybe this was already obvious, but if you really need to depend on multiple tables then the
:finder_sql is the recommended solution.