A Rails Feature You Should be Using: with_scope

Posted by ryan
at 5:48 AM on Thursday, July 20, 2006

The with_scope method provided by ActiveRecord has been talked about before (see the resources section below), but I don’t feel like people recognize what a great utility it is. Maybe awareness will increase with more tools that make use of this feature, like the MeantimeFilter by Roman, or just more public conversation about it. Add the following to the latter category:

with_scope lets you bind a block of code operating on an active record model to a particular subset of that model’s collection. For instance, using the standard blog application example, if I have a controller method that performs a series of operations on a single user’s articles I would need to pass in the user id condition on every operation:


def create_avoid_dups

  user_id = current_user.id
  # Find all user's posts
  user_posts = Post.find(:all, :conditions => ["user_id = ?", user_id])

  # Do some logic looking for dups in user_posts
  ...

  # then create new
  @post = Post.create(:body => params[:body], :user_id => user_id)

end

Notice we had to pass in the user_id on both the find and create method. with_scope lets us extract that parameter so the core operations aren’t obscured by excessive parameters:


def create_avoid_dups

  Post.with_scope(:find => {:conditions => "user_id = #{current_user.id}"},
                  :create => {:user_id => current_user.id}) do

    # Find all user's posts
    # No longer need user_id condition since we're in scope
    user_posts = Post.find(:all)

    # Do some logic looking for dups in user_posts
    ...

    # then create new without specifying user_id
    @post = Post.create(:body => params[:body])
    @post.user_id #=> user_id

  end
end

with_scope allowed us to specify conditions of the Post that would apply throughout the course of the block (conditions specified by operation, in this case :find and :create)

Contrived examples such as this one don’t do a great job of showcasing how useful this method is – but imagine never having to specify the user_id in any controller method because it’s been automatically scoped to that user for you. That’s exactly what the previously mentioned meantime filter does.

So don’t be shy. If you find yourself writing code that applies to a known subset of items, scope it with with_scope. All the cool kids are doing it.

Resources

tags: ,

Comments

Leave a response

  1. evanJuly 07, 2006 @ 03:15 AM
    Scoping is great. When you have more than one @.find@ that requires common conditions, it really dries up your code. The biggest benefit is that under scoping, all the fancy active_record tricks like @.find_by_some_field@ still work. I don't know if it will automatically scope associations on objects accessed through @obj.associations@, though. But if, for example, you want to access posts from a group of users, scoping is far more efficient than @[1,4,5,12].inject do {|posts, user| posts += User.find(user).posts}@ (I think that would work).
  2. Michael KoziarskiJuly 07, 2006 @ 03:15 AM
    with_scope is intended for more complicated situations and should only be used as a last resort. The correct, rails-sanctioned way to find the posts for the user is: @user.posts To create a post for that user, use @user.posts.create(params[:whatever]) We have associations for a reason, if you find yourself passing around an id, chances are you've missed a much much simpler option.
  3. SethJuly 07, 2006 @ 03:15 AM
    I see what this feature is trying to do, but I think it makes the intended logic more obscure. To alter your example slightly: user = User.find(current_user.id) user_posts = user.posts @post = user.posts.create(:body => params[:body]) No filtering, no scoping, no hiding my intent.
  4. Michael KoziarskiJuly 07, 2006 @ 03:15 AM
    Just goes to highlight that it's not necessarily a feature that you should be using ;). Cheers Koz
  5. DHHJuly 07, 2006 @ 03:16 AM
    Ryan, I think that's indicative of the real world applicability of this feature. The overwhelming need for with_scope is for it to implement the association scoping. Not for free-range use. Consider it a power-feature for the rare, off case.
  6. BWSJuly 07, 2006 @ 03:16 AM
    Why doesn't with_scope with with :new as well as :create? Create makes a new record in the database and causes validation (which can fail on a brand new record, unless you have all parameters ready to go). Allowing with_scope to work with :new would provide a great way of creating AR objects that are connected to the desired scope.
  7. Ryan DaigleJuly 07, 2006 @ 03:16 AM
    Michael (and Seth), I agree completely about using associations and provided accessor methods instead of passing around ids. My failure was in fabricating a real-world example to showcase @with_scope@ that still used this best practice.
  8. SkizAugust 08, 2006 @ 05:01 PM
    @post = current_user.posts.create!(:body => params[:body])
  9. DiegoAugust 16, 2006 @ 08:42 AM
    bugaga!
  10. RonAugust 19, 2006 @ 12:00 PM
    Is this an example? I have a list of items with user_id's: Users have many items, items belong to users. Sometimes I'll be working with the entire items table, but often I'll be working with one user's items. With either scope, I might want them sorted by color, or by date. Is that the kind of thing with_scope is good for, or am I mildly confused again?
  11. Jeff MFebruary 11, 2007 @ 10:08 PM

    with_scope is high_level access to an options merger, I’ve discovered.

    I’ve been using association proxy extensions a lot lately, and other custom find methods. Here is a common idiom I’ll start with:

    So you have a Employee object and has_many :phone_numbers

    def find_by_phone_number(digits, params={}) self.with_scope(:find => params) do find(:first, :include => :phone_numbers, :conditions => ['phone_numbers.digits = ?', digits]) end end

    So now you could say something like this:

    employee.find_by_phone_number('2135552323', :conditions => ['phone_numbers.created_at > ?', Time.now - 2.weeks])
  12. SoerenApril 22, 2007 @ 07:39 AM

    I was wondering, if there is a reason that nested scopings overwrite all previous parameters by inner rule except :conditions in :find?

    I also want to merge :joins. Therefore I tried to edit the base.rb of Active Record and added following to the with_scope method:

    if key :conditions && merge ... elsif key :include && merge ... elsif key == :joins && merge #—-added myself-- hashmethod = merge_includes(hashmethod, params[key]).uniq

    This works. Have anybody an idea why this isn’t in the rails core?

  13. GarryMay 05, 2007 @ 10:52 AM

    Soeren, I hope to see it in rails core.