nested_attributes
là 1 tính năng rất mạnh của Rails, được bắt đầu build từ 31/01/2009, cho tới nay, nó đã trở nên khá hoàn hảo với Rails.
Tuy nhiên, không phải lúc nào sử dụng nested_attributes
cũng dễ dàng và đúng với mong muốn của người coder, nhất là khi người coder muốn sử dụng nested_attributes
với 1 hệ thống đầu vào không hoàn toàn đúng theo chuẩn của nested_attributes
Bài toán khó đặt ra là việc sửa lại nested_attributes
mặc định của Rails để phù hợp với mục đích sử dụng của chúng ta, thì việc vận hành, bảo trì và nâng cấp phiên bản cho hệ thống sẽ trở nên nhiều rủi ro hơn, và phức tạp hơn.
Bài viết dưới đây là 1 cách sử dụng những thứ cơ bản có sẵn của Rails để hỗ trợ nested_attributes
trong 1 trường hợp đặc biệt nhưng rất hay xảy ra trong các dự án Rails: Sử dụng getter/setter một cách mềm dẻo để giải quyết vấn đề đầu vào của nested_attributes
không theo chuẩn Parent-Child
.
Rails Setter/Getter
Một setter của Rails được viết dạng
def xfunction= xvalue
@xvalue = xvalue
end
và một getter có dạng
def xvalue
@xvalue
end
Rails sử dụng cặp setter/getter default là write_attribute
và read_attribute
kiểu
write_attribute :xattr, xvalue
# attr= value
read_attribute :xattr
# return xattr
Cặp setter/getter này được sử dụng trong attr_accessible
(Rails3) và attr_accessor
. Tuy nhiên cặp setter/getter này lưu giá trị theo kiểu Hash
, rất khó cho chúng ta sử dụng
write_attribute :xattr, xvalue
thực chất là
self[:xattr] = xvalue
và
read_attribute :xattr
bản chất là
return self[:xattr]
Chính vì thế, đối với attr_accessor
, Rails sử dụng cặp setter/getter của Ruby, đúng theo OOP
để chúng ta dễ sử dụng hơn là attr_writer
và attr_reader
Có thể hiểu attr_writer :xattr
là
def xattr= xvalue
@xattr = xvalue
end
và attr_reader :xattr
là
def xattr
return @xattr
end
Như vậy là có sự khác nhau giữa các cặp setter/getter này. Việc sử dụng chúng hợp lý sẽ giúp chúng ta rất nhiều trong việc custom lại một số feature của Rails mà không phá vỡ kiến trúc của nó.
nested_attributes
nested_attributes
là 1 feature mạnh của Rails, nó
- Nhanh
- Mạnh trong việc quản lý đồng bộ objects (set of objects)
nhưng nó
- Khó trong việc quản lý giá trị nhập vào
- Khó trong hình dung phương thức giới hạn, call_backs, validates cho các trường
Vì thế, thay vì phải sử dụng 1 loạt call_backs
cho nested_attributes
nằm ở cả 2 phần Cha và Con của bộ objects, chúng ta có thể rewrite lại setter/getter cho nested_attributes
để thực hiện công việc của mình. Thông thường, ta chỉ nên rewrite lại setter của nó.
class XXX < ActiveRecord::Base
attr_accessible :xattr
has_many :xattrs
accepts_nested_attributes_for :xattrs
def xattrs_attributes= args
# Do some addition logic here
# Raise errors, rescue if needed, then call
# assign_nested_attributes_for_collection_association
# or
# assign_nested_attributes_for_one_on_one_association
# depend on your association type is has_many or has_one
assign_nested_attributes_for_collection_association :xattrs, args
end
end
Với cách viết này, ta có thể chuyển toàn bộ các call_backs vào block logic bên trong setter của nested_attributes
. Như vậy sẽ chỉ phải viết 1 block logic, xử lý 1 lần cho toàn bộ association.
Sử dụng linh hoạt nested_attributes
Qua phần 1 và 2, ta có thể hiểu sơ qua được cặp setter/getter và nested_attributes
. Vậy sử dụng mềm dẻo setter/getter trong nested_attributes
để làm gì?
- Khi project yêu cầu mức độ bảo mật cao hơn, bạn phải đưa việc load/init objects vào trong model ( không load/init objects trong controller giống như Rails document)
- Khi bạn phải làm việc với bộ objects không thật sự theo chuẩn
Parent-Child
mànested_attributes
yêu cầu, khi đó vấn đề xuất hiện
nested_attributes
không hoạt động khiParent Object
chưa được save- Rất khó để quản lý các association, vì phải làm việc với 3 hoặc hơn các Class liên quan
- Rất khó và rất yếu về bảo mật khi bạn chỉ muốn làm việc với 1 lượng hữu hạn được chỉ định các objects
Vì thế, việc rewrite setter của nested_attributes
là 1 phương thức khả quan hơn cho vấn đề này.
Ex: 1 mối quan hệ được mô tả theo diagram dưới đây
class Team < ActiveRecord::Base
has_many :members
has_many :time_tables
end
class Member < ActiveRecord::Base
belongs_to :team
has_many :schedules
end
class TimeTable < ActiveRecord::Base
attr_accessible :schedules
belongs_to :team
has_many :schedules
has_many :members, through: :team
accepts_nested_attributes_for :schedules
end
class Schedule < ActiveRecord::Base
belongs_to :time_table
belongs_to :member
end
với yêu cầu
- Một
Member
chỉ có 1Schedule
trong 1TimeTable
- Có thể tạo mới được
Schedule
ngay cả khiTimeTable
của nó chưa có- Có thể update/create
Schedule
cho 1 lượng chỉ định cácMember
Vấn đề trở nên phức tạp khi có mối quan hệ has_many :members, through: :team
và phải đảm bảo yêu cầu của dự án. Ta có thể đảm bảo security bằng việc validate cho scope(:member_id, :schedule_id)
trong TimeTable
, nhưng không đảm bảo được yêu cầu 2. Như thế, 1 loạt call_backs
và validates
được tạo ra ở 3 class Member
, TimeTable
, Schedule
và rất khó để kiểm soát tất cả.
Rewrite setter cho schedules_attributes
là phương án khả quan hơn.
Custom nested_attributes
Có thể mô tả quá trình viết lại nested_attributes
cho bài toán trên như sau
-
Middle Object
: object phá vỡ mối quan hệ Parent-child- Viết lại
nested_attributes
- Sử dụng một
attr_accessor
làm cầu nối - Trong setter của
nested_attributes
, ta lấy ra object cha hoặc tạo mới nó, raise error hoặc bỏ qua cả bộ giá trị nếu quá trình này gặp lỗi
- Viết lại
Child Object
: đối tượng chính củanested_attributes
- Sử dụng 1
attr_writer
đóng vai trò setter cho 1 trường có giá trị tương đương vớiParent Object
-> có khả năng tìm đượcParent Object
thông qua trường này - Sử dụng 1
instance method
như 1 getter cho setter ở trên
- Sử dụng 1
Parent Object
:- Sử dụng
instance method
làm getter - Gọi
Middle Object
thông quaattr_accessor
của nó nếu cần
- Sử dụng
Để dễ hiểu hơn về mô hình này, các bạn có thể tham khảo code của bài toán trên tại
https://github.com/yeuem1vannam/
Như vậy, những thao tác cần thiết cho việc rewrite lại nested_attributes
ở trên là
class Member < ActiveRecord::Base
belongs_to :team
has_many :schedules
attr_accessor :schedule_in_table
def schedules_attributes= attributes
# Logic block xử lý việc load/create Parent Object
# Raise errors hoặc next nếu có lỗi khi khởi tạo Parent Object
assign_nested_attributes_for_collection_association(:schedules, attributes)
end
end
class chedule < ActiveRecord::Base
belongs_to :time_table
belongs_to :member
attr_writer :date
def date
# Xử lý việc load giá trị trường date của Parent Object
# @date được tạo ra tự động bởi attr_writer
end
end
class TimeTable < ActiveRecord::Base
attr_accessible :schedules
belongs_to :team
has_many :schedules
has_many :members, through: :team
accepts_nested_attributes_for :schedules
def getter_method
# Load giá trị của attr_accessor của Middle Object
end
end
Với quá trình rewrite này, sẽ chỉ cần 1 block logic để xử lý toàn bộ mối quan hệ phức tạp này, đồng thời nested_attributes
vẫn có thể làm việc bình thường được.
Conclusion
Việc rewrite nested_attributes
chỉ nên dùng khi bài toán phức tạp và khó kiểm soát bởi nhiều call_backs
. Ta có thể tóm lược các ưu nhược điểm của phương pháp này như sau
- Cons:
- Phải thao tác đồng bộ 3 class
- Khó trong việc hình dung logic và luồng dữ liệu
- Pros:
- Chỉ sử dụng 1 block logic để kiểm soát toàn bộ quan hệ, không cần phải viết thêm các
call_backs
để xử lý riêng chonested_attributes
- Mạnh trong việc kiểm soát việc sai sót dữ liệu
- Nhanh, không cần thêm
call_backs
, khiParent Object
có lỗi, quá trình lập tức được hủy, chưa cần xử lý gì ởChild Object
- Không phá vỡ kiến trúc mặc định của Rails, chỉ sử dụng những thứ có sẵn trong Rails
- Chỉ sử dụng 1 block logic để kiểm soát toàn bộ quan hệ, không cần phải viết thêm các
References
Tài liệu SlideShare của bài viết
SlideShare – Dynamically using setter/getter in Rails – PhuongLH
- http://api.rubyonrails.org/
- http://apidock.com/
- http://stackoverflow.com/
- http://doblock.com/
- http://www.cowboycoded.com/
- lib/active_record/nested_attributes.rb:333