Use create_or_find_by to avoid race condition in Rails 6.0

Prem Sichanugrist
Sikachu's Blog
Published in
2 min readFeb 21, 2018

--

It’s pretty common in Ruby on Rails to use find_or_create_by method when you want to create a new record if the record isn’t already existed:

> User.find_or_create_by(username: "sikachu")
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."username" = ? LIMIT ? [["username", "sikachu"], ["LIMIT", 1]]
(0.1ms) begin transaction
User Create (1.9ms) INSERT INTO "users" ("username", "created_at", "updated_at") VALUES (?, ?, ?) [["username", "sikachu"], ["created_at", "2018-02-21 02:52:05.983257"], ["updated_at", "2018-02-21 02:52:05.983257"]]
(0.8ms) commit transaction
#=> #<User id: 1, username: "sikachu", created_at: "2018-02-21 02:52:05", updated_at: "2018-02-21 02:52:05">
> User.find_or_create_by(username: "sikachu")
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."username" = ? LIMIT ? [["username", "sikachu"], ["LIMIT", 1]]
#=> #<User id: 1, username: "sikachu", created_at: "2018-02-21 02:52:05", updated_at: "2018-02-21 02:52:05">

However, this method is actually prone to a race condition. It’s possible that user may double-click a submit button and duplicate a HTTP request. If the timing is perfect, bothSELECT would then see no records, then try to send INSERT with the same values. In the end, you will end up with either two rows with the same value in the database, or user will face an Internal Server Error page.

To prevent this type of race condition, DHH recently submitted a patch to add a new method call create_or_find_by to offer a safer way to create a new record in the database if the given condition isn’t already exists. This method avoids the race condition by using database unique key constraint error to control the flow instead of the traditional SELECT-then-INSERT (which is the behavior of find_or_create_by).

Because this new create_or_find_by method has the same method signature as find_or_create_by, you can just substitute it right away:

# Similar usage as the code above:
User.create_or_find_by(username: "sikachu")
# There is also a bang version which will raise if there is
# a validation error:
User.create_or_find_by!(username: "sikachu")

Internally, this method does a create, rescueActiveRecord::RecordNotUnique exception, and perform find_by! if that exception got raised. While this is totally using an exception as a flow control (which is discouraged), I think this scenario is an acceptable use as you are letting your database backend which stores your data to maintain your data consistency.

--

--

Senior Developer at Degica. I also contribute to open source projects, mostly in Ruby.