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:
parent
d6b39ea7fb
commit
ccd8a9b282
|
|
@ -6,4 +6,12 @@ class ApplicationRecord < ActiveRecord::Base
|
|||
def self.id_in(ids)
|
||||
where(id: ids)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -256,32 +256,12 @@ violation, for example.
|
|||
|
||||
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
|
||||
Project.transaction do
|
||||
begin
|
||||
User.find_or_create_by(username: "foo")
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
retry
|
||||
end
|
||||
end
|
||||
```
|
||||
This method can be used just as you would the normal
|
||||
`find_or_create_by` but it wraps the call in a *new* transaction and
|
||||
retries if it were to fail because of an
|
||||
`ActiveRecord::RecordNotUnique` error.
|
||||
|
||||
If the above block is run inside a transaction and hits the race
|
||||
condition, the transaction is aborted and we cannot simply retry (any
|
||||
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
|
||||
```
|
||||
To be able to use this method, make sure the model you want to use
|
||||
this on inherits from `ApplicationRecord`.
|
||||
|
|
|
|||
|
|
@ -10,4 +10,14 @@ describe ApplicationRecord do
|
|||
expect(User.id_in(records.last(2).map(&:id))).to eq(records.last(2))
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue