Behaviour of ActiveRecord `find_or_create_by` is changing in Rails 7.1.0, introducing subtransactions
GitLab is currently on 7.0.8, and the Rails version 7.1.0 changes the behaviour of the method find_or_create_by
7.0.8 version:
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
7.1.0 version:
def find_or_create_by(attributes, &block)
find_by(attributes) || create_or_find_by(attributes, &block)
end
The introduction of create_or_find_by
in this method is worthy of attention because that method introduces subtransactions:
def create_or_find_by(attributes, &block)
transaction(requires_new: true) { create(attributes, &block) }
rescue ActiveRecord::RecordNotUnique
find_by!(attributes)
end
This is now similar to our custom method, safe_find_or_create_by, which we already deem as unsafe in our guides because it opens a new sub-transaction.
Both methods [create_or_find_by & safe_find_or_create_by] use subtransactions internally if executed within the context of an existing transaction. This can significantly impact overall performance, especially if more than 64 live subtransactions are being used inside a single transaction.
TODO:
Before we update our Rails version to 7.1.0, we should
- audit our use of
find_or_create_by
- replace it with alternatives if possible. Alternatives are listed here: https://docs.gitlab.com/ee/development/sql.html#alternative-1-upsert, https://docs.gitlab.com/ee/development/sql.html#alternative-2-check-existence-and-rescue
Using a pattern of
find_by(attributes) || upsert(attributes, on_duplicate: :skip)
could be a good alternative. upsert
with on_duplicate: :skip
also prevents the race condition without using a subtransaction. Noticed during the code review of !142600 (comment 1747335749)