named scope?
Nick Kallen’s has_finder plugin has found it’s way into rails 2.x as named_scope. Named scope is all about scoping or narrowing the database query.
Let me go through an example to make things easy.
class Post < ActiveRecord::Base
belongs_to :author, :class_name => 'User'
has_many :comments, :dependent => :destroy
named_scope :published, :conditions => { :is_published => true }
named_scope :draft, :conditions => { :is_published => false }
named_scope :has_comment, :conditions => [ 'comments_count > 0' ]
named_scope :recent, lambda { |*date| {:conditions => { :created_at => date.first || 1.weeks.ago } } }
named_scope :before, lambda{ |date| { :conditions => ['created_at > ?', date] } }
end
You can
scope your query and make it more readable.
Post.published #equivalant to Post.all.published
You can also scope on finder methods like,
Post.find(:all, :conditions => { :created_at => 1.weeks.ago }).published
You can use finder on scope like,
Post.published.find(:all, :conditions => { :created_at => 1.weeks.ago })
Even scope on the relations.
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :author, :class_name => 'User'
named_scope :unread, :conditions => {:is_unread => true}
named_scope :read, :conditions => {:is_unread => false}
end
So now you can write like,
Post.find(id).comments.unread
Chaining
Now would not it be wonderful if you could write like “Get all posts which are recently published” and it would return you what it means.
Using named_scope that dream will come true ;). You can finally write code like this,
Post.recent.published
You can chain up to your hearts content ;). You can do all sort of combination like,
Post.recent.published.has_comment
Which will return you posts which are recently published and has comments. See how much readable the code has become.
Passing Arguments
You can pass arguments using lambda.
Post.published.before(1.days.ago)
Named Scope Extensions
Extend named scope and add methods to it.
class Post < ActiveRecord::Base
named_scope :draft, :conditions => { :is_published => false } do
def publish
each { |post| post.update_attribute(:is_published, true) }
end
end
end
Post.recent.draft.publish
This will find all recent draft posts and publish them.
Anonymous Scopes
Creating named scopes on the fly.
# Store named scopes
published = Post.scoped(:conditions => { :is_published => true })
recent = Post.scoped(:conditions => { :created_at => 1.weeks.ago })
# Which can be combined
recent_published = recent.published
Why use named scope?
- Increases readability
- Reduces number of queries as it can be chained
- Very helpful for dynamic queries(like using pagination or dynamic filter)
A common issue
Named scope is a class methods. And you should always be very careful for class methods. Here is an example where most people does it wrong.
class Post < ActiveRecord::Base
belongs_to :author, :class_name => 'User'
has_many :comments, :dependent => :destroy
named_scope :recent, :conditions => { :created_at => 1.weeks.ago }
end
Now I can call Post.recent and expect to get all Posts within one week. But in times you will find that this query is starting to give wrong results.
Why???!!!
Named scope is a class method and so 1.weeks.ago is evaluated when the class was loaded. Now it does not matter when you call the named scope it will always run the query with the same created_at date. We also made this mistake :( and learned from it :)
A lambda block is not evaluated when the class is loaded. So there is a work around it.
class Post < ActiveRecord::Base
belongs_to :author, :class_name => 'User'
has_many :comments, :dependent => :destroy
named_scope :recent, lambda { |*date| {:conditions => { :created_at => date.first || 1.weeks.ago } } }
end
Taking the lambda input like this will give you flexibility of not providing the parameter at all. For now ruby does not provide facility to take default parameter for lambda.
Deep inside named scope
The implementation of Named scope is a beauty of rails. Although the result that named scope produces seems like an Array, but actually it is not an Array but a simulation of an Array. Calling a named scope over a finder object actually returns a ActiveRecord:NamedScope:Scope object. When we chain scopes on that object, appends the query logic in the scope object. The query is not evaluated until an Array method(like each, first, last) etc is called. And when these Array methods are called the scope object then evaluates the query and delegates the method which was called as an array to the real array that has been evaluated. Now you can not chain the scope any more, as it is already been evaluated. That is how the named scope magic works in ruby on rails.
Reference
http://ryandaigle.com/articles/2008/3/24/what-s-new-in-edge-rails-has-finder-functionality
http://railscasts.com/episodes/108-named-scope
http://apidock.com/rails/ActiveRecord/NamedScope/ClassMethods/named_scope