Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
How do you use named_scopes?

You may have heard of some problems we’ve had with changes to named_scope in Rails 2.3.

The basic change is that when chaining named scopes together, their scoping does not apply only to the finder class, but also to any lambdas evaluated farther along the named scope chain.

So given a User class with a friends association (pointing at other Users) with the following named_scopes:

named_scope :named_bob, {
  :conditions => {:name => 'bob'}
}

named_scope :second_degree_friends, lambda{|user|
  user_friends = user.friends
  second_degree_friend_ids = user_friends.collect{|u| u.friend_ids}
  {
    :conditions => {:id => second_degree_friend_ids.flatten}
  }
}

These two calls are no longer the same.

User.second_degree_friends(user_sam).named_bob

User.named_bob.second_degree_friends(user_sam)

The first call does what we expect (giving us all of user_sam’s second degree friends who are named bob. But the second call actually gives us something different. Because the named_bob scope comes first in the chain, when it evaluates the lambda for second_degree_friends, it applies it in the scope of all previous named scopes. So our call to the user.friends association is actually scoped with the additional condition of :name => 'bob', which is probably not what we want in this case.

You can see the lighthouse ticket where I claim this should not be the default behavior of named scopes. But my question right now is, “How do you use named scopes?”

I tend to use them in a composable manner, especially in search objects. I take a base finder such as User or User.friends and then I pass it down to a add_conditions or add_sort method. Inside those methods, they add on any other named scopes they need to and return the new finder object. So inside of this chain, you never really know what finders have been applied already, but in the past, you didn’t need to know because the same named_scope with the same parameters always gave you the same conditions.

Often there will be one search object that inherits from another, say for instance LocationUserSearch < UserSearch that adds geo targeted searching on top of UserSearch. In these cases, we can just create our own add_conditions method, call super and tack on any new conditions that we need. Since conditions and joins are merged in scopes, this normally works out great.

Do you use named scopes in a composable way such as this? Or do you only combine them in a known way and might benefit from having the accumulated scope applied to the lambda?

Feel free to add your comments to the lighthouse ticket too.

Comments
  1. Aubrey Holland says:

    On my project we use a ton of composed named scopes and, because we got tired of this sort of problem, we wrote a replacement. [Record Filter](http://aub.github.com/record_filter/) supports one-off searches as well as named filters, both of which work like named scopes but have an easy-to-use syntax and compose correctly. The biggest problem we ran into with named scopes was their habit of throwing away joins if you tried to use more than one in a composition. Record Filter doesn’t do that either.

  2. Joseph Palermo says:

    We had some similar problem with joins a while back. [This patch](https://rails.lighthouseapp.com/projects/8994/tickets/501-merge-joins-instead-of-clobbering-them) from last August changed the behavior to merge identical joins.

    Then we added [this patch](https://rails.lighthouseapp.com/projects/8994/tickets/1077-chaining-scopes-with-duplicate-joins-causes-alias-problem) soon after to allow you to pass joins as an array which then get merged and uniqued correctly.

    :joins => [‘INNER JOIN foo ON foo.id = bar.id’, ‘INNER JOIN baz ON baz.id = bar.id’]

    This pretty much solved all of our join problems.

    Record Filter does look interesting though.

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *