Я пытаюсь использовать create_or_find_by, который похож на find_or_create_by
, за исключением того, что он сначала пробует create
и прибегает к find
, если произошла ошибка проверки уникальности (по крайней мере, это мое понимание того, что он должен делать). Его преимущество заключается в том, что гораздо меньше вероятность того, что на него повлияют условия гонки.
Однако, похоже, это не работает:
class Nation < ApplicationRecord
validates :name, presence: true, uniqueness: true
end
> n = Nation.take
=> #<Nation...
> Nation.create_or_find_by(name: n.name).id
=> nil
> Nation.create_or_find_by!(name: n.name).id
/app/vendor/bundle/ruby/3.0.0/gems/activerecord-6.1.4/lib/active_record/validations.rb:80:in `raise_validation_error': Validation failed: Name has already been taken (ActiveRecord::RecordInvalid)
Я делаю что-то неправильно?
Получается, что create_or_find_by
проглатывает только ActiveRecord::RecordNotUnique
ошибки, вызванные ограничением уникальности, установленным на уровне базы данных. Если у вас также есть эквивалентные ограничения уникальности, установленные на уровне модели, они вызывают другую ошибку (ActiveRecord::RecordInvalid
) до того, как запрос будет отправлен в базу данных, и такая ошибка не проглатывается. Поэтому create_or_find_by
работает только в том случае, если у вас есть проверка уникальности только в базе данных, а не в модели.
Это показывает, что это работает, когда мы отключаем проверку на уровне модели:
n = Nation.take
validators = Nation._validate_callbacks.select { _1.filter.class == ActiveRecord::Validations::UniquenessValidator }
validators.each { Nation.skip_callback(_1.name, _1.kind, _1.filter) }
Nation.create_or_find_by!(name: n.name).id # returns the ID of the existing nation as expected
validators.each { Nation.set_callback(_1.name, _1.kind, _1.filter) }
Я решил исправить этот недостаток в своем приложении, добавив этот код в ApplicationRecord.rb
:
def self.create_or_find_by(attributes, &block)
obj = super(attributes, &block)
unless obj.persisted?
attribute_names = attributes.keys.map { _1.to_s.delete_suffix('_id') }
if obj.errors.any? && obj.errors.all? { _1.type == :taken && _1.attribute.to_s.delete_suffix('_id').in?(attribute_names) }
obj = find_by!(attributes)
end
end
obj
end
def self.create_or_find_by!(attributes, &block)
obj = create_or_find_by(attributes, &block)
obj.save! unless obj.persisted?
obj
end
Теперь create_or_find_by
работает так, как я думаю (по крайней мере, в моем приложении)