Adds helper for `find_or_create_by` in transaction

This allows us to call `find_or_create_by` on all models and scopes.
This commit is contained in:
Bob Van Landuyt 2019-02-04 14:39:54 +01:00
parent d6b39ea7fb
commit ccd8a9b282
3 changed files with 25 additions and 27 deletions

View File

@ -6,4 +6,12 @@ class ApplicationRecord < ActiveRecord::Base
def self.id_in(ids) def self.id_in(ids)
where(id: ids) where(id: ids)
end end
def self.safe_find_or_create_by(*args)
transaction(requires_new: true) do
find_or_create_by(*args)
end
rescue ActiveRecord::RecordNotUnique
retry
end
end end

View File

@ -256,32 +256,12 @@ violation, for example.
Using transactions does not solve this problem. Using transactions does not solve this problem.
The following pattern should be used to avoid the problem: To solve this we've added the `ApplicationRecord.safe_find_or_create_by`.
```ruby This method can be used just as you would the normal
Project.transaction do `find_or_create_by` but it wraps the call in a *new* transaction and
begin retries if it were to fail because of an
User.find_or_create_by(username: "foo") `ActiveRecord::RecordNotUnique` error.
rescue ActiveRecord::RecordNotUnique
retry
end
end
```
If the above block is run inside a transaction and hits the race To be able to use this method, make sure the model you want to use
condition, the transaction is aborted and we cannot simply retry (any this on inherits from `ApplicationRecord`.
further queries inside the aborted transaction are going to fail). We
can employ [nested transactions](http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions)
here to only rollback the "inner transaction". Note that `requires_new: true` is required here.
```ruby
Project.transaction do
begin
User.transaction(requires_new: true) do
User.find_or_create_by(username: "foo")
end
rescue ActiveRecord::RecordNotUnique
retry
end
end
```

View File

@ -10,4 +10,14 @@ describe ApplicationRecord do
expect(User.id_in(records.last(2).map(&:id))).to eq(records.last(2)) expect(User.id_in(records.last(2).map(&:id))).to eq(records.last(2))
end end
end end
describe '#safe_find_or_create_by' do
it 'creates the user avoiding race conditions' do
expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
allow(Suggestion).to receive(:find_or_create_by).and_call_original
expect { Suggestion.safe_find_or_create_by(build(:suggestion).attributes) }
.to change { Suggestion.count }.by(1)
end
end
end end