What's New in Edge Rails: Has Finder Functionality

Posted by ryan
at 11:37 AM on Monday, March 24, 2008

It looks like Nick Kallen’s wildly popular has_finder plugin will be making its way into Rails 2.x in the form of named_scope. Observe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class User < ActiveRecord::Base
  named_scope :active, :conditions => {:active => true}
  named_scope :inactive, :conditions => {:active => false}
  named_scope :recent, lambda { { :conditions => ['created_at > ?', 1.week.ago] } }
end

# Standard usage
User.active    # same as User.find(:all, :conditions => {:active => true})
User.inactive # same as User.find(:all, :conditions => {:active => false})
User.recent   # same as User.find(:all, :conditions => ['created_at > ?', 1.week.ago])

# They're nest-able too!
User.active.recent
  # same as:
  # User.with_scope(:conditions => {:active => true}) do
  #   User.find(:all, :conditions => ['created_at > ?', 1.week.ago])
  # end

All the goodness you’ve come to love in has_finder is now available as named_scope – plus you get some extra goodies too. User.all is given to you for free as an alias for User.find(:all).

Advanced

For those with more discriminating needs, don’t forget some of these has_finder tidbits:

Passing Arguments

Pass in arguments to your named scopes to specify conditions (or other props) at run-time.

1
2
3
4
5
class User < ActiveRecord::Base
  named_scope :registered, lambda { |time_ago| { :conditions => ['created_at > ?', time_ago] }
end

User.registered 7.days.ago # same as User.find(:all, :conditions => ['created_at > ?', 7.days.ago])

Named Scope Extensions

Extend named scopes (in a similar fashion to association extensions).

1
2
3
4
5
6
7
8
9
10
class User < ActiveRecord::Base
  named_scope :inactive, :conditions => {:active => false} do
    def activate
      each { |i| i.update_attribute(:active, true) }
    end
  end
end

# Re-activate all inactive users
User.inactive.activate

Anonymous Scopes

You can also pass around scopes as first class objects using scoped (a named scoped provided to you for free) as a way to build hairy queries on the fly.

1
2
3
4
5
6
7
8
9
# Store named scopes
active = User.scoped(:conditions => {:active => true})
recent = User.scoped(:conditions => ['created_at > ?', 7.days.ago])

# Which can be combined
recent_active = recent.active

# And operated upon
recent_active.each { |u| ... }

named_scope is a truly great feature. If you haven’t started using it yet, do so. You won’t know how you lived without it. Major thanks goes out to Nick.

tags: ruby, rubyonrails

Comments

Leave a response

  1. BenMarch 24, 2008 @ 12:28 PM

    I’ve been waiting for this to get integrated – it’s fantastic, and will be a huge boon to Rails developers. Thanks for the update, Ryan!

  2. MattMarch 24, 2008 @ 05:42 PM

    This looks so incredibly useful. I’m relatively new to Rails and haven’t had a chance to play with the has_finder plugin, so I can’t wait to start using this.

  3. ZargonyMarch 24, 2008 @ 06:55 PM

    Great. One of the first things I’m doing, when creating a new Rails project, is to install the has_finder plugin. I’m happy to hear that it has been merged into Rails now.

  4. Arie Kusuma AtmajaMarch 26, 2008 @ 12:02 PM

    Thanks for the great post, Ryan. Will there be such a convenient shortcut for count specifically and rails statistics methods generally as well?

    ex. User.count :conditions => { :active => true, :created_at.gt => 1.week.ago }
  5. ryanMarch 27, 2008 @ 10:31 AM

    Arie, yes – these methods are already supported just as they are with association extensions. E.g with my previous user example:

    user.active.recent.count #=> 23

    or

    user.active.recent.sum(:age) #=> 391

  6. PratikMarch 27, 2008 @ 08:56 PM

    Ryan,

    named_scope :recent, :conditions => ['created_at > ?', 1.week.ago]

    That is really a bad example. As the conditions gets evaluated at load time, it’ll end up giving you inaccurate results in production where models get loaded only once.

    Correct way would be :

    named_scope :recent, lambda { { :conditions => ['created_at > ?', 1.minute.ago] } }
  7. RyanMarch 27, 2008 @ 10:40 PM

    Pratik – great point. I shall adjust my examples…

  8. ArunApril 01, 2008 @ 02:05 AM

    Really good feature… i like it very much… Thanks for the update Mr.Ryan…

  9. JamesApril 03, 2008 @ 12:34 PM

    Man, wish I would’ve found out about has_finder earlier! This is great!

  10. LarApril 08, 2008 @ 03:49 AM

    I’m playing around with this right now, and I’m wondering how you would spec something like this out in RSpec if you were following a BDD approach? Or is this one of those things that is tested as part of rails, and I shouldn’t bother with? I would think that if you were using this to put together some complicated queries that there would have to be some sort of tests to ensure you are getting the results you want.

  11. RailsApril 09, 2008 @ 03:02 AM

    Very nice, just read about it on the official Rails Site. thanks for your examples! Daniel

  12. Deepak JoisApril 12, 2008 @ 08:51 AM

    It does not support other options supported by find like :include (but order is supported). Any idea about that?

  13. rogerMay 26, 2008 @ 05:47 PM

    wow those rock!

  14. ErikJune 01, 2008 @ 09:05 AM

    Wow, fantastic! I’ve been dying for a way to do this elegantly for ages. You can’t beat having it right in the soup.

  15. Fabien JakimowiczJune 04, 2008 @ 10:54 AM

    Is there any way to eager load a named_scope from an association ?

    Something like

    User.find(:all, :include => [:messages.public])

    with

    User: has_many :messages

    Message: named_scope :public, :conditions => {:is_public => true}

  16. MickJune 05, 2008 @ 07:11 AM

    Ryan, Looks like you’ve got a syntax error in one of the examples. I believe:

    recent = User.scoped(:conditions => ['created_at > ?', 7.days.ago)

    should be:

    recent = User.scoped(:conditions => ['created_at > ?', 7.days.ago])
  17. PavelJune 06, 2008 @ 07:22 AM

    thank you very much! I have successfully used this! =)

  18. Walter HorstmanJune 13, 2008 @ 06:37 AM

    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

    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?

  19. AscanioJune 13, 2008 @ 03:37 PM

    OMG, I just understood how much a nerd I have become.

    Can you believe I got so ecstatic about this while reading this introduction (and falling in love with the beautiful code snippets) that I just started to jump around screaming how cool all this is?

    It’s true I just can’t believe how cool Ruby is, and the incredible work you guys are doing with Rails. It so just as it’s meant to be. It couldn’t be otherwise.

    It just fits.

    Keep it rolling guys!

    You rock really, really hard…