#112
Jun 02, 2008

Anonymous Scopes

The scoped method allows you to generate named scopes on the fly. In this episode I show how to use anonymous scopes to improve the conditional logic which was used in the previous episode on advanced search form.
Download (23.5 MB, 8:49)
alternative download for iPod & Apple TV (12.8 MB, 8:49)

Resources

# config/initializers/global_named_scopes.rb
class ActiveRecord::Base
  named_scope :conditions, lambda { |*args| {:conditions => args} }
end

# models/search.rb
def find_products
  scope = Product.scoped({})
  scope = scope.conditions "products.name LIKE ?", "%#{keywords}%" unless keywords.blank?
  scope = scope.conditions "products.price >= ?", minimum_price unless minimum_price.blank?
  scope = scope.conditions "products.price <= ?", maximum_price unless maximum_price.blank?
  scope = scope.conditions "products.category_id = ?", category_id unless category_id.blank?
  scope
end

RSS Feed for Episode Comments 42 comments

1. Kieran Jun 02, 2008 at 01:49

Nice new features. I upgraded to Rails 2.1.0 earlier. Really easy. Looking forward to using named scopes. Just need to find a nice bit to use it for.

Would you consider giving feedback on some code I've written if you have 5 mins? I'm new to Rails (3-4 weeks) and I've gotten to the point where I have some untidy spots I can't optimize with my current knowledge. Email sent with this comment. Understand if you're too busy.

Keep up the great work.


2. Mig Jun 02, 2008 at 07:24

Ryan, your screencasts get better as your skill increases. It's a lovely thing to watch.


3. James Edward Gray II Jun 02, 2008 at 07:53

I've been thinking about your cleaning up the `scope = scope.scoped …` code, so I'll throw out some ideas.

We can definitely use inject() to remove all of the assignments. I also like the idea of staring with the model class itself, instead of an empty scope. Using just that much, we can build something like:

    ….inject(Product) do |scope, …|
      scope.scoped …
    end

The tricky part here is knowing what to iterate over. Obviously, the find() parameters are needed, but you also only conditionally apply them. That made me want to try something like:

    [ minimum_price.blank?, {:conditions => ["products.price >= ?", minimum_price],
      … ].inject(Product) do |scope, (cond, params)|
      scope.scoped params unless cond
    end

Of course, that's just way too ugly. I haven't come up with the best way to improve that yet though.


4. rawdd Jun 02, 2008 at 08:14

thax


5. Jim Fisher Jun 02, 2008 at 08:48

Whenever I see code like this I think blocks to clean it up.

I'd look at doing something along the lines of:

Product.scoped({}) do |scope|
  scope.conditions "products.name LIKE ?", "%#{keywords}%" unless keywords.blank?
  scope.conditions "products.price >= ?", minimum_price unless minimum_price.blank?
end

def scoped(conditions, &block)
   ....
end

I'll leave it up to everyone else to do the hard work. :)


6. Ryan Bates Jun 02, 2008 at 09:27

@James, interesting idea. I hadn't thought of using inject here.

The problem with just starting with Product instead of Product.scoped is that if no further scopes are applied to it then it will simply return Product, not a scope. This means you can't iterate through the result or treat it like an array as you can with a scope.

@Jim, what kind of object is being passed to the block here? It can't be a normal named scope because calling further named scopes upon it returns a new named scope, it doesn't change the existing scope.

This means we would have to set up a new object which gathers up the called methods and then reapplies them to the scope. That solution seems a bit too complicated.


7. Matt Jun 03, 2008 at 09:59

Any idea how this could be combined with will_paginate?


8. Ben Jun 03, 2008 at 13:37

As always: nice screencast Ryan :)

A suggestion for future screencast(s): Tests. I think this is a very interesting topic, because i think there are a lot of programmers out there not testing at all... There are also a lot of testing frameworks, and it all can be a bit confusing at first...

Just a suggestion ;)

Ben


9. Ryan Bates Jun 04, 2008 at 14:36

@Matt, since we're using scopes here I *think* you can just call ".paginate" on the resulting scope and it will just work. I haven't tested this yet.

@Ben, good point. Testing is something I hope to cover more in future episodes.


10. Taylor luk Jun 09, 2008 at 06:38

Very interesting.. Thanks again ryan..

Will be interesting to see, if future episodes included testing as part of the vidoe..

I know there is bunch of condition builders out there, wouldn't it cool if your named_scope tutorials end up being a simple condition builder implemented on rails 2.1


11. Matthew Higgins Jun 11, 2008 at 17:04

One way to remove the duplication is by making a mixin module that you call like this inside of ProductSearch:

searches Product, {
  :minimum_price => "products.price >= ?",
  :maximum_price => "maximum_price <= ?",
  :category_id => "products.category_id = ?"
}

(There is not much room in this comment window). The module code would have this:
scope = klass.scoped({})
non_blanks = scopes.keys.select { |attr| !send(attr).blank? }
...

You can then use inject or a simple each to build up the scope inside of a loop over 'non_blanks'. To support the 'LIKE' statement, it would need to support lambda.


12. Randal "sw0rdfish" Santia Jun 13, 2008 at 08:25

Hey Ryan,

how could you make this OR instead of AND. For example I want to search some products and I want to search that my keyword is in the name OR the description OR the manufacture OR the distributor etc..

I just made one big find statement, and that's working for me... but I'd like to make it OR OR OR and hten have the ands at the end so that I could to the above, and also price below X amount, etc..

Thanks in advance if you have a chance.


13. Dave Nolan Jun 16, 2008 at 16:03

I'd drop the named_scope on AR::Base and do this:

# using a class variable here for convenience only...

@@searches = {
keywords => "products.name LIKE CONCAT('%', ?)",
minimum_price => "products.price >= ?",
maximum_price => "products.price <= ?",
category_id => "products.category_id = ?"
}

def find_products
@@searches.inject(Product.scoped({})) { |scope, search| search.first.blank? && scope || scope.scoped(:conditions => search.reverse) }
end

(http://pastie.org/216179)


14. Dave Nolan Jun 16, 2008 at 16:08

oops or rather

@@searches = {
:keywords => "products.name LIKE CONCAT('%', ?)",
:minimum_price => "products.price >= ?",
:maximum_price => "products.price <= ?",
:category_id => "products.category_id = ?"
}

etc.


15. Alex Moore Jun 18, 2008 at 17:36

A big issue i've come accross is that performing joins are not merged when using named scopes. In this example where we're doing a complex search, joining to other tables to filter results is really common. Just think of say, tags or particular codes.

Ryan, I noticed you posted in response to:
http://blog.teksol.info/2008/5/26/why-are-activerecord-scopes-not-merged

There aren't any active tickets in lighthouse for this unfortunately.


16. Davis Zanetti Cabral Jun 23, 2008 at 21:43

How to combine this with find_tagged_with from acts_as_taggable_on_steroids??


17. Thomas Maurer Jun 26, 2008 at 14:52

Just some bits I created after watching this episode:

With the presented conditions scope, you can only pass and array. I liked to pass any possible conditions "variant" (e.g. a hash) along and accomplished this with this little hack:

named_scope :conditions, proc { |*args| h = {}; h.store(:conditions, *args); h }

This way you say e.g.: SomeModel.conditions :some_attribute => value


18. jerome Jun 29, 2008 at 23:56

@Davis:

:conditions => "products.id IN (SELECT taggable_id FROM taggings WHERE tag_id IN(?) AND taggable_type = 'Products'", tag


19. jerome Jun 29, 2008 at 23:58

oops, didn't closed the parenthesis after the subselect

tag is a Tag object which will be turned into an integer in this statement.


20. Moustache Jun 30, 2008 at 03:03

I'm having an issue with concatened scopes:

I use Thomas' named_scope :conditions, proc { |*args| h = {}; h.store(:conditions, *args); h }

and I also have many named scopes already defined. I wish I could send a hash to a class method so that parsed arguments are wether sent to the right scope else to the default "conditions" method.

def self.search(*args)
  args.extract_options!.stringify_keys.to_a.inject(scoped({})) do |scope, search|
    scopes.include?(search.first) ? scope.send(search.first, search.last) : scope.conditions(search.first => search.last)
  end
end

But this is really unstable, I can only mix a defined scope and the "conditons" and nothing more.

# this works
User.search( :name => "foo", :city_begins_with => "c")

# this not:
User.search( :name => "foo", :city => "chicago")

# this not either
User.search( :name_begins_with => "f", :city_begins_with => "c")

Any idea ?


21. Davis Zanetti Cabral Jul 05, 2008 at 11:18

@jerome: Thanks! This works great!


22. bill siggelkow Jul 10, 2008 at 11:20

The link to 111 above is actually pointing to 108.


23. Fredrik W Jul 11, 2008 at 05:26

<code>
named_scope :published, {:order => "created_at DESC", :conditions => {:published => true}}
  named_scope :newest, { :order => "created_at DESC" }

def self.search(params)
  params.inject(Video) {|v,val| v.send(val) } == Video.newest.published
end
</code>

Seems to work just as fine, without any need of hacking AR::Base :)


24. Fredrik W Jul 11, 2008 at 05:27

Forgot the call itself,

Video.search(["published","newest"]).

You may also want to check if Video responds_to? the method you're sending and if it's allowed (keep an array of allowed methods or something in the class itself)


25. Fredrik W Jul 11, 2008 at 05:47

The previous code example was really bad since I was so excited, hence a better one follows:

http://pastie.org/231945

Please replace the previous example with this one :)


26. Fredrik W Jul 11, 2008 at 05:50

My bad, seems as if my test cases didn't take into account the joining issues related to named_scopes. The code doesn't seem to work at all. Might be a start though, I'm going to continue digging :(


27. Fredrik W Jul 11, 2008 at 06:03

Alright, finally got it working!

http://pastie.org/231954

Cheers


28. Danimal Jul 16, 2008 at 15:05

Ryan,

Love the episode. One quick question: What is the key-combination in TextMate to do the multi-line block select and replace it with "conditions" where it added that on each line?

I don't even know what you'd call that, otherwise I'd probably be able to figure it out.

Thanks!


29. Matt Aug 01, 2008 at 15:53

I would do something like this:

cons = {:keywords=>["products.name LIKE ?","%#{keywords}%"], :minimum_price=>["products.price >= ?",minimum_price], :maximum_price=>["products.price <= ?",maximum_price], :category_id=>["products.category_id = ?",category_id]}

scope = Product.scoped({})
cons.each{|attribute,carray| scope = scope.conditions carray[0], carray[1] unless send(attribute).blank?}
scope


30. Willem Aug 07, 2008 at 10:56

I just built and released a little Rails plugin that uses this technique to easily search your models using a scope. This can be really useful if you want to combine a search with another scope or will_paginate:

class MyModel < ActiveRecord::Base
  named_scope :my_scope, { ... }
  searchable_on :several, :fields
end

MyModel.my_scope.search_for("search query").paginate( ... )

Get the source at http://github.com/wvanbergen/scoped_search/


31. Matt Aug 07, 2008 at 16:47

One thing I've noticed with these 'scope' methods is that performing count doesn't work if you're selecting from more than one table (eg :from => 'table1, table2') It only uses the first table. It seems I have to use the size method on the returned array since the actual scope is handled properly. Where would I report a bug like this?


32. Yann Aug 18, 2008 at 00:27

Hello, first, thank you Ryan, you are doing a very good job here!

I just want to point out that the link to the "EPISODE 111" LEADS actually to "EPISODE 108".

But many people here are smart enough to get through that issue.


33. Ryan Bates Sep 29, 2008 at 07:20

@Yann, fixed now. Thanks for pointing this out.


34. Todd Miller Oct 21, 2008 at 07:00

Very useful! However, can you use scopes when you have polymorphic associations? The :include method wouldn't work, so is there anything you can do?


35. scooter Oct 22, 2008 at 18:05

this cleans up the code very nice.
useful screencast again ryan
go on with the show - we love it ;-)


36. scooter Dec 03, 2008 at 01:31

Thank you ryan - Another great screencast!


37. Horace Ho Jan 10, 2009 at 01:01

Great tutorial, as usual!

Yes, anonymous scopes work well with will_paginate, as:

scope.paginate(:per_page => 18, :page => page, :order => 'updated_at desc')


38. Erik Mar 02, 2009 at 05:31

Just a small typo:

The notes say scope.conditions where it should be "scope.scoped conditions". I have just used info in the (excellent) screencast but this time I just checked the notes for reference.


39. Erik Mar 02, 2009 at 05:35

Never mind, my memory is dodgy. The syntax in the notes is the refined version. Sorry :-)


40. Alfie Jun 02, 2009 at 17:28

Not sure if this will even get looked at after all those spam comments, but here it is:

Is there any way to add a dynamic order to the "find." For example:

def find_products
scope = Model.scoped({:order => sort_order('attribute_1')})
scope = scope.scoped :conditions => ...
end

def sort_order(default)
"#{(params[:c] || default.to_s)} #{params[:d] == 'down' ? 'DESC' : 'ASC'}"
end

(borrowed from: http://garbageburrito.com/blog/entry/447/rails-super-cool-simple-column-sorting)

My problem is passing "params" to the model - I can't seem to do it. Any help would be appreciated. These screencasts have really helped my ror education, thanks a lot.


41. Omega Jul 05, 2009 at 09:48

Out of curiosity, I'm also interested in seeing how "OR" logic could be implemented here.

This is actually quite clean given countless alternatives! ;)


42. John C Lewis Nov 08, 2009 at 12:54

You are performing a wonderful service. I'm new to rails and web programming but have been building commercial applications for 30 years.

I tried to implement your anonymous scoped search in a seemingly simple search and end up getting:
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.prospects

Extracted source (around line #3):

1: <h1>"Search" </h1>
2:
3: <%= render :partial => @Prospect.prospects %>

It appears the search is not being performed. What am I missing? Do you put any code into the 'show' def in the search controller?

If you do not have time to assist me perhaps you can suggest a consultant I can employ to help.

Thank you...keep up the good work!

John Lewis


43. John Lewis Nov 10, 2009 at 11:41

A bit of a folo-up to my email of 11/8/09...

I found the show form would execute its' select correctly and the select syntax for the partial seems to be correct when tested against the prospects table. Unfortunately I still get:

You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.each

Extracted source (around line #23):

20: <th>Entry</th>
21: </tr>
22:
23: <% for prospect in @prospects %>
24: <tr class="<%= cycle('list-line-odd', 'list-line-even') %>">
25: <td class="list-description">
26: <td><%=h prospect.id %></td>

Suggestions would be very much appreciated.


44. John Lewis Nov 10, 2009 at 12:34

Please disregard my earlier emails. I finally got it working.

Thanks


45. Jon Jan 14, 2010 at 01:41

At what point is the anonymous scope evaluated? put another way, what is the trigger that says "I have finished chaining scopes, now run the query" thanks


46. 绿意春天 Mar 30, 2010 at 01:47

高尔夫T恤![网址= http://www.hzguge.com/hzsy.html]杭州摄影培训[/网址]杭州搬家腻子粉专题最新最实用的化妆学科专家带领的AmyGao专业化妆造型学校,是一所时尚化妆,美发,美甲,高清喷枪化妆遮味剂,作为一种复合的化工产品添加剂,有着广泛的工业用途。由于是一种复配产品,因此没有固定的化学式,分子量。根据遮味剂的工作原理,遮味剂大概。腻子粉资讯企事业单位处理各种应急[网址= http://www.51ggs.com/zhizuo.htm] T恤衫[/网址]最新最实用的化妆学科专家带领的AmyGao专业化妆造型学校,是一所时尚化妆,美发,美甲,高清喷枪化妆遮味剂,作为一种复合的化工产品添加剂,有着广泛的工业用途。由于是一种复配产品,因此没有固定的化学式,分子量。根据遮味剂的工作原理,遮味剂大概。腻子粉资讯提供轴承检测仪提供轴承检测仪[网址= http://www.yt-edu.com.cn/]杭州搬家公司[/网址]。!腻子粉热点[网址= http://www.hzfxbj.cn/]杭州搬家公司[/网址]!腻子粉热点提供轴承检测仪[网址= http://www.hzweiheng 。com /]电机测试系统[/网址]!,最新最实用的化妆学科专家带领的AmyGao专业化妆造型学校,是一所时尚化妆,美发,美甲,高清喷枪化妆遮味剂,作为一种复合的化工产品添加剂,有着广泛的工业用途。由于是一种复配产品,因此没有固定的化学式,分子量。根据遮味剂的工作原理,遮味剂大概。腻子粉资讯![网址=呻/万维网。hzderek.com /]遮味剂[/网址]自成立杭州私人侦探所空白Ť恤等产品高尔夫Ť恤设备吊装[网址= http://www.hzyfzscl.cn/]腻子粉[/网址]专业服务杭州长途搬家。。<br/> [网址= http://www.hzgolden.cn/]杭州私家侦探[/网址]本公司设在杭州最新最实用的化妆学科专家带领的AmyGao专业化妆造型学校,是一所时尚化妆,美发,美甲,高清喷枪化妆遮味剂,作为一种复合的化工产品添加剂,有着广泛的工业用途。由于是一种复配产品,因此没有固定的化学式,分子量。根据遮味剂的工作原理,遮味剂大概。腻子粉资讯![网址= http://www.hzguge.com/]杭州化妆培训[/网址] <br/> ...三替搬家诚信第一[网址= http://www.hzjftm.com/]金银丝[/网址]设备吊装<br/>


47. 绿意春天 Mar 30, 2010 at 01:49

提供轴承检测仪最新最实用的化妝學科專家帶領的AmyGao專業化妝造型學校,是一所時尚化妝、美髮、美甲、高清噴槍化妝 遮味剂,作为一种复合的化工产品添加剂,有着广泛的工业用途。由于是一种复配产品,因此没有固定的化学式,分子量。根据遮味剂的工作原理,遮味剂大概。腻子粉资讯<a href="http://www.hzguge.com/hzsy.html">杭州摄影培训</a>。空调拆装,专业服务杭州长途搬家<a href="http://www.51ggs.com/zhizuo.htm">T恤衫</a>!…企事业单位处理各种应急三替搬家诚信第一<a href="http://www.yt-edu.com.cn/">杭州搬家公司</a>空调拆装本公司设在杭州腻子粉热点三替搬家诚信第一<a href="http://www.hzfxbj.cn/">杭州搬家公司</a>空白T恤等产品<br/>腻子粉价格等各方面内容。…<a href="http://www.hzweiheng.com/">电机测试系统</a>专业服务杭州长途搬家本公司设在杭州,腻子粉热点<a href="http://www.hzderek.com/">遮味剂</a>……,,<a href="http://www.hzyfzscl.cn/">腻子粉</a>设备吊装空调拆装,<a href="http://www.hzgolden.cn/">杭州私家侦探</a>!自成立杭州私人侦探所电机检测仪的使用方法电动车电机检测仪(维修设备)<a href="http://www.hzguge.com/">杭州化妆培训</a>杭州搬家公司电话:0571-85555857杭州三替搬家价格实惠腻子粉热点空调拆装空白T恤等产品<a href="http://www.hzjftm.com/">金银丝</a>。…


48. cheap handbags Jul 20, 2010 at 23:59

thaneks for you email now.and i like your website


49. jordan shoe Jul 25, 2010 at 23:04

 Thank you. I like it.


50. skype phone Aug 04, 2010 at 03:02

your code is very good, I have very good skype phone, we can exchange.


52. drawer slides Aug 08, 2010 at 22:21

Thanks for this great post. I have become a huge fan of this website and I really cant wait to read you next posts! Your post are inspirational.


53. booster-cable Aug 08, 2010 at 22:23

It's a very meaningful activity. Looking forward to joining you.


54. free directory list Aug 11, 2010 at 22:27

If God would exists it will be you... very thanks for this screencast.


55. jordan retro shoes Aug 20, 2010 at 22:55

have become a huge fan of this website and I really cant wait to read you next posts! I really enjoy watching the RailsCasts. I think type of site that is useful in sharing information and it is important to share. very thanks for this screencast.


56. louis vuitton shoes Aug 26, 2010 at 21:17

Thanks for sharing your article. I really enjoyed it. I put a link to my site to here so other people can read it. My readers have about the same interets


57. Wholesale Electronics Aug 27, 2010 at 00:53

Discount Wholesale Electronics, Wholesale Cell Phones, Electronic Gadgets and More from the Best Dropship Wholesaler


58. mbt shoe uk Aug 30, 2010 at 01:31

Hey, thanks for the insightful article. Please do keep up the good work!


59. snow boots Aug 31, 2010 at 00:19

I have just used info in the (excellent) screencast but this time I just checked the notes for reference.


60. louis vuitton sunglasses Sep 01, 2010 at 21:17

Good article! Thank you so much for sharing this post.Your views truly open my mind.

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player
Give Back to Open Source