Rails 2.1 has_many :through Conditions Regression

June 4, 2008     

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'"

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.

Christoph says…
June 4, 2008 at 9:47PM

Thanks for the hint