Person.first(n) and Person.last(n) to work like Array#first and Array#last

Ever since the ActiveRecord::Base.first and last convenience methods
were added, I've wanted to be able to call them as they can be called
on an array, passing a limit, n, as the first argument.

Person.first(5) is a bit sweeter than Person.all(:limit => 5).

Ticket/patch here:

https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/3565-add-limit-functionality-to-find-first-and-last#ticket-3565-2

Any thoughts?

Stephen

I’m not sure. AR::Base subclasses don’t act like Arrays, so I don’t think we should be matching same-named method implementations just for the sake of doing so, regardless of how sweet the syntactic sugar is.

Well, I do think that it goes a little beyond syntactic sugar.
AR::Base subclasses don't act like arrays, but their scopes and
association proxies do, to an extent. Consider a named scope, the
behavior through Ruby becomes more clear.

  Article.published.first(5) # Selects without a limit unless there is
a limit on the named scope.

It actually looks like the patch needs a little adjusting to fix this
case for named scopes, but if we're to make scopes smarter about this
array method, it only makes sense, for consistency, to offer the same
behavior at the AR::Base class level.

I've wanted to use #first and #last the same way, and in fact keep forgetting that I can't. I don't see any downside to supporting the syntax.

Cheers,
Luke

Does Person.first(1) return an collection or an object?

It would return a collection in order to remain consistent with
association proxies, scopes, and the arrays they act like (especially
after they've already executed).

  Person.active.first(1) # => [#<Person>]
  Person.active.first # => #<Person>
  Person.first(1) # => [#<Person>]
  Person.first # => #<Person>
  Person.find(1).blog_posts.first(1) # => [#<BlogPost>]
  Person.find(1).blog_posts.first # => #<BlogPost>

The #first and #last class methods on ActiveRecord::Base are examples
of Rails cleverness gone a little over the edge. Who hasn't run into
this problem?:

Person.blog_posts.first # => #<BlogPost name='foo'>
Person.blog_posts.first.name = 'bar'
Person.blog_posts.first # => #<BlogPost name='foo'>

The #first and #last methods always go to the database when called on
an unloaded association collection proxy, and do not update the
association target with the resulting record. So:

Person.blog_posts.loaded? # => false
Person.blog_posts.first # => #<BlogPost>
Person.blog_posts.loaded? # => false
Person.blog_posts.target # => []

This is obviously for performance reasons, given the assumption that
the most frequent case will be someone loading the first or last
record once, in which case returning a single record from the database
is faster than returning all records and then asking the underlying
array for the first one. Of course, in other cases, this causes
problems. For instance:

Person.blog_posts.count # => 5
Person.blog_posts.first.mark_for_destruction # => true
Person.save! # => true
Person.blog_posts.count #= 5 (!)

Now, if you start returning an arbitrarily large number of records
from the #first and #last methods, the single advantage of those
methods (performance) goes away, leaving only liabilities. Consider
this:

Person.blog_posts(100) # => [#<BlogPost, #<BlogPost]... ]
Person.blog_posts # => [#<BlogPost, #<BlogPost]... ]

Both of those calls generated database calls; the first returned 100
records, the second returned all records *including the 100 already
returned previously*. And nothing in that code makes it clear that
you're doing more database calls than necessary, or that you're
creating two in-memory objects for each of the 100 records returned
twice, or that the first 100 records aren't included in the
association proxy target.

Note that calling #first or #last with a count parameter on a
collection proxy works as expected with the current code because it
loads records and then delegates to the underlying array. This patch
would modify this existing behavior on association collections.

I realize the original intention was to add this capability to
ActiveRecord::Base class objects, but keep in mind you can't add an
ActiveRecord::Base class method without affecting the way association
proxies work (consider #sum).

The #first and #last class methods on ActiveRecord::Base are examples
of Rails cleverness gone a little over the edge.

The cleverness may go a little over the edge, but, to add to your list
of general confusion with these inconsistencies:

Person.scoped({}).first # SELECT * FROM `people` LIMIT 1

=> #<Person>

Person.scoped({}).first(5) # SELECT * FROM `people`

=> [#<Person>, #<Person>, #<Person>, #<Person>, #<Person>]

It works the same way with association proxies. Call "first" or "last"
with an number argument and Rails doesn't optimize, but call "first"
or "last" with an options hash or no argument, and it does. This
inconsistent behavior could easily cause confusion.

To add one more layer of inconsistency:

Person.first(5) # SELECT * FROM `people` LIMIT 1

=> #<Person>

Now, if you start returning an arbitrarily large number of records
from the #first and #last methods, the single advantage of those
methods (performance) goes away, leaving only liabilities.

Well, even an exaggerated example like @site.users.first(100) would
offer much better performance if that integer limit were respected, if
there were hundreds of thousands of users.

Note that calling #first or #last with a count parameter on a
collection proxy works as expected with the current code because it
loads records and then delegates to the underlying array. This patch
would modify this existing behavior on association collections.

Technically, this patch doesn't (as it stands) modify this behavior,
because named scopes and association proxies type check the first
argument for integers before performing.

I realize the original intention was to add this capability to
ActiveRecord::Base class objects, but keep in mind you can't add an
ActiveRecord::Base class method without affecting the way association
proxies work (consider #sum).

Actually, I did intend to add the capability through scopes and
proxies, as well, if there was community interest.

I do think seeking some kind of consistency is desirable. Making the
convenience methods cleverer just seems to be most backwards-
compatible way of handling it, retaining backwards compatibility and
general performance benefits.

Stephen

That's true. However, has_many associations and named scopes do act
like Arrays for the most part, and we implement most of that goodness
on AR::Base.

I would say that your argument is a fair one, but by that argument we
should not have .first and .last at all.

We do have them, a lot of us find them very convenient, and a number
of us would find the form with the numeric argument convenient too -
there were a lot of people keen on
https://rails.lighthouseapp.com/projects/8994/tickets/2591-passing-in-fixnum-to-firstlast-finders,
which is for the same issue, and the only arguments against were
people who wanted more (file yer own ticket! ;)) and one person who
doesn't want to use this (you don't have to).

Almost all of ActiveRecord is syntatic sugar for things you could do
the hard way if you want to. Where we have added sugar, we should
make it consistent with the rest of the language and framework, when
it is useful to do so - and in this case it clearly is, given the
number of people who are interested in this simple consistency
extension.

If this doesn't gain any traction, or till it does, I've fashioned an
opinionated little gem wherein I'll keep basic patches I tend to use:

http://github.com/stephencelis/rerails

Hopefully others can get some use out of them, too.

Stephen