2013-02-03 48 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

我想有三種方式來處理這個問題:

  • 寫我自己的方法來檢查這(感覺冗餘)
  • 包含的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 

但現在我已經在我的課上運行兩次檢查,第一次我用valid?,然後我用一個rescue聲明的數據存儲約束。

有誰知道處理這個問題的好方法?或許爲此寫一個自己的驗證器會更好一些(但是接下來我會對數據庫進行兩次查詢,理想情況下只需要一次)。

+0

如果這可以幫助任何人:在類似的情況下,我包括代替「::加載ActiveModel驗證」「的ActiveRecord ::驗證」 - 這樣* validates_uniqueness_of *是可用 – 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類中,並且在更改AccountUser中的任何驗證時,保持該類最新。

+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

這是一個註冊表單,它只在給定用戶的生命週期中出現一次。 :)但你的觀點是可以理解的。如果你想重用這個表單對象,一種方法可能是'#find_or_initialize_by',然後是'#persisted?'來處理每個事件。一種更簡單的替代方法將是一個單獨的表單對象,用於對持久對象進行編輯和更新。 – crftr

相關問題