ActiveRecord goodness

For once, ActiveRecord actually surprised me in a good way tonight. I'm used to pretty much the opposite happening: ActiveRecord is great for doing extremely simple stuff, but as soon as you need to do anything mildly sophisticated you find yourself dropping down to raw SQL:

SELECT topics.id, topics.title, topics.comments_count, topics.view_count, topics.updated_at, topics.last_comment_id,
       users.id AS last_active_user_id,
       users.display_name AS last_active_user_display_name
FROM topics
LEFT OUTER JOIN users ON (users.id = topics.last_commenter_id)
WHERE topics.forum_id = ? AND public = TRUE AND awaiting_moderation = FALSE AND spam = FALSE
ORDER BY topics.updated_at DESC
LIMIT ?, ?

Well, tonight the unexpected happened and ActiveRecord actually did something I'd never really expected it to do before. I had an "N+1 SELECT" problem (a page which was performing an additional database query for each record shown on the page), and was thinking about how I would solve the problem by manually fetching all the required records in one hit.

I had a model that looked something like this:

class Post < ActiveRecord::Base
  has_many                :comments,
                          :as         => :commentable,
                          :extend     => Commentable,
                          :order      => 'comments.created_at',
                          :dependent  => :destroy
...

And the Commentable extension referenced above had methods like this:

module Commentable
  def published
    find :all, :conditions => { :awaiting_moderation => false, :spam => false, :public => true }
  end
end

And finally, a view which accessed those comments by referencing @post.comments.published. On rendering the view there would be one query to get the comments:

SELECT * FROM `comments`
WHERE (`comments`.commentable_id = 10 AND `comments`.commentable_type = 'Post' AND
    (`comments`.`public` = 1 AND `comments`.`awaiting_moderation` = 0 AND `comments`.`spam` = 0))
ORDER BY comments.created_at

And then one query like the following to retrieve the user for each and every comment:

SELECT * FROM `users` WHERE (`users`.`id` = 23)

So for 10 comments that would be 10 additional queries; and for 20 comments it would be 20 queries.

Imagine my delight when on adding an :include parameter to my Post/Comment association ActiveRecord automatically did the right thing, transforming those multiple user queries into a single:

SELECT * FROM `users` WHERE (`users`.`id` IN (23,1,19,54))

Which is pretty much exactly what I would have done manually. I think this is the first time I've been in "N+1" SELECT situation and Rails has done what I would have done myself!

← Site enhancements
Fuzzyfinder →

All blog posts