In Bryan Helmkamp's excellent blog post called "7 Patterns to Refactor Fat ActiveRecord Models", he mentions using Form Objects
to abstract away multi-layer forms and stop using accepts_nested_attributes_for
.
在Bryan Helmkamp的优秀博客文章“7个模式来重构Fat ActiveRecord模型”中,他提到使用表单对象抽象出多层表单并停止使用accepts_nested_attributes_for。
Edit: see below for a solution.
编辑:请参阅下面的解决方案。
I've almost exactly duplicated his code sample, as I had the same problem to solve:
我几乎完全复制了他的代码示例,因为我有同样的问题需要解决:
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
One of the things different in my piece of code, is that I need to validate the uniqueness of the account name (and user e-mail). However, ActiveModel::Validations
doesn't have a uniqueness
validator, as it's supposed to be a non-database backed variant of ActiveRecord
.
我的代码中不同的一点是,我需要验证帐户名称(和用户电子邮件)的唯一性。但是,ActiveModel :: Validations没有唯一性验证器,因为它应该是ActiveRecord的非数据库支持的变体。
I figured there are three ways to handle this:
我想有三种方法可以解决这个问题:
- Write my own method to check this (feels redundant)
- 写我自己的方法检查这个(感觉多余)
- Include ActiveRecord::Validations::UniquenessValidator (tried this, didn't get it to work)
- 包括ActiveRecord :: Validations :: UniquenessValidator(试过这个,没有让它工作)
- Or add the constraint in the data storage layer
- 或者在数据存储层中添加约束
I would prefer to use the last one. But then I'm kept wondering how I would implement this.
我宁愿使用最后一个。但后来我一直想知道如何实现这一点。
I could do something like (metaprogramming, I would need to modify some other areas):
我可以做类似的事情(元编程,我需要修改其他一些方面):
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
But now I have two checks running in my class, first I use valid?
and then I use a rescue
statement for the data storage constraints.
但现在我在班上运行了两个检查,首先我使用有效?然后我使用救援声明来解决数据存储问题。
Does anyone know of a good way to handle this issue? Would it be better to perhaps write my own validator for this (but then I'd have two queries to the database, where ideally one would be enough).
有谁知道处理这个问题的好方法?或许为此编写我自己的验证器会更好(但后来我对数据库有两个查询,理想情况下一个就足够了)。
2 个解决方案
#1
8
Bryan was kind enough to comment on my question to his blog post. With his help, I've come up with the following custom validator:
布莱恩非常友好地在他的博客文章中评论我的问题。在他的帮助下,我想出了以下自定义验证器:
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
You can use it in your ActiveModel classes like so:
您可以在ActiveModel类中使用它,如下所示:
validates :account_name,
uniqueness: { case_sensitive: false, model: Account, attribute: 'name' }
The only problem you'll have with this, is if your custom model
class has validations as well. Those validations aren't run when you call Signup.new.save
, so you will have to check those some other way. You can always use save(validate: false)
inside the above persist!
method, but then you have to make sure all validations are in the Signup
class, and keep that class up to date, when you change any validations in Account
or User
.
你唯一的问题是,如果你的自定义模型类也有验证。当您调用Signup.new.save时,不会运行这些验证,因此您必须以其他方式检查这些验证。你总是可以在上面的持久化中使用save(validate:false)!方法,但是当您更改帐户或用户中的任何验证时,您必须确保所有验证都在注册类中,并使该类保持最新。
#2
7
Creating a custom validator may be overkill if this just happens to be a one-off requirement.
如果这恰好是一次性要求,那么创建自定义验证器可能会过度。
A simplified approach...
一种简化的方法......
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
unless Account.where(name: account_name).count == 0
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
#1
8
Bryan was kind enough to comment on my question to his blog post. With his help, I've come up with the following custom validator:
布莱恩非常友好地在他的博客文章中评论我的问题。在他的帮助下,我想出了以下自定义验证器:
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
You can use it in your ActiveModel classes like so:
您可以在ActiveModel类中使用它,如下所示:
validates :account_name,
uniqueness: { case_sensitive: false, model: Account, attribute: 'name' }
The only problem you'll have with this, is if your custom model
class has validations as well. Those validations aren't run when you call Signup.new.save
, so you will have to check those some other way. You can always use save(validate: false)
inside the above persist!
method, but then you have to make sure all validations are in the Signup
class, and keep that class up to date, when you change any validations in Account
or User
.
你唯一的问题是,如果你的自定义模型类也有验证。当您调用Signup.new.save时,不会运行这些验证,因此您必须以其他方式检查这些验证。你总是可以在上面的持久化中使用save(validate:false)!方法,但是当您更改帐户或用户中的任何验证时,您必须确保所有验证都在注册类中,并使该类保持最新。
#2
7
Creating a custom validator may be overkill if this just happens to be a one-off requirement.
如果这恰好是一次性要求,那么创建自定义验证器可能会过度。
A simplified approach...
一种简化的方法......
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
unless Account.where(name: account_name).count == 0
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