2013-02-03 12 views
10

Trong bài đăng blog tuyệt vời của Bryan Helmkamp có tên "7 Patterns to Refactor Fat ActiveRecord Models", anh đề cập đến việc sử dụng Form Objects để trừu tượng các biểu mẫu nhiều lớp và ngừng sử dụng accepts_nested_attributes_for.trên đối tượng ActiveModel, làm cách nào để kiểm tra tính duy nhất?

Chỉnh sửa: xem below để biết giải pháp.

Tôi đã gần như chính xác nhân đôi mẫu mã của mình, như tôi đã có cùng một vấn đề để giải quyết:

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 

Một trong những điều khác nhau trong mảnh của tôi về mã, là tôi cần phải xác nhận tính duy nhất của tên tài khoản (và email người dùng). Tuy nhiên, ActiveModel::Validations không có trình xác thực uniqueness vì nó được cho là biến thể không được cơ sở dữ liệu được hỗ trợ là ActiveRecord.

I figured có ba cách để xử lý này:

  • Viết phương pháp riêng của tôi để kiểm tra này (cảm thấy không cần thiết)
  • Bao gồm ActiveRecord :: validations :: UniquenessValidator (cố gắng này, đã không nhận được nó hoạt động)
  • Hoặc thêm các hạn chế trong lớp lưu trữ dữ liệu

tôi muốn sử dụng cuối cùng. Nhưng sau đó tôi vẫn tự hỏi cách Tôi sẽ thực hiện điều này.

tôi có thể làm một cái gì đó giống như (Lập trình meta, tôi sẽ cần phải sửa đổi một số lĩnh vực khác):

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 

Nhưng bây giờ tôi đã hai kiểm tra chạy trong lớp học của tôi, lần đầu tiên tôi sử dụng valid? và sau đó tôi sử dụng một tuyên bố rescue cho các ràng buộc lưu trữ dữ liệu.

Có ai biết cách tốt để xử lý vấn đề này không? Nó có thể là tốt hơn để có thể viết validator của riêng tôi cho điều này (nhưng sau đó tôi muốn có hai truy vấn cơ sở dữ liệu, nơi lý tưởng nhất là đủ).

+0

Nếu đây có thể giúp bất cứ ai: trong một tình huống tương tự như tôi bao gồm "ActiveRecord :: validations" thay vì "ActiveModel :: validations" - theo cách này * validates_uniqueness_of * là có sẵn – Mat

Trả lời

8

Bryan đã tử tế với comment on my question to his blog post. Với sự giúp đỡ của mình, tôi đã đi lên với validator tùy chỉnh sau:

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 

Bạn có thể sử dụng nó trong các lớp học ActiveModel bạn như vậy:

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

Vấn đề duy nhất mà bạn sẽ phải với điều này, là nếu lớp tùy chỉnh model của bạn cũng có hiệu lực. Những xác thực này không chạy khi bạn gọi Signup.new.save, vì vậy bạn sẽ phải kiểm tra các cách xác thực đó theo cách khác. Bạn luôn có thể sử dụng save(validate: false) bên trong phương thức persist! ở trên, nhưng sau đó bạn phải đảm bảo tất cả các xác thực nằm trong lớp Signup và giữ cho lớp đó được cập nhật, khi bạn thay đổi bất kỳ xác thực nào trong Account hoặc User.

+4

Lưu ý rằng trong Rails 4.1, '# setup' không được chấp nhận trên trình xác thực và sẽ bị xóa trong phiên bản 4.2. Thay đổi phương thức thành 'initialize' sẽ hoạt động như là. –

7

Tạo trình xác thực tùy chỉnh có thể quá mức cần thiết nếu điều này chỉ xảy ra là yêu cầu một lần.

Một cách tiếp cận đơn giản ...

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

Điều này sẽ chỉ hoạt động đối với các đối tượng mới. Khi cập nhật bản ghi, bạn sẽ nhận được một lỗi do đối tượng hiện tại đã có trong cơ sở dữ liệu. – Hendrik

+1

Đây là biểu mẫu Đăng ký, một hành động chỉ xảy ra một lần trong vòng đời của người dùng nhất định. :) Nhưng điểm của bạn được hiểu. Nếu bạn đang tìm cách sử dụng lại đối tượng biểu mẫu này, một cách tiếp cận có thể là '# find_or_initialize_by' theo sau là' #persisted? 'Để xử lý từng trường hợp. Một cách tiếp cận thay thế dễ dàng hơn sẽ là một đối tượng biểu mẫu riêng biệt để chỉnh sửa và cập nhật đối tượng được tiếp tục tồn tại. – crftr