Rails 6 has added ActiveRecord::Base.create_or_find_by/! as an alternative to ActiveRecord::Base.find_or_create_by/!
find_or_create_by
Relation#find_or_create_by
is one of Rails finder methods,
that finds the first record with the given attributes,
or creates a record with the attributes if one is not found.
# Find the first company with name "Saeloun"
# or create a new one
Company.find_or_create_by(name: 'Saeloun')
# => #<Company id: 1, name: "Saeloun">
# Second time we call the same,
# it will return existing record,
# since and existing record exists
Company.find_or_create_by(name: 'Saeloun')
# => #<Company id: 1, name: "Saeloun">
One of the problems of this approach is that its not an atomic operation.
It first runs a SELECT
, and if there are no results an INSERT
is attempted.
In high scale applications,
this can cause race conditions due to stale reads.
Separate threads might attempt to first SELECT
and then end up INSERT
ing multiple records.
Overcoming duplicate inserts
One way of still overcoming this race condition is catching duplicate record errors. These errors are thrown only if there is an underlying unique constraint on a field.
# name has a unique constraint in companies table
begin
Company.transaction(requires_new: true) do
Company.find_or_create_by(name: 'Saeloun')
end
rescue ActiveRecord::RecordNotUnique
retry
end
# => #<Company id: 1, name: "Saeloun">
In the above scenario, it attempts an insert,
if a race condition is hit,
an ActiveRecord::RecordNotUnique
is thrown.
We can simply rescue and retry again,
to fetch the existing record.
create_or_find_by
Enter create_or_find_by.
# Create a new company with the name "Saeloun"
# or return existing one
Company.create_or_find_by(name: 'Saeloun')
# => #<Company id: 1, name: "Saeloun">
create_or_find_by
tries to create a new record
with the given attributes,
that has a unique constraint on one or several of its columns.
As in our example above, if a record already exists with one of these unique constraints, an exception raised is first caught.
It then proceeds to use find_by!
and returns the record.
This helps users to overcome the stale reads issue which is caused by race conditions.
Limitations
create_or_find_by does have its limitations even though it overcomes stale reads issue.
We can still be hit by a race condition of INSERT
-> DELETE
-> SELECT
,
which will end up with no result found.
This is a much rarer scenario than the SELECT
-> INSERT
situation.
This only works if we have unique constraints on all columns we are using
to create or find by.
If we don’t, it will not raise ActiveRecord::RecordNotUnique
, and simply insert a duplicate record.
Since all this mechanism relies on throwing and catching exceptions, it can tend to be relatively slower.