Preserving with_scope on eager-loaded Associations

(I’m just gonna start posting things now. Please excuse the suddenness of this article. –brendan)

As web developers, one of the first places we turn to when tuning performance of pages that include a lot of data is to minimize the number of queries to the database needed to render a page. In Rails, we use the “eager loading” facility provided by the :include option as in Article.find(:all, :include => :author), the upshot being that the object represented by the author association is retrieved by the same query that retrieves all of the Article objects.

One of the other niceties of Rails is the ability to apply a scope to restrict the result set for operations conducted on a model within a block, such as (contrived example, just go with me here:)

Article.with_scope :find => {:conditions => ['published = ?', true]} do
   @author = User.find(user_id)
   @author.articles.each do |article|
     ... show an article or something ...
   end
end

The problem comes when we combine eager loading with expectations that the scope defined using with_scope is being applied properly:

Article.with_scope :find => {:conditions => ['published = ?', true]} do
   @author = User.find(user_id, :include => :articles)
   @author.articles.each do |article|
     ... show an article or something ...
   end
end

In the above example, the :include => :articles bit eager-loads ALL of the articles into the resulting User object @author and we wind up showing non-published articles in the ensuing loop. The reason for this is that the sql generated by ActiveRecord does not honor scopes for eager loading.

Here is some code that changes this.

ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.class_eval do

  def association_join_with_scoped_eager_loading
    # Don't bother to create the subquery unless we have a finder scope to apply.
    return association_join_without_scoped_eager_loading unless reflection.klass.class_eval{scope :find}
    "#{association_join_without_scoped_eager_loading} AND " <<
    "#{aliased_table_name}.#{reflection.klass.connection.quote_column_name(reflection.klass.primary_key)} IN " <<
    "(#{reflection.klass.__send__(:construct_finder_sql,{:select => 
      "#{reflection.klass.table_name}.#{reflection.klass.connection.quote_column_name(reflection.klass.primary_key)
    }"})})"
  end

  alias_method_chain :association_join, :scoped_eager_loading

end

So what’s going on here? What we’re basically doing is creating a subquery that returns all of the ids for the class referenced by the association, to match against the id for the records we are eager-loading, ensuring that we only eager-load associated records that are properly scoped. The negative performance impact of this approach depends on your database engine and the complexity of the scope you are applying.

Hello World v2008

Greetings, programs!

Its been more than half a year since I pulled this site down for renovations and much has happened. Firstly, I have moved on to another position, this time as lead developer for a web startup, which has resulted in the happy benefit of massive power-ups in my understanding and proficiency with Ruby/Rails. Due to the nature of the software we’ve been developing, I’ve had to concoct many extensions for Rails that I’ve been extracting and maintaining as plugins, now hosted here at Usergenic.com.

I haven’t had a chance to document them all yet, but this will be coming shortly. In the meantime, feel free to poke around the public subversion repository and let me know if you find anything useful or have any questions about making it work for you. I’ll be documenting and writing some tutorials soon, and will be very interested in incorporating feedback and patches. If there is enough interest I may migrate the subversion hosting to Rubyforge.

Anyways, it feels good to be back online and I’m looking forward to rejoining the conversation with the development community at large.