mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add #create_or_find_by to lean on unique constraints (#31989)
Add #create_or_find_by to lean on unique constraints
This commit is contained in:
parent
fbc7d63ab4
commit
fe6adf43e1
4 changed files with 78 additions and 13 deletions
|
@ -1,3 +1,9 @@
|
|||
## Rails 6.0.0.alpha (Unreleased) ##
|
||||
|
||||
* Add ActiveRecord::Base.create_or_find_by/! to deal with the SELECT/INSERT race condition in
|
||||
ActiveRecord::Base.find_or_create_by/! by leaning on unique constraints in the database.
|
||||
|
||||
*DHH*
|
||||
|
||||
|
||||
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activerecord/CHANGELOG.md) for previous changes.
|
||||
|
|
|
@ -5,7 +5,7 @@ module ActiveRecord
|
|||
delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all
|
||||
delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all
|
||||
delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
|
||||
delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
|
||||
delegate :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, to: :all
|
||||
delegate :find_by, :find_by!, to: :all
|
||||
delegate :destroy_all, :delete_all, :update_all, to: :all
|
||||
delegate :find_each, :find_in_batches, :in_batches, to: :all
|
||||
|
|
|
@ -147,18 +147,7 @@ module ActiveRecord
|
|||
# or processes there is a race condition between both calls and it could
|
||||
# be the case that you end up with two similar records.
|
||||
#
|
||||
# Whether that is a problem or not depends on the logic of the
|
||||
# application, but in the particular case in which rows have a UNIQUE
|
||||
# constraint an exception may be raised, just retry:
|
||||
#
|
||||
# begin
|
||||
# CreditAccount.transaction(requires_new: true) do
|
||||
# CreditAccount.find_or_create_by(user_id: user.id)
|
||||
# end
|
||||
# rescue ActiveRecord::RecordNotUnique
|
||||
# retry
|
||||
# end
|
||||
#
|
||||
# If this might be a problem for your application, please see #create_or_find_by.
|
||||
def find_or_create_by(attributes, &block)
|
||||
find_by(attributes) || create(attributes, &block)
|
||||
end
|
||||
|
@ -170,6 +159,47 @@ module ActiveRecord
|
|||
find_by(attributes) || create!(attributes, &block)
|
||||
end
|
||||
|
||||
# Attempts to create a record with the given attributes in a table that has a unique constraint
|
||||
# on one or several of its columns. If a row already exists with one or several of these
|
||||
# unique constraints, the exception such an insertion would normally raise is caught,
|
||||
# and the existing record with those attributes is found using #find_by.
|
||||
#
|
||||
# This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT
|
||||
# and the INSERT, as that method needs to first query the table, then attempt to insert a row
|
||||
# if none is found.
|
||||
#
|
||||
# There are several drawbacks to #create_or_find_by, though:
|
||||
#
|
||||
# * The underlying table must have the relevant columns defined with unique constraints.
|
||||
# * A unique constraint violation may be triggered by only one, or at least less than all,
|
||||
# of the given attributes. This means that the subsequent #find_by may fail to find a
|
||||
# matching record, which will then raise an <tt>ActiveRecord::RecordNotFound</tt> exception,
|
||||
# rather than a record with the given attributes.
|
||||
# * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by,
|
||||
# we actually have another race condition between INSERT -> SELECT, which can be triggered
|
||||
# if a DELETE between those two statements is run by another client. But for most applications,
|
||||
# that's a significantly less likely condition to hit.
|
||||
# * It relies on exception handling to handle control flow, which may be marginally slower.
|
||||
#
|
||||
# This method will return a record if all given attributes are covered by unique constraints
|
||||
# (unless the INSERT -> DELETE -> SELECT race condition is triggered), but if creation was attempted
|
||||
# and failed due to validation errors it won't be persisted, you get what #create returns in
|
||||
# such situation.
|
||||
def create_or_find_by(attributes, &block)
|
||||
transaction(requires_new: true) { create(attributes, &block) }
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
find_by!(attributes)
|
||||
end
|
||||
|
||||
# Like #create_or_find_by, but calls
|
||||
# {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception
|
||||
# is raised if the created record is invalid.
|
||||
def create_or_find_by!(attributes, &block)
|
||||
transaction(requires_new: true) { create!(attributes, &block) }
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
find_by!(attributes)
|
||||
end
|
||||
|
||||
# Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]
|
||||
# instead of {create}[rdoc-ref:Persistence::ClassMethods#create].
|
||||
def find_or_initialize_by(attributes, &block)
|
||||
|
|
|
@ -22,6 +22,7 @@ require "models/reader"
|
|||
require "models/category"
|
||||
require "models/categorization"
|
||||
require "models/edge"
|
||||
require "models/subscriber"
|
||||
|
||||
class RelationTest < ActiveRecord::TestCase
|
||||
fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans
|
||||
|
@ -1349,6 +1350,34 @@ class RelationTest < ActiveRecord::TestCase
|
|||
assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: "green") }
|
||||
end
|
||||
|
||||
def test_create_or_find_by
|
||||
assert_nil Subscriber.find_by(nick: "bob")
|
||||
|
||||
subscriber = Subscriber.create!(nick: "bob")
|
||||
|
||||
assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob")
|
||||
assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat")
|
||||
end
|
||||
|
||||
def test_create_or_find_by_with_non_unique_attributes
|
||||
Subscriber.create!(nick: "bob", name: "the builder")
|
||||
|
||||
assert_raises(ActiveRecord::RecordNotFound) do
|
||||
Subscriber.create_or_find_by(nick: "bob", name: "the cat")
|
||||
end
|
||||
end
|
||||
|
||||
def test_create_or_find_by_within_transaction
|
||||
assert_nil Subscriber.find_by(nick: "bob")
|
||||
|
||||
subscriber = Subscriber.create!(nick: "bob")
|
||||
|
||||
Subscriber.transaction do
|
||||
assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob")
|
||||
assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat")
|
||||
end
|
||||
end
|
||||
|
||||
def test_find_or_initialize_by
|
||||
assert_nil Bird.find_by(name: "bob")
|
||||
|
||||
|
|
Loading…
Reference in a new issue