Announcement: associated_named_scope plugin for Rails

by Mechaferret on December 6th, 2009

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

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS