На объекте ActiveModel, как я могу проверить уникальность?
В Bryan Helmkamp отличная запись в блоге под названием " 7 шаблонов для рефакторинга Fat ActiveRecord Models", он упоминает использование Form Objects
для абстрактного удаления нескольких -слойные формы и прекратить использование accepts_nested_attributes_for
.
Изменить: см. ниже для решения.
Я почти точно продублировал его образец кода, так как я решил такую же проблему:
class Signup
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :account
attribute :name, String
attribute :account_name, String
attribute :email, String
validates :email, presence: true
validates :account_name,
uniqueness: { case_sensitive: false },
length: 3..40,
format: { with: /^([a-z0-9\-]+)$/i }
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
@account = Account.create!(name: account_name)
@user = @account.users.create!(name: name, email: email)
end
end
Одна из вещей, отличных в моей части кода, заключается в том, что мне нужно проверить уникальность имени учетной записи (и электронной почты пользователя). Тем не менее, ActiveModel::Validations
не имеет валидатора uniqueness
, поскольку он должен быть резервным вариантом, не поддерживаемым базой данных ActiveRecord
.
Я понял, что есть три способа справиться с этим:
- Напишите мой собственный метод, чтобы проверить это (кажется лишним)
- Включить ActiveRecord:: Validations:: UniquenessValidator (попробовал это, не получил его для работы)
- Или добавьте ограничение на уровне хранения данных
Я бы предпочел использовать последний. Но потом я все время задаюсь вопросом, как это реализовать.
Я мог бы сделать что-то вроде (метапрограммирование, мне нужно было бы изменить некоторые другие области):
def persist!
@account = Account.create!(name: account_name)
@user = @account.users.create!(name: name, email: email)
rescue ActiveRecord::RecordNotUnique
errors.add(:name, "not unique" )
false
end
Но теперь у меня есть две проверки, запущенные в моем классе, сначала я использую valid?
, а затем я использую оператор rescue
для ограничений хранения данных.
Кто-нибудь знает хороший способ справиться с этой проблемой? Было бы лучше, возможно, написать мой собственный валидатор для этого (но тогда у меня было бы два запроса к базе данных, где в идеале было бы достаточно).
Ответы
Ответ 1
Брайан был достаточно любезен для комментариев по моему вопросу к его сообщению в блоге. С его помощью я придумал следующий пользовательский валидатор:
class UniquenessValidator < ActiveRecord::Validations::UniquenessValidator
def setup(klass)
super
@klass = options[:model] if options[:model]
end
def validate_each(record, attribute, value)
# UniquenessValidator can't be used outside of ActiveRecord instances, here
# we return the exact same error, unless the 'model' option is given.
#
if ! options[:model] && ! record.class.ancestors.include?(ActiveRecord::Base)
raise ArgumentError, "Unknown validator: 'UniquenessValidator'"
# If we're inside an ActiveRecord class, and `model` isn't set, use the
# default behaviour of the validator.
#
elsif ! options[:model]
super
# Custom validator options. The validator can be called in any class, as
# long as it includes `ActiveModel::Validations`. You can tell the validator
# which ActiveRecord based class to check against, using the `model`
# option. Also, if you are using a different attribute name, you can set the
# correct one for the ActiveRecord class using the `attribute` option.
#
else
record_org, attribute_org = record, attribute
attribute = options[:attribute].to_sym if options[:attribute]
record = options[:model].new(attribute => value)
super
if record.errors.any?
record_org.errors.add(attribute_org, :taken,
options.except(:case_sensitive, :scope).merge(value: value))
end
end
end
end
Вы можете использовать его в своих классах ActiveModel, например:
validates :account_name,
uniqueness: { case_sensitive: false, model: Account, attribute: 'name' }
Единственная проблема, с которой вы столкнетесь, заключается в том, что ваш собственный класс model
имеет также проверки. Эти проверки не выполняются при вызове Signup.new.save
, поэтому вам придется проверять их каким-либо другим способом. Вы всегда можете использовать save(validate: false)
внутри указанного метода persist!
, но тогда вы должны убедиться, что все проверки находятся в классе Signup
и обновите этот класс до даты, когда вы изменяете любые проверки в Account
или User
.
Ответ 2
Создание пользовательского валидатора может быть излишним, если это просто одноразовое требование.
Упрощенный подход...
class Signup
(...)
validates :email, presence: true
validates :account_name, length: {within: 3..40}, format: { with: /^([a-z0-9\-]+)$/i }
# Call a private method to verify uniqueness
validate :account_name_is_unique
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
# Refactor as needed
def account_name_is_unique
if Account.where(name: account_name).exists?
errors.add(:account_name, 'Account name is taken')
end
end
def persist!
@account = Account.create!(name: account_name)
@user = @account.users.create!(name: name, email: email)
end
end