12/08/2018, 13:52

Validating Nested Associations in Rails

Intro Rails cung cấp cho chúng ta rất nhiều những tuỳ chọn để tạo ra nhiều form cho model. Đơn gỉan nhất là form cho một đối tượng, phức tạp hơn là form cho nhiều đối tượng liên quan tới nhau (thường là mối quan hệ cha con). Chúng ta sẽ bắt đầu với một ví dụ sau: class Company < ...

Intro

Rails cung cấp cho chúng ta rất nhiều những tuỳ chọn để tạo ra nhiều form cho model. Đơn gỉan nhất là form cho một đối tượng, phức tạp hơn là form cho nhiều đối tượng liên quan tới nhau (thường là mối quan hệ cha con). Chúng ta sẽ bắt đầu với một ví dụ sau:

class Company < ActiveRecord::Base
  attr_accessible :name, :offices_attributes
  validates :name, presence: true
  has_many :offices
  accepts_nested_attributes_for :offices, allow_destroy: true
end

class Company::Office < ActiveRecord::Base
  attr_accessible :company_id, :name
  validates :name, presence: true
  belongs_to :company
end

Bằng việc thêm accepts_nested_attributes_for ta có thể truy cập các thuộc tính của Office thông qua Company.

> c = Company.create(name: 'Framgia')
>   => #<Company id: 1, name: "Framgia", created_at: 2016-09-22 21:16:44", updated_at: "2016-09-22 21:16:44">

# add two new offices
> c.offices_attributes = [{ name: 'Hanoi' }, { name: 'HCM }]
>   => [{:name=>"Hanoi"}, {:name=>"HCM"}]
> c.save
> c.offices
>   => [#<Company::Office id: 1, company_id: 1, name: "Hanoi", created_at: "2016-09-22 20:21:54", updated_at: "2016-09-22 20:21:54">, #<Company::Office id: 2, company_id: 1, name: "HCM", created_at: "2016-09-22 20:21:54", updated_at: "2016-09-22 20:21:54">]

# edit office in North America
> c.offices_attributes = [{ id: 1, name: "Ha Noi" }]
>   => [{:id=>1, :name=>"Ha Noi"}]
> c.save
> c.offices
>   => [#<Company::Office id: 1, company_id: 1, name: "Ha Noi", created_at: "2016-09-22 20:21:54", updated_at: "2016-09-22 20:25:18">, #<Company::Office id: 2, company_id: 1, name: "HCM", created_at: "2016-09-22 20:21:54", updated_at: "2016-09-22 20:21:54">]

# delete an office in Europe
> c.offices_attributes = [{ id: 2, _destroy: '1' }]
>   => [{:id=>2, :_destroy=>"1"}]
> c.save
> c.offices
>   => [#<Company::Office id: 1, company_id: 1, name: "Ha Noi", created_at: "2016-09-22 20:21:54", updated_at: "2016-09-22 20:25:18">]

Validating nested attributes :reject_if

class Company < ActiveRecord::Base
  attr_accessible :name, :offices_attributes
  validates :name, presence: true
  has_many :offices
  accepts_nested_attributes_for :offices, allow_destroy: true, reject_if: :office_name_invalid

private
def office_name_invalid(attributes)
  # office name shouldn't start with underscore
  attributes['name'] =~ /A_/
  end
end

Phương thức trên sẽ trả về gía trị true (reject record) hoặc false.

> c.offices_attributes = [{ id: 1, name: '_Ha Noi'}]
>   => [{:id=>1, :name=>"_Ha Noi"]
> c.save
> c.offices # no changes
>   => [#<Company::Office id: 1, company_id: 1, name: "Ha Noi", created_at: "2016-09-22 20:21:54", updated_at: "2016-09-22 20:46:22">]

Hoặc chúng ta có thể sử dụng Proc

Validating count of the nested attributes

Xem xét ví dụ trên, mỗi một công ty (company) thường có ít nhất một trụ sở (office).

class Company < ActiveRecord::Base
  OFFICES_COUNT_MIN = 1

  attr_accessible :name, :offices_attributes
  validates :name, presence: true
  validate do
    check_offices_number
  end
  has_many :offices
  accepts_nested_attributes_for :offices, allow_destroy: true

  private
  def offices_count_valid?
    offices.count >= OFFICES_COUNT_MIN
  end

  def check_offices_number
    unless offices_count_valid?
    errors.add(:base, :offices_too_short, :count => OFFICES_COUNT_MIN)
    end
  end
end

Nhưng ở đây có một vấn đề trong accepts_nested_attributes_for gọi destroy sau khi check validate. Nghĩa là người dùng vẫn có thể xoá hết office.

> c.offices_attributes = [{ id: 1, _destroy: '1' }]
>   => [{:id=>1, :_destroy=>"1"}]
> c.save
> c.offices
>   => []

Chúng ta thử sử dụng length như validates :offices, length: { minimum: OFFICES_COUNT_MIN }. Nó vẫn chạy nhưng sẽ không đếm những office được đánh dấu destroy.

class Company < ActiveRecord::Base
  ...

  private
  def offices_count_valid?
    offices.reject(&:marked_for_destruction?).count >= OFFICES_COUNT_MIN
  end
end

Phương thức này đánh dấu những bản ghi offices cùng với thuộc tính _destroy khi xoá. Khi check validate số lượng office thì tất cả các bản ghi có liên quan tới offices đều bị check. Và điều ta cần làm là chọn ra những bản ghi đã bị đánh dấu xoá.

> c.offices_attributes = [{ id: 1, _destroy: '1' }]
>   => [{:id=>1, :_destroy=>"1"}]
> c.save
> c.errors
>   => #<ActiveModel::Errors:0x000000038fc840 @base=#<Company id: 1, name: "Ha Noi", created_at:2016-0922 20:16:44", updated_at: "2016-09-22 20:16:44">, @messages={:base=>["Company should have at least one office."]}>

Validating presence of the parent object Điều cuối cùng là việc kiểm tra presence trong nested attributes.

class Company::Office < ActiveRecord::Base
  attr_accessible :company_id, :name

  validates :name, presence: true
  # add validator to company
  validates :company, presence: true

  belongs_to :company
end

Chúng ta luôn muốn rằng office phải ở trong một công ty tuơng ứng. Nhưng khi tạo một company thì việc này lại fail.

> c = Company.create(name: 'Framgia Inc', offices_attributes: [{ name: 'lab' }])
>   => #<Company id: nil, name: "Framgia Inc", created_at: nil, updated_at: nil>
> c.errors
>   => #<ActiveModel::Errors:0x000000036387a8 @base=#<Company id: nil, name: "Framgia Inc", created_at: nil, updated_at: nil>, @messages={:"offices.company"=>["can't be blank"]}>

Giải pháp ở đây là sử dụng inverse_of. Nó thường không được dùng trong polymorphic mà chỉ trong belongs_to, has_one và has_many.

class Company < ActiveRecord::Base
  ...
  has_many :offices, inverse_of: :company
  ...
end

class Company::Office < ActiveRecord::Base
  ...
  belongs_to :company, inverse_of: :offices
  ...
end

Và giờ chúng ta có thể create được company.

> c = Company.create(name: 'Framgia Inc', offices_attributes: [{'lab' }])
>   => #<Company id: 2, name: "Framgia Inc", created_at: "2016-09-22 21:07:07", updated_at: "2016-09-22 21:07:07">
> c.offices
>   => #<Company::Office id: 6, company_id: 2, name: "lab", created_at: "2016-09-22 21:07:07", updated_at: "2016-09-22 21:07:07">

0