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)
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue