November 16th, 2009Merging named scopes without performing a query
It all started when I tweeted:
wish I could get hold of a chain of named_scopes without performing any query (which means it can’t be dynamically composed) #activerecord
My team has currently been performing some refactorings on our Rails app and one of our goals is to move complex find queries into more composable/meaningful/maintainable named scopes. Contrary to my expectations, when you call a named scope, Active Record performs a find(:all)
query implicitly as you finish your statement:
class Shirt < ActiveRecord::Base named_scope :red, :conditions => {:color => 'red'} named_scope :clean, :conditions => {:clean => true} end >> red_proxy = Shirt.red Shirt Load (0.3ms) SELECT * FROM "shirts" WHERE ("shirts"."color" = 'red') +----+-------+-------------------------+-------------------------+-------+ | id | color | created_at | updated_at | clean | +----+-------+-------------------------+-------------------------+-------+ | 5 | red | 2009-11-16 18:36:37 UTC | 2009-11-16 18:36:37 UTC | false | | 6 | red | 2009-11-16 18:36:37 UTC | 2009-11-16 19:15:18 UTC | true | +----+-------+-------------------------+-------------------------+-------+ 2 rows in set >> red_proxy.clean Shirt Load (0.4ms) SELECT * FROM "shirts" WHERE (("shirts"."clean" = 't' AND "shirts"."color" = 'red')) +----+-------+-------------------------+-------------------------+-------+ | id | color | created_at | updated_at | clean | +----+-------+-------------------------+-------------------------+-------+ | 6 | red | 2009-11-16 18:36:37 UTC | 2009-11-16 19:15:18 UTC | true | +----+-------+-------------------------+-------------------------+-------+ 1 row in set >> |
So, even though I can compose named scopes, I can’t defer the query to the point I define which operation to execute. I’m obviously not the first one who had this problem, but it seems like this won’t be fixed until a new solution is proposed in Rails 3.
In our particular case, we didn’t even need the proxy object, but just a hash representation of the composed query parameters. We’re using ActiveRecord::Extensions (ar-extensions) to perform some UNION queries to aggregate results, and it expects a list of hashes that represent each query. It would be nice if I could grab the hash from my named scopes without performing the query…
The documentation points to a proxy_options
method that you can use to test your named scope, but it didn’t solve my problem, since it only returns the hash for the last scope in your chain:
>> Shirt.red.proxy_options => {:conditions=>{:color=>"red"}} >> Shirt.red.clean.proxy_options => {:conditions=>{:clean=>true}} |
The solution, after digging a while through the source code, was to use an internal method that’s used during the merge algorithm, called current_scoped_methods
:
>> Shirt.red.clean.current_scoped_methods[:find] => {:conditions=>"("shirts"."clean" = 't' AND "shirts"."color" = 'red')"} |
This is not a perfect solution, but saved me the trouble of performing the merge myself. I hope the Rails 3 solution addresses these issues and that this post saves me some time digging through ActiveRecord source code if I need this before then :)