ActiveRecord is the object-relational mapping (ORM) library used in Ruby on Rails. .find
and .find_by
are two of its methods that, given an ID, can query a record from the database for a given model.1 At first glance, .find
and .find_by
look pretty similar. In this blog post, however, we’ll discuss the readability benefits of .find_by
over .find
when we want to query by ID.
Suppose that we’re using our Rails console, and we want to read an instance of Foo
that has an ID of 1
from the database. Using .find
and .find_by
looks like so:
2.5.1 :001 > Foo.find(1)
Foo Load (0.1ms) SELECT "foos".* FROM "foos" WHERE "foos"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<Foo id: 1, created_at: "2021-03-07 23:55:46.994305000 +0000", updated_at: "2021-03-07 23:55:46.994305000 +0000">
2.5.1 :002 > Foo.find_by(id: 1)
Foo Load (0.1ms) SELECT "foos".* FROM "foos" WHERE "foos"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<Foo id: 1, created_at: "2021-03-07 23:55:46.994305000 +0000", updated_at: "2021-03-07 23:55:46.994305000 +0000">
In .find
, we pass in 1
as a positional argument, while in .find_by
, we pass 1
in as a keyword argument for id
.
The generated SQL is identical for .find
and .find_by
, and both returned the same object. However, what happens if we try to query a Foo
that doesn’t exist? Suppose we don’t have a Foo with the ID of 100
in our database, and we try to query it:
2.5.1 :003 > Foo.find(100)
Foo Load (0.1ms) SELECT "foos".* FROM "foos" WHERE "foos"."id" = ? LIMIT ? [["id", 100], ["LIMIT", 1]]
Traceback (most recent call last):
1: from (irb):16
ActiveRecord::RecordNotFound (Couldn't find Foo with 'id'=100)
2.5.1 :004 > Foo.find_by(id: 100)
Foo Load (0.1ms) SELECT "foos".* FROM "foos" WHERE "foos"."id" = ? LIMIT ? [["id", 100], ["LIMIT", 1]]
=> nil
.find
raises a RecordNotFound
exception when a Foo
with ID of 100
doesn’t exist. find_by
, on the other hand, returns nil
without raising an exception – if we did want to raise a RecordNotFound
exception, we would call the bang method find_by!
:2
2.5.1 :005 > Foo.find_by!(id: 100)
Foo Load (0.1ms) SELECT "foos".* FROM "foos" WHERE "foos"."id" = ? LIMIT ? [["id", 100], ["LIMIT", 1]]
Traceback (most recent call last):
1: from (irb):27
ActiveRecord::RecordNotFound (Couldn't find Foo)
Based on the examples above, there are two readability advantages to using .find_by
, which we’ll discuss below:
First, when we use .find_by
, it’s clearer that we’re querying by ID. The examples above clearly stated that 1
and 100
were IDs for our Foo
records, but what if we were working with a snippet of code like so?
some_undescriptive_variable_name = 1
Foo.find(some_undescriptive_variable_name)
If we haven’t named our variables well, it may not immediately clear to a reader that we’re querying by ID, especially if our reader is unfamiliar with Ruby on Rails. Foo.find_by(id: some_undescriptive_variable_name)
, on the other hand, makes this more explicit via its id
keyword argument.
Second, .find_by
allows us to be more explicit about whether or not we want to raise an exception. From its name alone, non-obvious that .find
will raise an exception if it can’t find a record. On the other hand .find_by!
follows the bang method convention, s0 it’s more clear that we intend to raise an exception when we call it. And if we don’t want to raise an exception, we can simply call .find_by
.
This preference for .find_by
over .find
only applies to the case where we’re querying on a single ID, since these two methods actually generate different SQL when querying multiple IDs. When we pass in an array of IDs to .find
, it returns an array of the matching records, while .find_by
would truncate the result to one record:
2.5.1 :006 > Foo.find([1,2])
Foo Load (0.2ms) SELECT "foos".* FROM "foos" WHERE "foos"."id" IN (?, ?) [[nil, 1], [nil, 2]]
=> [#<Foo id: 1, created_at: "2021-03-07 23:55:46.994305000 +0000", updated_at: "2021-03-07 23:55:46.994305000 +0000">, #<Foo id: 2, created_at: "2021-03-08 00:10:12.849400000 +0000", updated_at: "2021-03-08 00:10:12.849400000 +0000">]
2.5.1 :007 > Foo.find_by(id: [1,2])
Foo Load (0.2ms) SELECT "foos".* FROM "foos" WHERE "foos"."id" IN (?, ?) LIMIT ? [[nil, 1], [nil, 2], ["LIMIT", 1]]
=> #<Foo id: 1, created_at: "2021-03-07 23:55:46.994305000 +0000", updated_at: "2021-03-07 23:55:46.994305000 +0000">
For cases where we want to query a single ActiveRecord record by ID, however, we should prefer .find_by
to .find
, as this produces more readable code.
Start your journey towards writing better software, and watch this space for new content.
1: This is a non-exhaustive set of methods. .where
is another ActiveRecord method that can query by ID.
2: By convention, a bang method in Ruby is expected to modify the object it’s called on, has a state-changing side effect, or will raise an exception.