14

ブライアンヘルムカンプの優れたブログ投稿「ファットActiveRecordモデルをリファクタリングするための7つのパターン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

私のコードの違いの1つは、アカウント名(およびユーザーの電子メール)の一意性を検証する必要があることです。ただし、データベースにバックアップされていないのバリアントであると想定されているためActiveModel::Validations、バリデーターはありません。uniquenessActiveRecord

これを処理する方法は3つあると思いました。

  • これをチェックするための独自のメソッドを作成します(冗長に感じます)
  • 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

しかし、今ではクラスで2つのチェックを実行しています。最初に使用valid?し、次にrescueデータストレージ制約のステートメントを使用します。

この問題を処理する良い方法を知っている人はいますか?このために独自のバリデーターを作成する方がよいでしょうか(ただし、データベースに対して2つのクエリがあり、理想的には1つで十分です)。

4

2 に答える 2

12

カスタムバリデーターの作成は、これが1回限りの要件である場合、やり過ぎになる可能性があります。

簡略化されたアプローチ...

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
于 2013-04-23T18:28:35.317 に答える
9

ブライアンは親切にも彼のブログ投稿への私の質問にコメントしてくれました。彼の助けを借りて、私は次のカスタムバリデーターを思いついた。

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

于 2013-02-03T11:29:41.687 に答える