Validating Nested Associations in Rails
Bài đăng này đã không được cập nhật trong 8 năm
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">
All rights reserved