在ActiveModel对象上,如何检查唯一性?
在Bryan Helmkamp的优秀博客文章“ 7个模式来重构Fat ActiveRecord模型 ”中,他提到使用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
我的代码中不同的一点是,我需要validation帐户名称(和用户电子邮件)的唯一性 。 但是, ActiveModel::Validations
没有uniqueness
validation器,因为它应该是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
但现在我在class上运行了两个检查,首先我使用valid?
然后我使用rescue
声明来解决数据存储问题。
有谁知道处理这个问题的好方法? 或许为此编写我自己的validation器会更好(但之后我会对数据库进行两次查询,理想情况下一个就足够了)。
布莱恩非常友好地在他的博客文章中评论我的问题 。 在他的帮助下,我想出了以下自定义validation器:
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
类也有validation。 当您调用Signup.new.save
,不会运行这些validation,因此您必须以其他方式检查这些validation。 你总是可以在上面的persist!
使用save(validate: false)
persist!
方法,但是当您更改Account
或User
任何validation时,您必须确保所有validation都在Signup
类中,并使该类保持最新。
如果这恰好是一次性要求,那么创建自定义validation器可能会过度。
一种简化的方法……
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