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

时间:2022-12-06 07:31:26

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