Named Scopes and Default Scopes
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.