0

Rails Polymorphic Associations

Chắc chắn đầu tiên chúng ta muốn biết liên kết polymorphic là gì? Nó là nơi mà một mô hình ActiveRecord có thể belong to nhiều hơn một model khác. Như trường hợp chiếc oto của bạn phụ thuộc bạn hay một người nào đó, trong khi các xe khác có thể phụ thuộc nhiều xe hay doanh nghiệp.

1. Tại sao chúng ta cần nó?

Trong rails, ActiveRecord mặc định cách quan hệ giữa các bản ghi trong DB của bạn. Một bản ghi đơn giản truy cập đến một cái khác sẽ đơn giản là chèn tên trước _id (vd: model_name_id). Một User có thể sở hữu một Profile. Nên chúng ta sẽ có cả model UserProfile. Profile sẽ belongs_to phụ thuộc vào UserUser chỉ có một Profile.

# /app/models/user.rb
class User < ActiveRecord::Base
  has_one :profile, dependent: :destroy
end

# /app/models/profile.rb
class Profile < ActiveRecord::Base
  belongs_to :user
end

Model profile trong database cần có cột user_id, từ đó ta sẽ biết được profile đó của user nào. Sử dụng lệnh sau để tạo model Profile một cách tự động:

rails generate model Profile user_id:integer:index name:string:index
rake db:migrate

Bây giờ bạn có thể tạo một profile. Nếu ta đã có User với id là 1 ta có thể tạo profile cho user đó.

Profile.new(
  user_id: 1,
  name: "Master of Lorem Ipsum"
).save

chúng ta đã định nghĩa quan hệ has_onebelongs_to trong models, ta có thể truy cập thông tin profile thông qua biến user.

user = User.first
user.profile.name
# => "Master of Lorem Ipsum"

Và nếu bạn muốn tìm user từ profile bạn có thể dùng lệnh đơn giản như sau:

profile = Profile.where(name: "Master of Lorem Ipsum").first
profile.user.id
# => 1

Bây giờ nếu bạn muốn có các profile phụ thuộc nhiều hơn một loại model thì phải làm sao? Chúng ta sẽ muốn business có profile của riêng họ. Bây giờ chúng ta đang có cột user_id để biết profile đó của user nào, vậy bây giờ chúng ta cũng cần có cột business_id trong Profileđể biết đó là của business nào.

rails generate migration AddBusinessIdToProfile business_id:integer:index
rake db:migrate

Bây giờ, khi chúng ta cập nhật các model, chúng ta có một vấn đề. Bây giờ, nếu ta muốn xem những người sở hữu những profile, ta không còn có thể tra cứu một trường để chắc chắn về nó. Điều gì nếu cả user_id và profile_id có một số trong đó? Điều gì nếu chúng đều rỗng?

Một giải pháp đơn giản là di chuyển tham chiếu từ Profile vào UserBusiness. Vì thế thay vì profile cho biết nó thuộc về ai thì user và business nói về hồ sơ mà họ sở hữu.

Một giải pháp tốt nhất để giải quyết việc này là sử dụng polymorphic.

2. How does it work?

Polymophic thực hiện thay vì sử dụng user_id ta sẽ tách ra name vào một trường và một trường khác cho id. Nên thay vì một phương thức với từ khoá user trong nó. Chúng ta lấy "User" như một giá trị cuả loại sở hữu và chúng ta vẫn giữ tham chiếu ID của "User" đó. Nếu chúng ta muốn thay đổi người sở hữu chúng ta sẽ thay đổi giá trị từ "User" thành "Business" và sau đó cập nhật lại id phù hợp.

Chúng ta sẽ tạo một quan hệ polymophic:

rails g model Profile name:string:index profileable:references{polymorphic}
rake db:migrate

khi chọn một tên tham chiếu nó là thực tế để sử dụng tên đối tượng và thêm vào từ "able" vào cuối. Nên Profile sẽ trở thành profileable. Tham chiếu truy cập đến nguời sở hữu trong model Profile bây giờ sẽ là profileable_idprofileable_type.

Các model của bạn sẽ giống như sau:

# /app/models/user.rb
class User < ActiveRecord::Base
  has_one :profile, as: :profileable, dependent: :destroy
end

# /app/models/business.rb
class Business < ActiveRecord::Base
  has_one :profile, as: :profileable, dependent: :destroy
end

# /app/models/profile.rb
class Profile < ActiveRecord::Base
  belongs_to :profileable, polymorphic: true
end

Bây giờ chúng ta có thể tạo ra các profile như sau:

user = User.first
Profile.new(
  profileable_id: user.id,
  profileable_type: user.class.name,
  name: "Master of Lorem Ipsum"
).save

business = Business.first
Profile.new(
  profileable_id: business.id,
  profileable_type: business.class.name,
  name: "Taco Shell"
).save

Sẽ đơn giản hơn nếu bạn sử dụng liên kết polymorphic. Chúng ta thêm vào một số phương thức để tạo các record. Từ khi chúng ta sử dụng quan hệ has_one, phương thức có thể sử dụng là build_ với class polymorphic của chúng ta. Nên nó là build_profile.

user = User.first
user.build_profile(name: "Master of Lorem Ipsum").save
user.profile.name
# => "Master of Lorem Ipsum"

business = Business.first
business.build_profile(name: "Taco Shell").save
business.profile.name
# => "Taco Shell"

Bây giờ nếu bạn muốn permit nhiều hơn một Profile cho 1 người chúng ta phải làm thế nào? Đó là những người sống hai nơi và những người thuộc các nhóm hoặc tổ chức khác ... Do óó chúng ta cần đến quan hệ has_many.

3. has_many

Các model được sinh ra bằng cách dùng câu lệnh, chúng ta chỉ cần cập nhật quan hệ giữa các model trong thư mục /app/models.

# /app/models/user.rb
class User < ActiveRecord::Base
  has_many :profiles, as: :profileable, dependent: :destroy
end

Bây giờ mọi thứ đã thay đổi. Chúng ta không thể truy cập một profile (user.profile) bởi vì user bây giờ có nhiều profile nên phải sử dụng user.profiles.

user = User.first
user.profiles
# => #<ActiveRecord::Associations::CollectionProxy []>

Ở trên tương ứng với một mảng rỗng. Bây giờ chúng ta sẽ tạo thêm các profile mới. Thay vì lúc trước sử dụng build_profile thì giờ ta chỉ cần gọi build.

user = User.first
user.profiles.build(name: "Master of Lorem Ipsum").save
user.profiles.first.name
# => "Master of Lorem Ipsum"

Bây giờ chúng ta có thể tạo nhiều profile cho mỗi user.

4. Form Nested Attributes

Form để update cho user có thể giống như sau:

<%= form_for(@user) do |f| %>
  email: <%= f.text_field :email %>
  <%= f.submit "Save changes" %>
<% end %>

Biến @user được thiết lập trong controller app/controllers/users_controller.rb

Thông thường, chúng ta muốn có các bản ghi polymorphic mà phụ thuộc User được đưa vào form. Chúng ta có thể tạo một phần profile của user trong form với fields_for. Cho quan hệ has_one với profile, nó sẽ có dạng như sau. Nhưng trước đó ta cần thêm một trường vào model User vì vậy chúng ta có thể chấp nhận các thuộc tính lồng nhau.

# /app/models/user.rb
class User < ActiveRecord::Base
  has_one :profile, as: :profileable, dependent: :destroy
  accepts_nested_attributes_for :profile
end

Form của bạn sẽ có dạng:

<%= form_for(@user) do |f| %>
  email: <%= f.text_field :email %>

  <% f.fields_for :profile do |prf| %>
    <%= prf.text_field :name %>
  <% end %>

  <%= f.submit "Save changes" %>
<% end %>

Bạn có thể tìm hiểu thêm về nested attributes tại đây

Khi tạo hồ sơ mới, bạn sẽ cần phải sử dụng một trong các điều sau đây trong controller của bạn:

# For a single profile
@user = User.new # New User
@user.build_profile

# For the first of multiple profiles
@user = User.new # New User
@user.profiles.build

Có cái này trong controller của bạn sẽ cho phép bạn sử dụng fields_for cho việc tạo các record mới trong form của bạn.

Kết Luận:

Trên đây là một số kiến thức về polimorphic. Cảm ơn mọi người đã theo dõi bài viết. Kiến thức còn hạn hẹp mong mọi người góp ý để bài viết hoàn thiện hơn.

Tham khảo:

http://6ftdan.com/allyourdev/2015/02/10/rails-polymorphic-models/


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí