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">