Lighthouse has a new layout. Prefer the old one? Return to the old layout, and switch back any time from the link at the top of each page.

This project is archived and is in readonly mode.

Add singular version of named_scope that returns first object

#1429

So, I find myself creating a bunch of named_scopes designed to return only one object, like this:


class Book
  named_scope :most_recently_read, :order => "last_read desc", :limit => 1
end

Which means I have to chain .first to get what I want like this:


Book.most_recently_read.first

Which just feels plain wrong!

My proposition is adding some way of creating a named_scope that is designed to return only one object.

I've attached two patches (with docs and tests) for this. The first one (add_first_to_named_scope.patch) adds a first option to named_scope, so it looks like this:


named_scope :most_recently_read, :first => true, :order => "last_read desc", :limit => 1

But I think the :first => true format looks pretty ugly.

My second patch (add_singular_named_scope.patch) proposition is to add a new method singular_named_scope. You would define your singular scopes thusly:


singular_named_scope :most_recently_read, :order => "last_read desc", :limit => 1

But..I'm open to suggestions!

Reported by Chris O'Sullivan · November 21st, 2008 @ 05:24 PM

State: invalid
Milestone: 2.x
Assigned to: Pratik Pratik
Importance: Low

Activity

  1. Chris O'Sullivan
    Chris O'Sullivan

    (The second patch file for the singular_named_scope method)

    November 21st, 2008 @ 05:25 PM

  2. Ryan Bates
    Ryan Bates

    Sorry, perhaps I'm missing something. Is there a reason you don't like doing this?

    
    class Book < ActiveRecord::Base
      named_scope :most_recently_read, :order => "last_read desc"
    end
    Book.recently_read.first
    

    To me this is much more useful and flexible. If you need to list multiple recently read books no need to add a second named scope. And this is chainable with other named scopes. If you have a singular named scope you couldn't chain others after it:

    
    Book.recently_read.favorites.first
    Book.most_recently_read.favorites # wouldn't work
    

    Of course you can always chain the singular one at the end, but in general that seems to go against the grain of named scopes to me.

    November 21st, 2008 @ 07:25 PM

  3. Ryan Bates
    Ryan Bates

    Oops, the first code snippet should be:

    
    class Book < ActiveRecord::Base
      named_scope :recently_read, :order => "last_read desc"
    end
    Book.recently_read.first
    

    November 21st, 2008 @ 07:26 PM

  4. Chris O'Sullivan
    Chris O'Sullivan

    I think it IS in the spirit to want to return one object from a named_scope. After all, .first and .last return only one object (although I guess they're not 'technically' named_scopes.) This patch would allow developers to create named_scopes like oldest or largest - (or if you're so inclined second through forty_second).

    With traditional ActiveRecord finders we don't have to write code like:

    
    Book.find(:all, :order => "last_read desc").first
    

    So why should named_scopes be any different?

    I also prefer the readability of something like:

    Book.last_read

    rather than

    Book.recently_read.first

    And there's no reason I can't have both scopes, one to define the last book that was read, and one to define a list of books ordered by 'most recently read'.

    But, of course there's also nothing to stop me defining a class method like:

    
    def self.last_read
      self.recently_read.first
    end
    

    November 23rd, 2008 @ 09:22 PM

  5. Chris Kilmer
    Chris Kilmer

    To continue the conversation a little bit. We are currently looking at this exact problem. We want a particular model to return a default. The model can have one and only one model.

    We currently have:

    Device.default.first

    (where Device.default is a named_scope)

    First seems redundant. Default should imply 1.

    February 19th, 2009 @ 05:28 PM

  6. MatthewRudy
    MatthewRudy

    I dont mind having some sort of declaration like this.

    But it isn't a "named_scope" by any means

    
    Book.most_recently_read.count => Exception,
    Book.most_recently_read.in_stock => Exception,
    

    It doesn't scope anything. It doesnt even use the named scope.

    If I were implementing it I'd called it "named_finder"

    
    named_finder :most_recently_read, :first, :order => "last_read desc"
    

    or

    
    named_scope :recently_read, :order => "last_read DESC" do
      named_finder :most_recently_read, :first
    end
    

    It feels like an abuse of the name and the code with the patches submitted so far.

    February 25th, 2009 @ 03:45 PM

  7. MatthewRudy
    MatthewRudy
    
    def named_finder(name, *find_args)
      (class << self; self end).instance_eval do 
        define_method name do
          self.find(*find_args)
        end
      end
    end
    

    and it actually seems to work

    
    >> class User
    >>   named_finder :first_matthew, :first, :conditions => {:firstname => "Matthew"}
    >> end
    => #<Proc:0x03eed6f0@(irb):4>
    
    >> User.first_matthew
    => #<User id: 633, a_whole_load_of_stuff: "redacted", html_email: true>
    

    nice.

    February 25th, 2009 @ 03:52 PM

  8. MatthewRudy
    MatthewRudy

    (I need to have just one lighthouse user)

    February 25th, 2009 @ 03:56 PM

  9. Pratik
    Pratik
    • Assigned user set to Pratik
    • Tag changed from named_scope to named_scope, patch
    • State changed from new to invalid
    • Title changed from [PATCH] Add singular version of named_scope that returns first object to Add singular version of named_scope that returns first object

    I don't think named_scopes should be returning a single object. It's scoping, not a finder.

    But please do discuss in the mailing list if you guys think it's worth it.

    Thanks.

    March 9th, 2009 @ 01:33 PM

  10. Timothy Jones
    Timothy Jones

    Anyone care to create a plugin/gem for this, since many of us think it a good idea but it didn't meet Pratik's personal taste?

    February 10th, 2010 @ 05:34 PM

  11. Jeremy Kemper
    Jeremy Kemper

    Let's see a wider body of examples of where you'd use a singular finder+scope.

    Every one I've seen (or thought of) is something like latest: latest version, latest comment, etc. Order + find first.

    Others?

    February 10th, 2010 @ 06:31 PM

  12. Timothy Jones
    Timothy Jones

    Sometimes the result set contains exactly one (or at most one) record, or I just one one random record and really don't care which it is, so order isn't necessary.

    For example, in a geographic database containing US cities and states I have a named_scope "Location.city_in_state(city_name, state_abbreviation)". Having to suffix that with ".first" everyplace it's used throughout the application isn't very DRY, is it?

    Basically, named_scope() should have a :first mode for the same reason that find() has a :first mode.

    February 10th, 2010 @ 06:49 PM

  13. DHH
    DHH

    Tim, why not do Location.find_by_city_and_state(city_name, state_abbreviation) in that case?

    February 10th, 2010 @ 07:47 PM

  14. Timothy Jones
    Timothy Jones

    In this case my query is more complicated than the attribute-based finders support AFAIK (specifically it requires a join).

    February 10th, 2010 @ 07:54 PM

  15. Timothy Jones
    Timothy Jones

    Ok, I take that back, now realizing that attribute-based finders do accept general find() options. But the whole point of named_scopes is syntactic sugar, right?

    My Location model is generic, so locations have a name, type, and potentially multiple parents. In the US, cities' parents are counties, then states. In Canada, cities are parented directly by states. Here's my (admittedly gnarly) named_scope:

    named_scope(

    :city_in_state,
    lambda do |c,s|
      {
        :select => "locations.*",
        :joins => "JOIN locations_parents AS l_p ON locations.id = l_p.location_id LEFT JOIN locations AS counties ON l_p.parent_id = counties.id AND counties.location_type_id = #{LocationType.find_by_name('county').id} LEFT JOIN locations_parents AS c_p ON counties.id = c_p.location_id JOIN locations AS states ON (c_p.parent_id = states.id OR l_p.parent_id = states.id) AND states.location_type_id = #{LocationType.find_by_name('state').id}",
        :conditions => ["locations.location_type_id = ? AND locations.name = ? AND (states.name = ? OR states.abbreviation = ?)", LocationType.find_by_name("city").id, c, s, s]
      }
    end
    

    )

    If you can show me how to write that with current finders in a way that's not drastically uglier than:

    city = Location.city_in_state(city_name, state_name)

    then I'll shut up :-).

    February 10th, 2010 @ 08:05 PM

  16. Lachlan Sylvester
    Lachlan Sylvester

    Tim, why not define a scope to find cities within a state, then you can use find_by_name to get the city.

    city = Location.cities_in_state(state_name).find_by_name(city)
    

    February 11th, 2010 @ 10:43 AM

  17. Timothy Jones
    Timothy Jones

    Yes, that would work and is not drastically uglier than what I proposed.

    February 11th, 2010 @ 06:07 PM

  18. Ryan Bigg
    Ryan Bigg
    • Tag cleared.
    • Importance changed from to Low

    Automatic cleanup of spam.

    October 9th, 2010 @ 10:03 PM

  19. bingbing