Commit ccd8a9b2 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Adds helper for `find_or_create_by` in transaction

This allows us to call `find_or_create_by` on all models and scopes.
parent d6b39ea7
...@@ -6,4 +6,12 @@ class ApplicationRecord < ActiveRecord::Base ...@@ -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. ...@@ -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
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 To be able to use this method, make sure the model you want to use
Project.transaction do this on inherits from `ApplicationRecord`.
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 ...@@ -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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment