L-exp Mobile

Investigating named_scope

This morning I spent quite a while going through the source code that implements named_scopes in ActiveRecord 2.1. The code is short and kind of interesting in itself, so give it a read if you're interested! Kudos to rails-core for getting this in.

What I wanted to achieve is being able to write code like:

1 milk = Product.find_by_name('milk') 2 customer.orders.placed.since(1.week.ago).for(milk)

Using vanilla named_scopes works fine for most of the above, but I ran into a gotcha when I tried to add a non-database-based named scope.
I had something like this:

1 class Cart < ActiveRecord::Base 2 named_scope :placed, {:conditions => ["status IN (?)", ["Ordered", "Closed"]} 3 named_scope :since, lambda{|date| {:conditions => ["created_at >= ?", date]} } 4 end

With that in place I can do:

1 customer = Customer.first 2 customer.orders.placed.since(1.week.ago)

Sweet.

Now I needed:

1 beef = Product.find(123) 2 customer.orders.placed.since(1.week.ago).for(beef)

If "for" could have been solved by some SQL I could have added it as another bunch of conditions. In my case it's not that simple though ("products" sit on LineItems, and are grouped by categories that are considered equivalent if X, Y and Z are all true and the stars are correctly aligned...) and I need some ruby code in there to check what product a given order was for.

You can extend named_scopes just like with has_many associations, by passing a block or using the ":extend" option:

1 class Cart < ActiveRecord::Base 2 named_scope :placed, {:conditions => ["status IN (?)", ["Ordered", "Closed"]} 3 named_scope :since, lambda{|date| {:conditions => ["created_at >= ?", date]} } do 4 def for(product) 5 # stuff 6 end 7 end 8 end

And here's the gotcha: the scope inside for() changes if you call the named_scope and the :extended method from the class or from a has_many association, i.e.:

1 Order.placed.since(1.week.ago).for(beef)

doesn't give you the same "self" as:

1 customer.orders.placed.since(1.week.ago).for(beef)

In actuallity, it's not "self" that is different, but the "proxy_scope". In the former case proxy_scope is the Order class, in the latter it's the Array of orders placed by our customer.

Not what I expected.
:-(

To make it work for both I had to do:

1 collection = proxy_scope.is_a?(Array) ? proxy_scope : self.send(:load_found)

'load_found' is the private instance method on ActiveRecord::NamedScope::Scope (the "self" inside the for() method above) that actually goes out and retrieves the collection for all the named_scopes.

Not so pretty, but working.

Final version:

1 class Cart < ActiveRecord::Base 2 named_scope :placed, {:conditions => ["status IN (?)", ["Ordered", "Closed"]} 3 named_scope :since, lambda{|date| {:conditions => ["created_at >= ?", date]} } do 4 def for(product) 5 collection = proxy_scope.is_a?(Array) ? proxy_scope : self.send(:load_found) 6 collection.reject do |order| 7 # crazy stuff to find out if it's in our out 8 end 9 end 10 end 11 end

Options:   Save This | Share
Viewed 0 times
Published 3 months ago
By dpalm
From Resource Elc Tech Blog in lists:
Best Ruby on Rails Blogs

Menu

by Genís