Sickpea

Named Scopes and Default Scopes

Tuesday, 7 July 2009

Rails 2.1 introduced named scopes which basically allow you to give a set of finder criteria a name. For instance, you may only want to find / operate on published articles, where published_at is nil. Instead of adding that condition to all your find calls, you can define that named scope directly in your model:

app/models/articles.rb — Named scope

class Article < ActiveRecord::Base
  named_scope :published, :conditions => 'published_at IS NOT NULL'
  ...
end

Article.all
  # => SELECT * from `articles`
Article.published
  # => SELECT * from `articles` WHERE (published_at IS NOT NULL)

Nice. There's a whole lot more you can do with named scopes if you're interested. Another cool feature introduced in Rails 2.3 is called default scoping. When I'm fetching articles for this site, I almost always want to get the most recently published entries first. Default scopes let you do just that:

app/models/articles.rb — Default scope

class Article < ActiveRecord::Base
  default_scope :order => 'published_at DESC'
  named_scope :published, :conditions => 'published_at IS NOT NULL'
  ...
end

Article.published.first
  # => SELECT * FROM `articles` WHERE (published_at IS NOT NULL)
  #      ORDER BY published_at DESC LIMIT 1

Default scoping is powerful but can be dangerous when provoked with complicated :condition clauses.

I emphasized almost a bit earlier because there are situations when you don't want the default scope. The "next article" feature is one of those. My first naïve attempt:

app/models/article.rb — Broken attempt #1

class Article < ActiveRecord::Base

  def next_article
    @next_article ||= Article.first(:conditions => [ 'published_at > ?',
      published_at ])
  end

end

current.next_article # wrong order
  # => SELECT * FROM `articles` WHERE (published_at > '2009-06-19 07:00:00')
  #      ORDER BY published_at DESC LIMIT 1

If you're looking at Article 1, the next article should be 2. This code gives the most recently published article — the default ordering should be ascending. So can we just specify the :order explicitly?

app/models/article.rb — Broken attempt #2

class Article < ActiveRecord::Base

  def next_article
    @next_article ||= Article.first(:conditions => [ 'published_at > ?',
      published_at ], :order => 'published_at ASC')
  end

end

current.next_article # bad order by
  # => SELECT * FROM `articles` WHERE (published_at > '2009-06-19 07:00:00')
  #      ORDER BY published_at ASC, published_at DESC LIMIT 1

That doesn't work either because default scopes are merged into your existing conditions / order by clauses: they do not replace them. This make sense. So how do you disable the default scope completely? Enter with_exclusive_scope, stage left:

app/models/article.rb — With exclusive scope

class Article < ActiveRecord::Base

def next_article
  @next_article ||= Article.send(:with_exclusive_scope) do # send(:hack)
    Article.first(:conditions => [ 'published_at > ?', published_at ],
      :order => 'published_at ASC')
  end
end

current.next_article # success
  # => SELECT * FROM `articles` WHERE (published_at > '2009-06-19 07:00:00')
  #      ORDER BY published_at ASC LIMIT 1

Sweet! Except for the send hack. We can't call Article.with_exclusive_scope directly because it is a protected method. That's usually a sign you're doing something unintended or wrong or both. It makes your code fragile because now it's dependent on Active Record internals which may change in the future. I don't like it, but I'm going to keep it for now and we'll see if it comes back to bite me.

Archives

Thu, 2 Jul 2009

Canonical URLs in Rails

Sat, 11 Jul 2009

Bootstrapping Development Databases

Hi, I'm Adrian (@sickp).

I like to build things: websites, games, robots, and mobile apps. I'm a software tinkerer and an MIT-approved engineer (i.e. they can ask me for money.)

During the day I help build fine games at Wonderhill, and lend my expertise to other Ooga Labs companies. In my spare time, I create useful iPhone apps at Zooble with my wife, Alexandra.

You should follow me on Twitter and subscribe to this site's RSS feed.

© 1988-2010 Adrian B. Danieli. Some rights reserved.