Wow, this looks really cool. I especially like the efficiency of the generated SQL query when you concatenated the named_scopes. Really amazing and definately a great feature for bigger, data-filled applications.
Foo:
I would guess that yes, pretty much all finds will be replaced by named scopes in near future. Think of your own experience and some of Ryan's previous screencasts: You shouldn't really have finds anyway as soon as you're using find conditions. Like, say, you'd have find_all_activated instead of find(:all, :conditions => "activated_at IS NOT NULL")
Mauricio:
As far as I can see in the source, there's not too much going on metaprogramming-wise. So I guess it's safe to say that scoping doesn't cause performance issues. And as Marcel points out, named scopes produce really clean and efficient SQL - so no problem there either. Plus, scopes have existed before in Rails (with_scope - see here: http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M001416).
Ryan:
Best episode so far, in my opinion. And not only because it was so long! ;-)
@Adam, I doubt you can use this with acts_as_ferret, but other plugins which end up calling "find" should work. For example, will_paginate just adds a custom paginate class method. You can append this to the search. (Untested).
Product.recent.paginate(:page => 1)
@Foo, this definitely lowers the need to create custom "self.find..." methods. However I would hesitate getting too fancy here. If you have a complicated search which needs to dynamically build up the conditions based on the passed parameters, then I wouldn't use named_scope for this.
@Mauricio, to echo Clemens, I doubt performance will be much of an issue. There's probably a slight overhead on the ruby side to get the scoping stuff figured out, but beyond that it seems efficient.
I have been using edge rails as of late just for this amazing functionality. In addition to the new level of dynamism this feature adds to Rails, it also adds such simplicity.
My favorite has to be simply the Model.all, Model.first, and Model.last shortcuts... cleans up my controllers for sure!
Man, with each new update of Rails, so many new possibilities for refactoring!
I have been trying to do some of the same things using with_scope and some of them were not working correctly (I have an idea why now after watching your screencast) and now it will be so much easier to do what I was trying to do. Thanks for telling me about this new feature!
I've been using the has_finder gem on which this is based for some time now, and it's really useful. One thing I still struggle with has to do with using named_scopes between models in associations.
In your example you are using 'visible' which is just a simple attribute. What if category had more advanced attributes, like hidden, private, public, read_only and stuff like that. Say Category already has some named_scopes, for example 'free_for_all' which means a category is public but not read-only.
Now it would make sense (maybe not in this example but in general) to be able to use these named_scopes in the Product model, not on instances only, but in named_scopes themselves as well. So Product.in_free_for_all can just use the 'free_for_all' scope in Category. Otherwise you would have to define this 'logic' twice, which isn't very DRY. I hope my explanation is clear enough :)
Of course I know that I could also use Category.free_for_all.map(&:products), but then all products get fetched right away which means sorting them (and other nice stuff finders have) will have to be done in ruby instead of in the DB.
A solution I use at the moment is this, but I'm not fully satisfied with it yet, so any comments are very welcome...
class Product << ActiveRecord::Base
belongs_to :category
named_scope :in_free_for_all, lambda {{ :conditions => {:category_id => Category.free_for_all.map(&:id)} }}
end
Which will do 2 queries, which is default for :include in rails 2.1 anyway I believe. I could use :select => 'id' to speed up the query a bit since I only need id's, but in the end, it would be nicest if rails could somehow write a subquery for it. Along the lines of:
@Mathijs, currently one named scope can't use another named scope, but I think many people are wanting this feature. There's already a couple tickets along this line:
This was a really great Railscast highlighting probably my favorite new feature of Rails. Those SQL queries are great! Can't wait to take advantage of this...
Hello Ryan (or anyone else who knows),
Great screencast. In your demo, you are typing the commands in the console and showing the SQL queries in the console below. How do you isolate the SQL Queries display in the window below? It is quite useful. A screencast summarizing the tips on using Rails Console for these kinds of tricks would be very useful.
Keep up the good work.
Regards,
Bharat
@Bharat: you could just keep a window with ``tail -f log/development.log'' open, that should show the queries. I prefer to have them in the same window though, and have explained how to achieve this here:
http://skionrails.wordpress.com/2008/05/22/loading-additional-files-when-starting-scriptconsole/
On my website there is a page to view standings for a so called soccer pool. There are member records with a name per pool, that can be left empty. Each member is linked to a user record with a name field as well. In case the name of the member is empty, the user's name is used in the standings.
When using named_scope, I see strange behavior, since the eager loading isn't quite as I expected. I guess things have been changed in Rails 2.1 in this area that have not necessarily have to do with named_scope.
Anyway, in my Member class this scope doesn't work:
The query doesn't join with the users table, so a missing column error is returned. But when I add :conditions => 'users.id = users.id', it works! So is Rails looking in the conditions to see if a associated table is used?
Anybody see my points and have some clues/comments?
hi ryan, great job as usual. quick question for you... when you are working around the inability to pass in a default value for the lamdba, you splat args and then in the proc you use "args.first || 2.weeks.ago"...
my question is why? why not just leave the argument to the lambda as "t" and then in the proc use "t || 2.weeks.ago" ?
seems to work for me, but i'm wondering if you had some other rationale for using the splat/first method.
You can also pass in a params hash into the named scope and use for example params[:newer_than] || 2.weeks.ago for the default parameter. I prefer this solution over using args when having more than one parameter as the named scope call becomes easier to understand and parameter order doesn't matter.
You didn't try the case where 2 named_scope have a block of code. In your screencast only :recent ha a block of code, but what about chaining it with :cheap that also had a block of code?
If anyone's interested, I've created a plugin which automagically generates common named scopes for all AR models and columns. See http://github.com/zubin/autoscope
Thanks so much for all your work. Question - I'm seeing that
scope = Product.scoped({})
is indeed doing a find which is undesired in my case. I'm on Rails 2.1. I'd like to build up the scopes before executing. Do I need to use your scope_builder?
I have been trying to get the named scopes to work in my model without any success.
I have model called Contact.
And my named scope is like the follow:-
class Contact < ActiveRecord::Base
named_scope :guardians, {:conditions =>["relationship = ?", 'Guardian']}
And in the IRB:-
>contact = Contact.find(2)
>contact.guardians
gives the following error:
NoMethodError: undefined method `gaudians' for #<Contact:0x636d4e4>
from F:/Projects/WorkshopProjects/testproject/vendor/rails/activerecord/lib/active_record/attribute_method
s.rb:256:in `method_missing'
from (irb):7
I am not sure what is going wrong.
I even tried using lambda as follows, but same thing happens:-
named_scope :test_guardians, lambda { { :conditions => ['relationship = ?', 'Guardian'] } }
I'm such a fan of named scopes now and see so many opportunities to streamline code. I ran into a problem. How can I create a named scope that uses an association and a lambda expression for parameterization. For example how would you retrieve all the products that are from a certain category type, for example 'electronics'. When I try something analagous to this I get an error: wrong number of arguments (3 for2).
Since this was not clearly stated in text form I thought I would include this little snippet from the documentation on named_scope:
-----------------------
Unlike Shirt.find(...), however, the object returned by Shirt.red is not an Array; it resembles the association object constructed by a has_many declaration. For instance, you can invoke Shirt.red.find(:first), Shirt.red.count, Shirt.red.find(:all, :conditions => {:size => ‘small’}). Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; Shirt.red.each(&block), Shirt.red.first, and Shirt.red.inject(memo, &block) all behave as if Shirt.red really was an Array.
Wow, and I thought working with the DB couldn't get any easier, but this is seriously cool. Thanks for this excellent overview.
I wonder if these named scopes can be used in conjunction within existing search plugins like acts_as_ferret in one line...
Another great screencast.
Does this essentially make all self.finds obsolete? Will the new conventions be named_scope instead of def self.find_some_criteria?
Thanks for these Rails 2.1 tips.
Very interesting screencast ! Cool new feature ! Thanks for this tip !
What about the performance cost of this "magic"?
Wow, this looks really cool. I especially like the efficiency of the generated SQL query when you concatenated the named_scopes. Really amazing and definately a great feature for bigger, data-filled applications.
Thanks for another great screencast!
Foo:
I would guess that yes, pretty much all finds will be replaced by named scopes in near future. Think of your own experience and some of Ryan's previous screencasts: You shouldn't really have finds anyway as soon as you're using find conditions. Like, say, you'd have find_all_activated instead of find(:all, :conditions => "activated_at IS NOT NULL")
Mauricio:
As far as I can see in the source, there's not too much going on metaprogramming-wise. So I guess it's safe to say that scoping doesn't cause performance issues. And as Marcel points out, named scopes produce really clean and efficient SQL - so no problem there either. Plus, scopes have existed before in Rails (with_scope - see here: http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M001416).
Ryan:
Best episode so far, in my opinion. And not only because it was so long! ;-)
@Adam, I doubt you can use this with acts_as_ferret, but other plugins which end up calling "find" should work. For example, will_paginate just adds a custom paginate class method. You can append this to the search. (Untested).
Product.recent.paginate(:page => 1)
@Foo, this definitely lowers the need to create custom "self.find..." methods. However I would hesitate getting too fancy here. If you have a complicated search which needs to dynamically build up the conditions based on the passed parameters, then I wouldn't use named_scope for this.
@Mauricio, to echo Clemens, I doubt performance will be much of an issue. There's probably a slight overhead on the ruby side to get the scoping stuff figured out, but beyond that it seems efficient.
@Ryan:
in Ruby 1.9 you can use -> operator for lambda and put default values, like this:
named_scope :recent, ->(time = 2.weeks.ago) { {:conditions => ["released_at > ?", time]} }
I've been using has_finder for a while now, and it's been a real pleasure to work with. I'm really glad it's made it into rails.
I have been using edge rails as of late just for this amazing functionality. In addition to the new level of dynamism this feature adds to Rails, it also adds such simplicity.
My favorite has to be simply the Model.all, Model.first, and Model.last shortcuts... cleans up my controllers for sure!
Man, with each new update of Rails, so many new possibilities for refactoring!
I have been trying to do some of the same things using with_scope and some of them were not working correctly (I have an idea why now after watching your screencast) and now it will be so much easier to do what I was trying to do. Thanks for telling me about this new feature!
Indeed a very cool screencast.
Thanks again for this great episode!
I've been using the has_finder gem on which this is based for some time now, and it's really useful. One thing I still struggle with has to do with using named_scopes between models in associations.
In your example you are using 'visible' which is just a simple attribute. What if category had more advanced attributes, like hidden, private, public, read_only and stuff like that. Say Category already has some named_scopes, for example 'free_for_all' which means a category is public but not read-only.
Now it would make sense (maybe not in this example but in general) to be able to use these named_scopes in the Product model, not on instances only, but in named_scopes themselves as well. So Product.in_free_for_all can just use the 'free_for_all' scope in Category. Otherwise you would have to define this 'logic' twice, which isn't very DRY. I hope my explanation is clear enough :)
Of course I know that I could also use Category.free_for_all.map(&:products), but then all products get fetched right away which means sorting them (and other nice stuff finders have) will have to be done in ruby instead of in the DB.
Any 'nice' solutions for this?
A solution I use at the moment is this, but I'm not fully satisfied with it yet, so any comments are very welcome...
class Product << ActiveRecord::Base
belongs_to :category
named_scope :in_free_for_all, lambda {{ :conditions => {:category_id => Category.free_for_all.map(&:id)} }}
end
Which will do 2 queries, which is default for :include in rails 2.1 anyway I believe. I could use :select => 'id' to speed up the query a bit since I only need id's, but in the end, it would be nicest if rails could somehow write a subquery for it. Along the lines of:
(example, doesn't work)
named_scope :in_free_for_all, :conditions => {:category => Category.free_for_all}
should translate to:
Select * from products where category_id in (select id from categories where public = 't' and read_only = 'f')
Any chance of getting behavior like this in the near future?
@Mathijs, currently one named scope can't use another named scope, but I think many people are wanting this feature. There's already a couple tickets along this line:
http://rails.lighthouseapp.com/projects/8994/tickets/57
http://rails.lighthouseapp.com/projects/8994/tickets/223
However, neither of these mention using an association's scope. You may want to add a comment to those tickets about that.
Thanks for a great post! My attention tends to drift after about 7 minutes, so i'll have to come back and watch again a couple times :)
This was a really great Railscast highlighting probably my favorite new feature of Rails. Those SQL queries are great! Can't wait to take advantage of this...
Thanks!
Wow! Truly awesome new feature!
Now I just need to get Hostgator to update to 2.1....
Hello Ryan (or anyone else who knows),
Great screencast. In your demo, you are typing the commands in the console and showing the SQL queries in the console below. How do you isolate the SQL Queries display in the window below? It is quite useful. A screencast summarizing the tips on using Rails Console for these kinds of tricks would be very useful.
Keep up the good work.
Regards,
Bharat
@Bharat: you could just keep a window with ``tail -f log/development.log'' open, that should show the queries. I prefer to have them in the same window though, and have explained how to achieve this here:
http://skionrails.wordpress.com/2008/05/22/loading-additional-files-when-starting-scriptconsole/
Hope that helps. Great work as always Ryan :)
On my website there is a page to view standings for a so called soccer pool. There are member records with a name per pool, that can be left empty. Each member is linked to a user record with a name field as well. In case the name of the member is empty, the user's name is used in the standings.
When using named_scope, I see strange behavior, since the eager loading isn't quite as I expected. I guess things have been changed in Rails 2.1 in this area that have not necessarily have to do with named_scope.
Anyway, in my Member class this scope doesn't work:
named_scope :standings, :include => :user, :order => 'members.position, COALESCE(members.name, users.name)'
The query doesn't join with the users table, so a missing column error is returned. But when I add :conditions => 'users.id = users.id', it works! So is Rails looking in the conditions to see if a associated table is used?
Anybody see my points and have some clues/comments?
Hi
im getting this:
named_scope > NoMethodError
ruby-v
ruby 1.8.5 (2006-08-25) [i486-linux]
rails-v
Rails 2.1.0
gem1.8 list
*** LOCAL GEMS ***
actionmailer (2.1.0, 2.0.2)
actionpack (2.1.0, 2.0.2)
activerecord (2.1.0, 2.0.2)
activeresource (2.1.0, 2.0.2)
activesupport (2.1.0, 2.0.2)
jruby-openssl (0.2.3)
rails (2.1.0, 2.0.2)
rake (0.8.1)
my problem / question: as soon as i have a named_scope declaration within the class, im experiencing the error above ( NoMethodError)
what am i doing wrong/missing?
thx tom
never , i figures it out, its working now on the console.as a newbie i forgot to chnage envrinment.rb
thx
hi ryan, great job as usual. quick question for you... when you are working around the inability to pass in a default value for the lamdba, you splat args and then in the proc you use "args.first || 2.weeks.ago"...
my question is why? why not just leave the argument to the lambda as "t" and then in the proc use "t || 2.weeks.ago" ?
seems to work for me, but i'm wondering if you had some other rationale for using the splat/first method.
You can also pass in a params hash into the named scope and use for example params[:newer_than] || 2.weeks.ago for the default parameter. I prefer this solution over using args when having more than one parameter as the named scope call becomes easier to understand and parameter order doesn't matter.
hey Ryan, still working on that image trouble. I'll let you know when i get it sorted out.
had a tricky scope question though.
Is it possible to order a find call my an associated model's attribute.
e.g.
@category.members.order_by_members_businesses_names do |member|
i would just do
@category.businesses.order_by_name do |business|
but i the member model has addtional info i need to access.
named_scope :order_by_members_businesses_names ...?
So would this replace the need to use the scope out plugin?
@chris ("why not just leave the argument to the lambda as "t" and then in the proc use "t || 2.weeks.ago" ?"):
It's so you can leave the arg out -- it fakes an optional parameter.
See:
http://pastie.org/258863
This is so easy, it's great! Thanks for the walkthrough.
Hi,
You didn't try the case where 2 named_scope have a block of code. In your screencast only :recent ha a block of code, but what about chaining it with :cheap that also had a block of code?
I can't make it work.
If anyone's interested, I've created a plugin which automagically generates common named scopes for all AR models and columns. See http://github.com/zubin/autoscope
>> Page
=> Page(id: integer, name: string, content: text, enabled: boolean, category_id: integer, created_at: datetime, updated_at: datetime)
...creates these (for example):
>> Page.enabled
>> Page.not_enabled
>> Page.named "foo"
>> Page.content_contains "bar"
>> Page.created_before 1.year.ago
Install:
./script/plugin install git://github.com/zubin/autoscope.git
Feedback welcome!
Thank you very much for this screencast.
Your tip about working with optional arguments saved my live :-)
Really great screencast again Ryan
Thanks again for this great episode!
Just what I have been looking for!
Hi Ryan,
Thanks so much for all your work. Question - I'm seeing that
scope = Product.scoped({})
is indeed doing a find which is undesired in my case. I'm on Rails 2.1. I'd like to build up the scopes before executing. Do I need to use your scope_builder?
Thanks!
Per comment #44 by me:
I was in the console when testing this. Appears to be ok when running in Rails.
Sorry for any confusion. Still seems a bit weird though.
Hi,
I have been trying to get the named scopes to work in my model without any success.
I have model called Contact.
And my named scope is like the follow:-
class Contact < ActiveRecord::Base
named_scope :guardians, {:conditions =>["relationship = ?", 'Guardian']}
And in the IRB:-
>contact = Contact.find(2)
>contact.guardians
gives the following error:
NoMethodError: undefined method `gaudians' for #<Contact:0x636d4e4>
from F:/Projects/WorkshopProjects/testproject/vendor/rails/activerecord/lib/active_record/attribute_method
s.rb:256:in `method_missing'
from (irb):7
I am not sure what is going wrong.
I even tried using lambda as follows, but same thing happens:-
named_scope :test_guardians, lambda { { :conditions => ['relationship = ?', 'Guardian'] } }
Can you help me out here ?
Thanks in advance.
-Arjun
I have been trying to get the named scopes to work in my model without any success. How to do next?
Ryan, or any other expert,
I'm such a fan of named scopes now and see so many opportunities to streamline code. I ran into a problem. How can I create a named scope that uses an association and a lambda expression for parameterization. For example how would you retrieve all the products that are from a certain category type, for example 'electronics'. When I try something analagous to this I get an error: wrong number of arguments (3 for2).
named_scope :retrieve_by_type, :include=>:category, lambda{|type_id| {:conditions=>['categories.type'=type_id]}
-Thanks in advance
Hi,
i'm new, how can i see generated queries like you do in the bottom terminal ?
Since this was not clearly stated in text form I thought I would include this little snippet from the documentation on named_scope:
-----------------------
Unlike Shirt.find(...), however, the object returned by Shirt.red is not an Array; it resembles the association object constructed by a has_many declaration. For instance, you can invoke Shirt.red.find(:first), Shirt.red.count, Shirt.red.find(:all, :conditions => {:size => ‘small’}). Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; Shirt.red.each(&block), Shirt.red.first, and Shirt.red.inject(memo, &block) all behave as if Shirt.red really was an Array.
Could it be possible to have a named_scope that include columns from another association?
something like:
named_scope :my_fields,:select => "..."
or maybe another way?
Thanks
that was most excellent.
Excellent, now I understand this. Thanks
Is named_scope deprecated now in Rails 4.0? 'scope' still seems to work though
yup it is
use scope :name, ->{stuff}
http://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html#method-i-all