在ActiveModel对象上,如何检查唯一性?

时间:2013-02-03 07:32:52

标签: ruby-on-rails validation activemodel rails-activerecord

在Bryan Helmkamp的一篇名为" 7 Patterns to Refactor Fat ActiveRecord Models"的优秀博文中,他提到使用Form Objects抽象出多层表格并停止使用{{1} }。

修改:请参阅below了解解决方案。

我几乎完全重复了他的代码示例,因为我有同样的问题需要解决:

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 :: Validations :: UniquenessValidator(试过这个,没有让它工作)
  • 或在数据存储层中添加约束

我更愿意使用最后一个。但后来我一直想知道 我将如何实现这一点。

我可以做一些像(元编程,我需要修改其他一些区域)

ActiveRecord

但是现在我在我的班级中运行了两个检查,首先我使用 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?语句来处理数据存储限制。

有谁知道处理这个问题的好方法?或许为此编写我自己的验证器会更好(但是我对数据库有两个查询,理想情况下一个就足够了)。

2 个答案:

答案 0 :(得分:8)

布莱恩对comment on my question to his blog post很友善。在他的帮助下,我想出了以下自定义验证器:

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课程中,并且在您更改任何课程时保持该课程的最新状态AccountUser中的验证。

答案 1 :(得分:8)

如果这恰好是一次性要求,那么创建自定义验证器可能会过度。

简化方法......

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