Joseph Palermo's blog
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.
We had an odd bug last week where we ended up with different results after we had eager loaded an association vs loaded directly.
There are apparently two issues with :has_one :through, one of which also applies to :has_many :through.
So given:
class Person
has_many :friendships
has_one :best_friend, :through => :friendships, :conditions => "friendships.best = 1"
end
If you do a Person.find(:all, :include => :best_friend), the best_friend that gets preloaded is not necessarily one that has a "friendship.best = 1"
This is due to a bug in the association preloading code that doesn't pass down the finder options, so any :conditions or :order are completely ignored. This problem is easy to fix, just a one line change, but it then exposes another problem.
This problem applies to both :has_many :through and :has_one :through associations. The problem is that the :through association is loaded separately from the :has_one or :has_many association. So it first loads :friendships, and then when it tries to load :best_friend, it doesn't have the table it needs for the :conditions and explodes.
Our current work around is basically putting the conditions on the :through association, although sometimes you need to create a new association just for that which is certainly not idea, especially if you plan on accessing the :through model after it has been loaded.
The way to fix it in Rails is unfortunately a rewrite of how the :through associations are eager loaded.
