The Gnar Company
The Gnar Company

The last record - according to whom?

by Andrew Palmer

TL;DR

Unless you explicitly tell the database the order in which to return records, it'll just give you the records you requested in whatever order it wants.

Background

Let's say I have an author model, and authors have many books. Each author's most-recent book is considered their "current book", and the current book is very important for some business reason that we don't need to get into. Since an author has_many :books, we'll define their current_book as books.last.

class Author < ApplicationRecord
  has_many :books

  ...

  def current_book
    books.last
  end
end
class Book < ApplicationRecord
  belongs_to :author

  ...

end

Great. So now, if an author has 2 books and we try to access the current one, we'll get back the one with the largest id:

irb(main):001:0> author.current_book
  Book Load (6.8ms)  SELECT  "books".* FROM "books" WHERE "books"."author_id" = $1 ORDER BY "books"."id" DESC LIMIT $2  [["author_id", 37], ["LIMIT", 1]]
=> #<Book id: 2, author_id: 1, created_at: "2019-10-21 17:28:26", updated_at: "2019-10-21 17:28:26">

Note the ORDER BY "books"."id" in the generated SQL above.

That happens because ActiveRecord's .last method finds the last record, ordered by primary key if no order is specified. Perfect.

Or is it?

If you dig into the source a little bit, you'll see that there's a little more going on than just a simple query ordered by primary key:

# https://github.com/rails/rails/blob/f250da5397b967fcba58356547bc26127c1be93e/activerecord/lib/active_record/relation/finder_methods.rb#L147
def last(limit = nil)
  return find_last(limit) if loaded? || has_limit_or_offset?

  result = ordered_relation.limit(limit)
  result = result.reverse_order!

  limit ? result.reverse : result.first
end

That first line, specifically find_last(limit) if loaded?, is important. So if the association is already loaded into memory, we'll return the last record of the association in memory instead of running the result = ordered_relation.limit(limit) line. The ordered_relation in that line is responsible for ordering the query by id, like we expect.

Problem

Let's write a test to demonstrate the problem that can arise based on how your code is written:

require "rails_helper"

RSpec.describe Service, type: :service do
  describe "#do_something" do
    it "passes the current book to OtherService" do
      author = create(:author)
      previous_book = create(:book, author: author, id: 1, title: "Always Start with A")
      current_book = create(:book, author: author, id: 2, title: "Basically the Best")

      allow(OtherService).to receive(:do_something_with_current_book)

      Service.new.do_something

      expect(OtherService).to have_received(:do_something_with_current_book).with(current_book.title)
    end
  end
end

If we ever load a bunch of books into memory ordered by anything other than id, subsequently calling .current_book will return a different book than we expect, assuming we expect the "last" book to be the author's most-recent book. This example is contrived, but illustrates the point:

class Service
  def do_something
    Author.includes(:books).order("books.title desc").each do |author|
      OtherService.do_something_with_current_book(author.current_book.title)
    end
  end
end
Service
  #do_something
    passes the correct book to OtherService (FAILED - 1)

Failures:

  1) Service#do_something passes the current book to OtherService
     Failure/Error: expect(OtherService).to have_received(:do_something_with_current_book).with(current_book.title)

       #<OtherService (class)> received :do_something_with_current_book with unexpected arguments
         expected: ("Basically the Best")
              got: ("Always Start with A")

Finished in 0.72237 seconds (files took 11.15 seconds to load)
1 example, 1 failure

In this case we're explicitly sorting the books by title, which ensures the last book returned is different from the author's most-recent book. In a less-contrived and more realistic scenario with more data, where no order is explicitly specified, the responsibility of ordering results gets delegated to the database, which may or may not sort the results by primary key. As a result, without specifying an order, eager-loading an association might load records into memory sorted by something other than primary key, and the .last record of that association might not be what you expect it to be.

Solution

Fortunately, this is a complicated problem that has a quick solution, if you know to look for it: explicitly order the query instead of relying on .last to do it for you:

class Author < ApplicationRecord
  has_many :books

  ...

  def current_book
    books.order(:id).last
  end
end
Service
  #do_something
    passes the correct book to OtherService

Finished in 1 second (files took 11.59 seconds to load)
1 example, 0 failures

Outcome/Takeaways

The column by which the association is ordered doesn't have to be the id. Regardless, the important point is that just because .last often returns the record you're looking for doesn't mean that it always will, unless you tell it to.