named_scope
named_scope(name, options = {}, &block)Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query, such as :conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions.
class Shirt < ActiveRecord::Base named_scope :red, :conditions => {:color => 'red'} named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true] end
The above calls to named_scope define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, in effect, represents the query Shirt.find(:all, :conditions => {:color => 'red'}).
Unlike Shirt.find(...), however, the object returned by Shirt.red is not an Array; it resembles the association object constructed by a has_many declaration. For instance, you can invoke Shirt.red.find(:first), Shirt.red.count, Shirt.red.find(:all, :conditions => {:size => 'small'}). Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; Shirt.red.each(&block), Shirt.red.first, and Shirt.red.inject(memo, &block) all behave as if Shirt.red really was an Array.
These named \scopes are composable. For instance, Shirt.red.dry_clean_only will produce all shirts that are both red and dry clean only. Nested finds and calculations also work with these compositions: Shirt.red.dry_clean_only.count returns the number of garments for which these criteria obtain. Similarly with Shirt.red.dry_clean_only.average(:thread_count).
All \scopes are available as class methods on the ActiveRecord::Base descendant upon which the \scopes were defined. But they are also available to has_many associations. If,
class Person < ActiveRecord::Base has_many :shirts end
then elton.shirts.red.dry_clean_only will return all of Elton’s red, dry clean only shirts.
Named \scopes can also be procedural:
class Shirt < ActiveRecord::Base named_scope :colored, lambda { |color| { :conditions => { :color => color } } } end
In this example, Shirt.colored('puce') finds all puce shirts.
Named \scopes can also have extensions, just as with has_many declarations:
class Shirt < ActiveRecord::Base named_scope :red, :conditions => {:color => 'red'} do def dom_id 'red_shirts' end end end
For testing complex named \scopes, you can examine the scoping options using the proxy_options method on the proxy itself.
class Shirt < ActiveRecord::Base named_scope :colored, lambda { |color| { :conditions => { :color => color } } } end expected_options = { :conditions => { :colored => 'red' } } assert_equal expected_options, Shirt.colored('red').proxy_options
15Notes
Passing find() arguments
I you need to pass additional arguments to a scope (e.g. limit), do this:
Shirt.colored('red').all(:limit => 10)
Use lambda to avoid caching of generated query
If you're using a named_scope that includes a changing variable you need to wrap it in a lambda to avoid the query being cached and thus becoming unaffected by future changes to the variable, example:
named_scope :translated, :conditions => { :locale => I18n.locale }
Will always return the same locale after the first hit even though I18n.locale might change. So do this instead:
named_scope :translated, lambda { { :conditions => { :locale => I18n.locale } } }
Ugly, but at least it's working as we expect it...
acts_as_state_machine named scopes
If you are using the acts_as_state_machine plugin, this will generate all named scopes for your various states.
Place it after the acts_as_state_machine and state declarations.
class Task < ActiveRecord::Base
acts_as_state_machine :initial => :waiting
state :waiting
state :running
state :finished
states.each { |s| named_scope s, :conditions => { :state => s.to_s } }
end
Then doing a Task.waiting will return the corresponding tasks.
Passing optional arguments with defaults to a named_scope
An easy way to do this. (This also shows how you can use joins in a named_scope as well.)
Class User << ActiveRecord::Base
belongs_to :semester
named_scope :year, lambda { |*year|
if year.empty? || year.first.nil?
{ :joins => :semester, :conditions => ["year = #{CURRENT_SEMESTER}"]}
else
{ :joins => :semester, :conditions => ["year = #{year}"]}
end
}
end
You can then call:
User.year # defaults to CURRENT_SEMESTER constant
User.year() # same as above
User.year(nil) # same as above; useful if passing a param value that may or may not exist, ie, param[:year]
User.year(2010)
Extract the aggregated scoping options
If you want to get the aggregated scoping options of a chain of named scopes use ActiveRecord::Base.current_scoped_methods
It works in the fashion of:
Shirt.red.medium.alphabetical.current_scoped_methods
# ==>
{
:create => {},
:find => {
:conditions => {:color => 'red', :size => 'medium'},
:order => 'shirts.name ASC'
}
}
How using Array methods
It's not possible to use Array methods with a scope because it's not an Array but an ActiveRecord::NamedScope::Scope :
this Article.promotion.sum(&:price) doesn't run.
But you can use the to_a method to transform the ActiveRecord::NamedScope::Scope to an Array :
Article.promotion.to_a.sum(&:price)
Generating empty conditions
In some cases, you might find it useful for your lamba to generate empty conditions based on the passed parameter.
Class Article << ActiveRecord::Base
named_scope :category, lambda { |cat|
if cat == :all
{ :conditions => {} }
else
{ :conditions => { :category_id => cat } }
end
}
end
Allows you to call something like this:
categories = user_is_admin ? :all : @current_category
Article.category(categories)
Mostly useful when chaining named_scopes together. Avoids more complicated if statements.
AASM named scopes
If you are using the aasm plugin/gem, this will generate all named scopes for your various states.
==== Code example
Class Article < ActiveRecord::Base
include AASM
aasm_initial_state :created
aasm_state :published
aasm_state :unpublished
aasm_state :deleted
aasm_state :created
aasm_event :publish do
transitions :to => :published, :from => [:created]
end
aasm_event :unpublish do
transitions :to => :unpublished, :from => [:created, :published]
end
aasm_event :delete do
transitions :to => :deleted, :from => [:published, :unpublished]
end
aasm_states.each { |s| named_scope s, :conditions => { :state => s.to_s } }
end
named_scopes and Acts as State Machine
As of AASM 2.1 named_scope(s) are defined by default for each state of a model.
Testing Named Scopes
Thanks for the example of testing named_scopes. Being new to Rails, I struggled to find examples that I could understand. Here is another "simple" test for a named_scope
Mine differs slightly from the one above in that I had to remove a set of {} in the :conditions in my test to avoid an "odd number list for Hash" error. I also replace the param-binding "?" with the number I expect to send in as an argument. My test would did know what args[0] was. I got an "undefined local variable" error.
The named scope in my model:
named_scope :up_to_and_including_year, lambda{ |*args| {
:conditions => ["to_char(grad_dt1,'YYYY') <= ?", args[0]]
}}
The test:
test "named_scope :up_to_and_including_year" do expected_options = { :conditions => ["to_char(grad_dt1,'YYYY') <= ?", '2010'] } assert_equal expected_options, Sso::CourseTaken.up_to_and_including_year('2010').proxy_options end
using joins, group, having
==== Code example
named_scope :red, :joins => [:color_lis => :color], :group => "color.id", :having => ["color.xx IS NULL"]
A named_scope also responds to model class methods
for instance
class Student < ActiveRecord::Base
named_scope :sophomore, :conditions => 'year=2'
def self.eligible_to_vote
select{|s| s.age >= 18}
end
end
ss = Student.sophomore.eligible_to_vote
Anyone know the order the scopes assemble conditions?
It seems like last scope = first condition in sql. Can anyone confirm?
Remember, named_scope returns an array
named_scope always returns a named_scope object, that acts like an array, even if you're using it to only find one record. So if you're trying to perform an association on the results of a named_scope, use the first method to return the model object and not the named_scope object.
Ie:
user = User.my_name_scope
user.articles # assuming User has_many Articles
will return an error. use this instead:
user = User.my_named_scope.first
user.articles
(Of course this is a poor example because what you should be doing is performing the named_scope on Article with user as the condition, instead of on User. But if you do need to use the results of a named_scope to perform an association call, you have to do it this way to avoid an error.)
some gotchas
Works
named_scope :public, :conditions => "public = true"
Works
PUBLIC_CONDITIONS = "public = true"
named_scope :public, :conditions => SomeModel::PUBLIC_CONDITIONS
Works
named_scope :public, lamba { {:conditions => SomeModel.public_conditions} }
def self.public_conditions
"public = true"
end
Doesn't work
named_scope :public, :conditions => SomeModel.public_conditions
def self.public_conditions
"public = true"
end