Rails has a long history of providing expressive, intuitive query methods. You’re probably familiar with find_by
and find_by!
, which let you grab records by certain attributes. But since Rails 6.0, a newer method has joined the Active Record toolbox: find_sole_by
.
It looks deceptively similar to find_by
, but it behaves quite differently—and more strictly. In this post, we’ll break down what it does, why it exists, and when you should use it.
⸻
A Refresher: find_by vs. find_by!
Before diving in, let’s quickly review:
find_by
: Returns the first record that matches the given conditions. If no record is found, it returns nil.find_by!
: Same as above, but raises ActiveRecord::RecordNotFound if no record is found.
Both of these methods are fine when you don’t mind multiple rows potentially matching the query. But what if you want to enforce uniqueness at query time, not just in your model validations?
Enter find_sole_by
find_sole_by
is like find_by!
but stricter. It ensures:
- Exactly one record matches the conditions.
- If no records match, it raises ActiveRecord::RecordNotFound.
- If multiple records match, it raises ActiveRecord::SoleRecordExceeded.
That’s right—it enforces sole-ness.
Example:
# Suppose users have a unique email
User.find_sole_by(email: "[email protected]")
# => returns the single record
User.find_sole_by(email: "[email protected]")
# => raises ActiveRecord::RecordNotFound
User.find_sole_by(role: "admin")
# => raises ActiveRecord::SoleRecordExceeded if more than one admin exists
Why Would You Use It?
There are several scenarios where find_sole_by
is a better fit than find_by
:
- Enforcing assumptions in code: If your application logic assumes there will only ever be one matching record,
find_sole_by
ensures that assumption holds true in the database. - Preventing silent bugs:
find_by
will quietly return the first record if multiple exist, which can lead to confusing, hard-to-track issues.find_sole_by
forces you to deal with duplicates immediately. - Safer domain modeling: Even if you have validations or unique indexes, database drift and data corruption can happen. This method adds another safeguard.
Comparing All Four Methods
Method | No Match | One Match | Multiple Matches |
---|---|---|---|
find_by | nil | record | first record found (not guaranteed) |
find_by! | raises RecordNotFound | record | first record found (not guaranteed) |
find_sole_by | raises RecordNotFound | record | raises SoleRecordExceeded |
find_sole_by! ⚠️ | doesn’t exist (use find_sole_by) | - | - |
A Real-World Example
Imagine you’re building a tournament app. A Tourney might have a single “final match” you want to fetch. You know there must be exactly one match with round: “final”.
tourney.matches.find_sole_by(round: "final")
This guarantees you don’t accidentally fetch the wrong match if multiple finals exist—or a nil if one hasn’t been created yet. Your code will blow up loudly instead of failing silently.
Takeaways
- Use
find_by
if multiple matches are acceptable, and you’re fine with nil when none exist. - Use
find_by!
if multiple matches are acceptable, but you want to enforce at least one match. - Use
find_sole_by
when your domain logic requires exactly one record.
In short: find_sole_by
helps you write safer, more intentional queries.