+2

Quan hệ của Rails trong Ruby

Với Associations (liên kết), việc thực hiện nhiều phép tính lên các record trong code của bạn trở nên vô cùng dễ dàng. Có nhiều kiểu liên kết bạn có thể sử dụng:

One-to-one (một-một) One-to-many (một-nhiều) Many-to-many (nhiều-nhiều) Polymorphic one-to-many (đa dạng-nhiều)

Liên kết một-nhiều

Để bắt đầu, hãy tạo một Rails app mới:

$ rails new OhMyRelations -T

Với phần demo này, Rails 5 sẽ được sử dụng, nhưng mọi nội dung trong bài có thể dùng cho cả Rails 3 và 4.

Liên kết một-nhiều có lẽ là kiểu liên kết được sử dụng rộng rãi nhất. Ý niệm khá đơn giản: record A có nhiều record B và record B chỉ thuộc về một record A duy nhất. Với mỗi record B, bạn sẽ phải lưu trữ một id của record A sở hữu record B này, id này được gọi là foreign key.

Hãy xem thử trong thực tế. Giả dụ, ta có một user, user này có thể có nhiều post. Đầu tiên, tạo model User:

$ rails g model User name:string

Còn về table post, table này phải chứa một foreign key và theo thông lệ, ta phải đặt tên cột này theo table liên quan. Vậy trong trường hợp này, ta sẽ đặt user_id(chú ý thể số ít):

$ rails g model Post user:references body:text

user:references là cách xác định foreign key nhanh gọn nhất – nó sẽ tự động đặt tên cột tương ứng user_id và thêm index vào đó. Bên trong migration của bạn, bạn sẽ thấy:

t.references :user, foreign_key: true

Tất nhiên, bạn cũng có thể nói:

$ rails g model Post user_id:integer:index body:text

Áp dụng migration:

$ rake db:migrate

Đừng quên rằng bản thân model phải được trang bị những method đặc biệt để thiết lập quan hệ đúng cách. Miễn là chúng ta đã dùng keyword references khi tạo migration. model Post sẽ có sẵn dòng sau:

belongs_to :user

Tuy vậy, phải điều chỉnh thủ công User:


models/user.rb

[...]
has_many :posts
[...]

Chú ý thể số nhiều (“posts”) cho tên quan hệ. Với quan hệ belongs_to, bạn hãy sử dụng thể số ít (“user”).

Không khó lắm đúng không? Khi đã thiết lập quan hệ, bạn có thể sử dụng method như: user.posts – tham chiếu posts của user user.posts << post – thiết lập quan hệ mới giữa một user và một post post.user – tham chiếu người sở hữu post user.posts.build({ }) – khởi tạo post mới cho user, nhưng vẫn chưa lưu vào database. Nhưng có populate user_id attribute trên post. Cái này cũng giống như Post.new({user_id: user.id}). user.posts.create({ }) – tạo post mới và lưu vào database. post.build_user – giống như trên, instantiate (thực thế hóa) user mới mà không lưu post.create_user – giống như trên, instantiate và lưu user vào database Hãy thảo luận một số tùy chọn bạn có thể thiết đặt khi xác định quan hệ. Giả sử ví dụ, bạn muốn quan hệ belongs_tođược gọi là author, chứ không phải user:

models/post.rb

[...]
belongs_to :author
[...]

Tuy nhiên, chỉ thế này vẫn chưa đủ, vì Rails sử dụng tham số :author để suy tên của model được liên kết và foreign key. Miễn là ta không có model tên Author, thì ta phải chỉ định đúng tên của class:

models/post.rb

[...]
belongs_to :author, class_name: 'User'
[...]

Nhưng table posts cũng không có trường author_id nữa, vậy nên ta phải tái định nghĩa tùy chọn :foreign_key:

models/post.rb

[...]
belongs_to :author, class_name: 'User', foreign_key: 'user_id'
[...]

Đến đây, bạn có thể làm thế này trong console:

$ post = Post.new
$ post.create_author

Hãy chú ý, với liên kết has_many, vẫn còn có tùy chỉnh :class_name và :foreign_key dùng được. Còn gì nữa, sử dụng những tùy chọn này, bạn có thể thiết lập một mối quan hệ tại đó model tự tham chiếu như ở đây.

Bạn có thể set một tùy chọn khác là :dependent, thường cho mối quan hệ has_many. Tại sao ta cần đến nó? Giả sử, một người dùng tên John có rất nhiều post. Rồi, bỗng nhiên John bị xóa khỏi database – À hèm, chuyện này có khả năng sảy ra đấy… vậy còn mấy posts của anh ta? Chúng vẫn có cột user_id set sang id của John, nhưng record này không còn tồn tại nữa! Những post này gọi là orphaned (mồ côi) và có thể dẫn đến rất nhiều vấn đề, vậy nên bạn có lẽ sẽ muốn xử lý nhanh những tình huống như thế này đấy.

Tùy chọn :dependent chấp nhận những giá trị sau:

:destroy – tất object được liên kết lần lượt bị loạt bỏ (trong query riêng). Những callbacks phù hợp sẽ chạy trước và sau khi loại bỏ. :delete_all – Tất cả object được liên kết sẽ bị xóa bỏ trong một query duy nhất. Sẽ không có callback nào được thực thi. :nullify – foreign keys cho các objects được liên kết sẽ set về NULL. Sẽ không có callback nào được thực thi. :restrict_with_exception – Nếu có record được liên kết, sẽ xuất hiện exception. :restrict_with_error – Nếu có record liên kết, sẽ thêm một error vào người sỡ hữu (the record bạn đang cố xóa). Vậy, như bạn thấy, có nhiều cách giải quyết tình huống này. Với demo này, tôi sẽ dùng :destroy:

models/user.rb

[...]
has_many :posts, dependent: :destroy
[...]

Điều thú vị ở đây là, belongs_to cũng có hỗ trợ tùy chọn :dependent – tùy chọn này có thể set thành :destroy hoặc :delete). Tuy nhiên, với quan hệ một-nhiều, tôi cực kỳ khuyên bạn không nên set tùy chọn này.

Một điều nữa phải chú ý với Rails 5 là bạn mặc định không thể tạo record nếu không có record mẹ. Nói cách khác, bạn không thể:

Post.create({user_id: nil})

Vì rõ ràng không có user nào như vậy cả.

Tính năng mới này có thể tắt trên cả ứng dụng bằng cách tweak file khởi tạo sau:

config/initializers/active_record_belongs_to_required_by_default.rb
Rails.application.config.active_record.belongs_to_required_by_default = false # default is true
config/initializers/active_record_belongs_to_required_by_default.rb

Rails.application.config.active_record.belongs_to_required_by_default = false # default is true Bạn còn có thể set tùy chọn :optional cho quan hệ đơn lẻ:

belongs_to :author, optional: true

Liên kết một-một

Với quan hệ một-một bạn đang cơ bản nói rằng một record chứa chính xác một instance của một model khác. Ví dụ như, hãy lưu trữ địa chỉ người dùng trong một bảng tách riêng gọi là addresses. Bảng này phải chứa một foreign key, mặc định đặt tên theo quan hệ:

$ rails g model Address street:string city:string country:string user_id:integer:index
$ rake db:migrate

Đơn giản với user:

models/user.rb
has_one :address

Khi đã xong bước này, bạn có thể call một số methods như

user.address – truy xuất địa chỉ liên quan user.build_address – tương tự như method của belongs_to; thực thể hóa (instantiate) địa chỉ mới, nhưng không lưu vào database. user.create_address – thực thể hóa địa chỉ mới, lưu vào database. Quan hệ has_one cho phép bạn xác định :class_name, :dependent, foreign_key, và nhiều tùy chọn khác, giống has_many.

Liên kết nhiều-nhiều

Liên kết “Has and Belongs to Many”

Liên kết nhiều-nhiều hơi phức tạp hơn một chút và có thể được thiết đặt theo hai cách. Đầu tiên, hãy bàn về quan hệ trực tiếp không có models trung gian. Liên kết này gọi là “has and belongs to many” (HABTM).

Giả sử, một user có thể enroll vào nhiều event khác nhau và một event có thể chứa nhiều user. Để đạt được mục tiêu này, chúng ta cần một table riêng biệt (thường gọi là “join table”) chứa quan hệ giữa user và event. Table này phải có một tên đặc biệt: users_events. Về cơ bản, đây chỉ là kết hợp giữa hai tên table mà ta đang tạo quan hệ.

Đầu tiên, tạo events:

$ rails g model Event title:string

Giờ đến table trung gian:

$ rails g migration create_events_users user:references event:references
$ rake db:migrate

Chú ý tên của table trung gian – Rails muốn tên này gồm tên của hai table (events và users). Hơn nữa, tên bậc cao (events) nên đứng trước (events > users, vì chữ cái “e” đứng trước chữ “u”). bước cuối cùng, ta sẽ thêm has_and_belongs_to_many đến cả hai models:

model/user.rb
[...]
has_and_belongs_to_many :events
[...]
model/event.rb
[...]
has_and_belongs_to_many :users
model/user.rb
[...]
has_and_belongs_to_many :events
[...]
model/event.rb
[...]
has_and_belongs_to_many :users
[...]

Đến đây bạn có thể call các method như:

user.events user.events << [event1, event2] – tạo quan hệ giữa một người dùng và một loạt events user.events.destroy(event1) – hủy quan hệ giữa các records (sẽ không xóa records thật). Vẫn còn một delete method có tác dụng tương tự, nhưng lại không chạy được callbacks user.event_ids – một method gọn gàng, giúp trả một array ids từ collection user.event_ids = [1,2,3] – làm collection chỉ chứa các objects do các key values chính (được cung cấp) xác định. Lưu ý, nếu collection ban đầu chứa các objects khác, những objects này sẽ bị loại bỏ. user.events.create({}) – tạo object mới và thêm object vào collection. has_and_belongs_to_many chấp nhận tùy chọn :class_name và :foreign_key mà ta đã bàn đến. Tuy nhiên, has_and_belongs_to_many cũng có hỗ trợ một số tùy chọn khác:

:association_foreign_key – theo mặc định, Rails sử dụng tên quan hệ để tìm foreign key trong table trung gian, table này sẽ dần được sử dụng để tìm object đã được liên kết. Vậy, ví dụ, nếu bạn nói has_and_belongs_to_many :users, cột user_id sẽ được sử dụng. Tuy nhiên, cách này không phải lúc nào cũng quá nhanh, tiện; nên ta có thể dùng :asscosiation_foreign_key để xác định tên của một cột tùy chỉnh. :join_table – có thể dùng tùy chọn này để tái xác định tên cho table trung gian (trong ví dụ của chúng ta là users_events) Tuy vậy, cách xác định liên kết nhiều-nhiều has_and_belongs_to_many vẫn còn khá cứng nhắc vì bạn không thể độc lập làm việc với model quan hệ. Trong nhiều trường hợp, bạn sẽ muốn lưu trữ một số dữ liệu bổ sung cho mỗi quan hệ, hoặc xác định extra callbacks; những nhiệu vụ này không thể hoàn thành với quan hệ HABTM được. Bởi vậy, trước những công việc như thế này, tôi sẽ chia sẻ một cách thức tiện lợi, và hiệu quả hơn.

Liên kết “Has Many Through”

Một cách xác định liên kết nhiều-nhiều nữa là sử dụng loại liên kết has many through. Giả sử ta có một loạt game, và mỗi một đoạn thời gian, những cuộc thi đấu của game này sẽ được tổ chức. Nhiều user có thể tham gia vào nhiều cuộc thi. Bên cạnh việc thiết đặt mối quan hệ nhiều-nhiều giữa user và game, ta còn muốn lưu trữ thông tin bổ sung về mỗi enrollment, như loại cuộc thi (nhiệp dư, semi-pro, pro,…)

Đầu tiên, hãy tạo một model Game mới:

$ rails g model Game title:string

Chúng ta còn cần một table trung gian, nhưng lần này, kèm theo model:

$ rails g model Enrollment game:references user:references category:string
$ rake db:migrate

Với model Enrollment mọi thứ đều được thiết đặt tự động:

models/enrollment.rb
[...]
belongs_to :game
belongs_to :user
[...]

Tweak hai model khác:

models/user.rb
[...]
has_many :enrollments
has_many :games, through: :enrollments
[...]
[...]
models/game.rb
[...]
has_many :enrollments
has_many :users, through: :enrollments
[...]

models/game.rb [...] has_many :enrollments has_many :users, through: :enrollments [...]

Ở đây, ta chỉ định rõ model trung gian để thiết lập quan hệ này. Đến đây bạn có thể làm việc với mỗi enrollment như một thực thể độc lập (vô cùng tiện lợi). Lưu ý, nếu không suy ra được tên liên kết nguồn từ tên liên kết, bạn có thể tận dụng tùy chọn :source và đặt giá trị tương ứng.

Tóm lại, sử dụng has_many :through vẫn tốt hơn là has_and_belongs_to_many. Tuy nhiên, trong nhiều trường hợp đơn giản, bạn vẫn nên “gắn bó” với HABTM.

Liên kết “Has One Through”

Tương tự như phần trước, ý tưởng ở đây mà một model sẽ được ghép mới một model khác thông qua model trung gian. Giả sử một user có một cái ví, và ví này chứa lịch sử thanh toán. Đầu tiên, hãy tạo một model Purse:

$ rails g model Purse user:references funds:integer

user_id chính là foreign key giúp thiết lập quan hệ giữa user và ví. Và giờ đến model PaymentHistory:

$ rails g model PaymentHistory purse:references
$ rake db:migrate

Giờ hãy tweak các model như sau:

models/user.rb
has_one :purse
has_one :payment_history, through: :purse
models/purse.rb
belongs_to :user
has_one :payment_history
models/payment_history.rb
belongs_to :purse
models/user.rb
has_one :purse
has_one :payment_history, through: :purse
models/purse.rb
belongs_to :user
has_one :payment_history
models/payment_history.rb
belongs_to :purse

Loại quan hệ này hiếm khi được dùng tới, nhưng vẫn có chỗ hữu dụng riêng.

Liên kết đa hình

Liên kết đa hình, trái với cái tên “hầm hố”, khái niệm của kiểu liên kết này lại khá đơn giản: bạn có một model có thể thuộc về nhiều model khác nhau trong một liên kết duy nhất. Giả sử, bạn chuẩn bị tạo game và user commentable. Tất nhiên, bạn có thể có hai model độc lập là UserComment và GameComment. Nhưng thành thật mà nói, comment khá tương tự, ngoại trừ việc chúng thuộc vào những model khác nhau. Đây là lúc liên kết đa hình phát huy tác dụng.

Tạo model Comment:

$ rails g model Comment body:text commentable_id:integer:index commentable_type:string

commentable_id chính là foreign key để thết lập quan hệ với các table khác. Dần dần, commentable_type sẽ chứa tên thật của model (có comment tương ứng). Migration:

create_table :comments do |t|
  t.text :body
  t.integer :commentable_id
  t.string :commentable_type

  t.timestamps
end
add_index :comments, :commentable_id

Có thể viết lại thành:

create_table :comments do |t|
  t.text :body
  t.references :commentable, polymorphic: true, index: true

  t.timestamps
end
add_index :comments, :commentable_id

Trước đó, ta đã thấy method references, nhưng lần này nó còn đi với tùy chọn :polymorphic.

Áp dụng migration:

$ rake db:migrate

Comment model sẽ có liên kết belongs_to, nhưng với một thay đổi nhỏ:

models/comment.rb
[...]
belongs_to :commentable, polymorphic: true
[...]

Miễn ta call hai trường :commentable_id và :commentable_type, cả quan hệ phải được gọi là commentable.

Giờ đến model User và Game:

models/user.rb
[...]
has_many :comments, as: :commentable
[...]
models/game.rb
[...]
has_many :comments, as: :commentable
[...]
models/game.rb
[...]
has_many :comments, as: :commentable
[...]

:as là một tùy chọn đặc biệt, giải thích rằng “đây là liên kết đa hình. Giờ, hãy boot console và thử chạy:

$ u = User.create
$ u.comments.create({body: 'test'})

Trong table comments, commentable_type sẽ được set về User, và commentable_id set về id của user. Liên kết đa hình của bạn giờ đây sẽ làm việc trơ tru, và dễ dàng làm các model khác comment được!

Kết luận

Trong bài viết này, chúng ta đã thảo luận nhiều loại liên kết dùng được trong Rails. Ta cũng đã biết cách thiết đặt và tùy chỉnh sâu hơn. Hy vọng, bài viết đã phần nào giúp bạn mở rộng thêm nhiều kiến thức bổ ích.


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í