I'm trying to enforce uniqueness of values in one of my table fields. Changing the table isn't an option. I need to use ActiveRecord to conditionally insert a row into the table but I'm concerned about synchronization.
Does first_or_create
in Rails ActiveRecord prevent race conditions?
This is the source code for first_or_create
from GitHub:
def first_or_create(attributes = nil, options = {}, &block)
first || create(attributes, options, &block)
end
Is it possible that a duplicate entry will result in the database due to synchronization issues with multiple processes?
The Rails 4 documentation for find_or_create_by
provides a tip that may be useful for this situation:
Please note this method is not atomic, it runs first a SELECT, and if there are no results an INSERT is attempted. If there are other threads or processes there is a race condition between both calls and it could be the case that you end up with two similar records.
Whether that is a problem or not depends on the logic of the application, but in the particular case in which rows have a UNIQUE constraint an exception may be raised, just retry:
begin CreditAccount.find_or_create_by(user_id: user.id) rescue ActiveRecord::RecordNotUnique retry end
Similar error catching may be useful for Rails 3. (Not sure if the same ActiveRecord::RecordNotUnique
error is thrown in Rails 3, so your implementation may need to be different.)
Yes, it's possible.
You can significantly reduce the chance of conflict with either optimistic or pessimistic locking. Of course optimistic locking requires adding a field to the table, and pessimistic locking doesn't scale as well--plus, it depends on your data store's capabilities.
I'm not sure whether you need the extra protection, but it's available.
Rails 6 (released in 2019) introduced a new method, create_or_find_by
, specifically to deal with the potential race condition / synchronization issue that exists with first_or_create
and find_or_create_by
, where two different threads could simultaneously SELECT to see if the target record exists, both see that it doesn't, and then both try to INSERT it.
Example usage:
company = Company.create_or_find_by(name: 'Stack Overflow')
(Where Company
is an existing ActiveRecord model, and name
is a unique-constrained column on the company
table.)
Also available: create_or_find_by!
, which raises an exception if a validation error occurs.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With