History
The overwhelming usefulness of named_scope is common knowledge in the Rails community. Named scopes are so useful that you want to use them everywhere, and that’s when you begin to encounter one of their frustrations: while they are quite easy to write and use, they are not always so easy to re-use.
Most of the time, reuse is provided by the (also overwhelmingly useful) chaining feature of named scopes. If you want to use a named scope in an association (has_one, has_many, or belongs_to), you can simply define the association and then chain the scope to it, optionally defining a method to access it if you will be using it often. For example, if you have a named scope published_in_month on the Post class that finds all posts published in a given month, and the User class has_many posts, then the code to find all posts for a given user written in a given month is simply
user.posts.published_in_month(month)
If you want to combine two named scopes together, again, you can just chain them (again optionally defining a new method — in this case a class method). If Post has another named scope with_comments that returns posts with comments, then the following code finds all posts published in a given month that have comments:
Post.published_in_month(month).with_comments
So far, so very good. But what if you want to define a named scope that reuses a named scope not for the given class, but for one of its associations? To be more precise, what if you have a named scope published_in_month on the Post class that finds all posts published in a given month, and the User class has_many posts, and you want to write a named scope on User to find all users who have posts published in a given month? Sure, you can do
(Post.published_in_month(month).collect {|post| post.user}).uniq
but that’s a little harder to read — and is not itself a named scope, and thus doesn’t get executed as a single query and doesn’t have the nice features of named scopes such as chaining.
This problem came up emphatically in some code I was writing recently. In our system, many objects belongs to a user, a user has many addresses that include a travel radius, and an address may be within the range of a given lat/lng point. The SQL to calculate whether an address is in-range is pretty messy, as you’d expect, and it was showing up all over the code. I refactored to put it in a named scope in_range on Address, but what I really needed was a named scope to find all objects whose addresses (through their user) were within range of a given specific address, and there wasn’t a clean way to reuse the named scope on Address. And from that need was born this plugin, associated_named_scope.
Note: While looking around online to see if there was already such a thing available, I discovered that this request had been made before [discussion now moved here]. This plugin doesn’t exactly provide the syntax as requested there, but it does provide the same functionality.
The plugin
associated_named_scope is a Rails plugin that, quite simply, allows (re)using named scopes on child associations to define named scopes in the parent classes. It is available at
http://github.com/Mechaferret/associated_named_scope
Usage
associated_named_scope adds a new option :association to named_scope, which expects two suboptions:
- :source — the source association for the child named scope. You can specify nested associations using standard hash syntax (e.g., {:user=>:addresses} for the addresses of the user of the current model).
- :scope — the named scope itself. You can specify arguments to the scope by using an array [:scope_name, arg1, ...]
These scopes chain and in every way behave just like normal named_scopes.
Examples
Here’s a simple example to find all users who have posts published in a given month:
class Post < ActiveRecord::Base
belongs_to :user
named_scope :published_in_month, lambda{|month|
{:conditions=>["month(posts.publication_date)=?", month]}}
end
class User < ActiveRecord::Base
has_many :posts
named_scope :has_posts_for_month, lambda{|month|
{:association=>{:source=>:posts, :scope=>[:published_in_month, month]}}
}
end
And here’s a more complex example, using a nested association:
class Address < ActiveRecord::Base
belongs_to :addressable, :polymorphic => true
named_scope :in_range, lambda {|lat, lng, zip_code|
{:conditions =>
"(
(POW(addresses.radius,2) >=
(POW(69.1*(#{lat}-addresses.lat),2) +
POW(69.1*(#{lng}-addresses.lng) * cos(#{lat} / 57.3), 2)
)
)
OR (addresses.zip = '#{zip_code}')
)"
}}
end
class User < ActiveRecord::Base
has_one :object_with_user
has_many :addresses, :as => :addressable
end
class ObjectWithUser < ActiveRecord::Base
belongs_to :user
named_scope :within_range, lambda{|address|
{:association=>{:source=>{:user=>:addresses},
:scope=>[:in_range, address.lat, address.lng, address.zip]}}}
end
