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] } } endYou 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
5 comments:
Really nice and helpful one.
Thank you for sharing a comprehensive post on named_scope. Its interesting that using hard coded conditions with date_time may yield wrong results.
Other than date time, I think its fine to use hardcoded values instead of lambdas in named scopes.
Using hard coded values in named scope for dynamic data(which can change over time), will give wrong results. But on the contrary it is efficient for the static values as it is evaluated once on project load.
Very helpful info about named_scope 'wrong results' when date is used without lambda.
Very well said, thanks a lot for sharing. There are many ROR developers in India. Cryptex Technologies is have experts ROR developers. They are having 9 years of experience on ROR and experts in developing web and mobile app. If you need any help from us email at: info@cryptextechnologies.com
Post a Comment