2013-02-03 19 views
10

W doskonałym blogu Bryana Helmkampa o nazwie "7 Patterns to Refactor Fat ActiveRecord Models", wspomina on o użyciu Form Objects, aby oddzielić formularze wielowarstwowe i przestać używać accepts_nested_attributes_for.na obiekcie ActiveModel, jak sprawdzić wyjątkowość?

Edycja: patrz below dla rozwiązania.

Mam prawie dokładnie duplikowane jego próbkę kodu, jak miałem ten sam problem do rozwiązania:

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 

Jedną z rzeczy, różnych w moim kawałku kodu, jest to, że muszę zweryfikować unikalność nazwy konta (i e-mail użytkownika). Jednak ActiveModel::Validations nie ma walidatora uniqueness, ponieważ ma być wariantem bez bazy danych z obsługą ActiveRecord.

Pomyślałem, istnieją trzy sposoby, aby sobie z tym poradzić:

  • napisać własny sposób to sprawdzić (czuje się zbędny)
  • Dołącz walidacji ActiveRecord :: :: UniquenessValidator (próbowałem, nie dostał go do pracy)
  • Albo dodać ograniczenie w warstwie przechowywania danych

wolałbym użyć ostatni. Ale potem zastanawiam się, jak mogę to zaimplementować.

mógłby zrobić coś podobnego (metaprogramowanie, będę musiał zmodyfikować kilka innych obszarów):

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 

Ale teraz mam dwie kontrole działa w mojej klasie, najpierw używam valid? a następnie użyć instrukcja rescue dla ograniczeń przechowywania danych.

Czy ktoś wie o dobrym sposobie rozwiązania tego problemu? Byłoby lepiej być może napisać do tego mój własny weryfikator (ale wtedy będę miał dwa zapytania do bazy danych, gdzie najlepiej wystarczyłoby).

+0

Jeśli to może pomóc każdemu: w podobnej sytuacji, że to „ActiveRecord :: walidacji” zamiast „ActiveModel :: walidacji” - w ten sposób * validates_uniqueness_of * jest dostępny – Mat

Odpowiedz

8

Bryan był na tyle uprzejmy, że otrzymał comment on my question to his blog post. Z jego pomocą, mam wymyślić następujący niestandardowy walidator:

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 

można go używać w swoich klasach ActiveModel tak:

validates :account_name, 
    uniqueness: { case_sensitive: false, model: Account, attribute: 'name' } 

Jedyny problem będziesz miał z tego, jest jeśli twoja niestandardowa klasa model ma również walidacje. Te sprawdzania poprawności nie są uruchamiane po wywołaniu Signup.new.save, więc będziesz musiał sprawdzić te w inny sposób. Zawsze możesz użyć save(validate: false) w powyższej metodzie persist!, ale musisz upewnić się, że wszystkie sprawdzenia są w klasie Signup i uaktualnić tę klasę, gdy zmienisz jakiekolwiek zatwierdzenia w Account lub User.

+4

Zauważ, że w Rails 4.1, "# setup" jest przestarzałe na walidatorach i zostanie usunięte w wersji 4.2. Zmiana metody na 'initialize' powinna działać tak jak jest. –

7

Tworzenie niestandardowego weryfikatora może być przesadzone, jeśli jest to jednorazowy wymóg.

uproszczonego podejścia ...

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

To będzie działać tylko dla nowych obiektów. Podczas aktualizacji rekordu pojawi się błąd z powodu bieżącego obiektu będącego już w bazie danych. – Hendrik

+1

To jest formularz rejestracyjny, działanie, które występuje tylko raz w cyklu życia danego użytkownika. :) Ale twój punkt jest zrozumiany. Jeśli chciałbyś ponownie użyć tego obiektu formularza, jednym podejściem może być '# find_or_initialize_by', a następnie' #persisted? 'Do obsługi każdego przypadku. Łatwiejszym alternatywnym podejściem byłoby oddzielny obiekt formularza do edycji i aktualizacji utrwalonego obiektu. – crftr