Lately everyone has been going mad over a relatively new addition to rails: named_scope. Up until now, I hadn't had the chance to try it out. I always just assumed that it was more syntactic fluff that seems to accumulate. Wow, was I wrong! It's an unbelievably cool and useful idea. Here's a really simple example. Say I have 2 models: Country and Region. They are as you would expect. A Region belongs to a Country and a Country has many Regions, etc. I use them pretty much for dropdown lists and things like that. Here's some code:
- class Region < ActiveRecord::Base
- belongs_to :country
- end
- class Country < ActiveRecord::Base
- has_many :regions
- end
class Region < ActiveRecord::Base
belongs_to :country
end
class Country < ActiveRecord::Base
has_many :regions
end
Prior to named_scope, if you wanted to get all Regions in alphabetical order (as an example), you'd have to do something like:
- class Region < ActiveRecord::Base
- #other code
- def self.ordered
- find(:all, :order => "name ASC")
- end
- end
class Region < ActiveRecord::Base
#other code
def self.ordered
find(:all, :order => "name ASC")
end
end
Which was ok, but this is nicer:
- class Region < ActiveRecord::Base
- named_scope :ordered, :order => "name ASC"
- #other code
- end
class Region < ActiveRecord::Base
named_scope :ordered, :order => "name ASC"
#other code
end
This creates a method on the Region class called ordered that can simply be called.
OK, so that's cool and all, but really not that big a deal, right? Well, no - until you consider that you can chain calls to named_scope, thus adding more and more constraints. For example:
- class Region < ActiveRecord::Base
- belongs_to :country
- named_scope :ordered, :order => "name ASC"
- named_scope :by_country, lambda { |c| { :conditions => ["country_id = ?", c.id] } }
- named_scope :containing, lambda { |s| { :conditions => ["name like ?", "%#{s}%"] } }
- end
- class Runner
- c = Country.find_by_name("Canada")
- regions = Region.by_country(c).containing("A").ordered
- puts "Provinces in Canada containing the letter A in ascending order: #{regions.inspect}"
- end
class Region < ActiveRecord::Base
belongs_to :country
named_scope :ordered, :order => "name ASC"
named_scope :by_country, lambda { |c| { :conditions => ["country_id = ?", c.id] } }
named_scope :containing, lambda { |s| { :conditions => ["name like ?", "%#{s}%"] } }
end
class Runner
c = Country.find_by_name("Canada")
regions = Region.by_country(c).containing("A").ordered
puts "Provinces in Canada containing the letter A in ascending order: #{regions.inspect}"
end
This generates SQL that looks like this:
SELECT * FROM `regions` WHERE ((name like '%A%') AND (country_id = 1)) ORDER BY name ASC
Another reason that I like name_scope is that because models such as these are frequently eused in dropdowns, etc, you can use named scope to return a very lightweight version of the class, while excluding attributes that are unnecessary for a simple dropdown. For example:
- class Region < ActiveRecord::Base
- belongs_to :country
- named_scope :ordered, :order => "name ASC"
- named_scope :by_country, lambda { |c| { :conditions => ["country_id = ?", c.id] } }
- named_scope :containing, lambda { |s| { :conditions => ["name like ?", "%#{s}%"] } }
- named_scope :simple, :select => "id, name"
- end
class Region < ActiveRecord::Base
belongs_to :country
named_scope :ordered, :order => "name ASC"
named_scope :by_country, lambda { |c| { :conditions => ["country_id = ?", c.id] } }
named_scope :containing, lambda { |s| { :conditions => ["name like ?", "%#{s}%"] } }
named_scope :simple, :select => "id, name"
end
This allows me to only bring back the attributes that I need. And again, it could be chained with other named scopes
I think that this is a very powerful concept that allows developers to write more DRY and readable code.