2013-02-03 6 views
10

Bryan Helmkampの優れたブログ記事「7 Patterns to Refactor Fat ActiveRecord Models」では、Form Objectsを使用して、複数レイヤーのフォームを抽象化し、accepts_nested_attributes_forの使用をやめるように言及しています。ActiveModelオブジェクトで、一意性をチェックするにはどうすればよいですか?

編集:解決策はbelowを参照してください。私は解決するために同じ問題を抱えていたよう

は、私はほぼ正確に、彼のコードサンプルを複製しました:コードの私の作品では異なるものの

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の非データベースバッキングバリアントであるためです。私はこれを処理するための3つの方法があります考え出し

  • (冗長な感じ)これを確認するために自分自身のメソッドを書くのActiveRecord ::検証を含める:: 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つあれば十分でしょう)。

+0

これは誰を助けることができる場合は、次の似たような状況で、私は含める「のActiveRecord ::の検証」の代わりに「ActiveModel ::検証」 - このように* validates_uniqueness_of *ですavailable – Mat

答えて

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に電話したときに実行されないので、他の方法でそれらをチェックする必要があります。上記のpersist!メソッド内ではいつでもsave(validate: false)を使用できますが、すべてのバリデーションがSignupクラスにあることを確認し、バリデーションをAccountまたはUserに変更すると、そのクラスを最新の状態に保つ必要があります。

+4

Rails 4.1では、バリデーションで '#setup'が廃止され、4.2では削除されることに注意してください。メソッドを 'initialize'に変更すると、そのまま動作するはずです。 –

7

カスタムバリデーターを作成することは、一度限りの必要条件である場合には、残念です。

単純化されたアプローチ...

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 
+0

これは新しいオブジェクトに対してのみ機能します。レコードを更新すると、現在のオブジェクトがすでにデータベースに存在するため、エラーが発生します。 – Hendrik

+1

これはサインアップフォームであり、特定のユーザーのライフサイクルで1回だけ発生するアクションです。 :)しかしあなたのポイントは理解されています。このフォームオブジェクトを再利用しようとしている場合、1つのアプローチは、それぞれのケースを処理するために '#find_or_initialize_by'の後に' #persisted? 'をつけることです。より簡単な代替アプローチは、永続オブジェクトの編集と更新のための別のフォームオブジェクトです。 – crftr

関連する問題