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.
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
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">
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
result = ordered_relation.limit(limit) line. The
ordered_relation in that line is
responsible for ordering the query by id, like we expect.
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
record of that association might not be what you expect it to be.
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
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.
Learn more about how The Gnar builds Ruby on Rails applications.