在Bryan Helmkamp的优秀博客文章中,他提到使用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
语句来存储数据存储约束。
有谁知道处理这个问题的好方法?也许为此编写我自己的验证器会更好(但随后我会对数据库进行两次查询,理想情况下一个就足够了)。
如果创建自定义验证器恰好是一次性要求,则可能会矫枉过正。
简化的方法...
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
布莱恩很友好地在他的博客文章中评论了我的问题。在他的帮助下,我想出了以下自定义验证器:
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
时,这些验证不会运行,因此您必须以其他方式检查这些验证。您始终可以在上述persist!
方法中使用save(validate: false)
,但是您必须确保所有验证都在Signup
类中,并在更改Account
或User
中的任何验证时使该类保持最新。