Các kiểu liên kết trong Rails
Hôm nay, bài viết sẽ nói về ActiveRecord associations. 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) ...
Hôm nay, bài viết sẽ nói về ActiveRecord associations.
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)
1. Liên kết một - nhiều
Để bắt đầu, hãy tạo một Rails app mới:
$ rails new OhMyRelations
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
Bạn cũng có thể dùng:
$ rails g model Post user_id:integer:index body:text
Đừng quên chạy lệnh migration:
$ rake db:migrate
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, ta vẫn 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”).
Khi thiết lập xong, các bạn có thể sử dụng các phương thức sau:
- 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
Bạn có thể thay đổi một số tùy chọn cho mối 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:
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"
2. 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.
3. Liên kết nhiều-nhiều
3.1 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”.
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ờ tạo bảng liên kết user và event:
$ 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 (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
Đế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.
- user.events.create({}) – tạo object mới và thêm object vào collection.
3.2 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 (nghiệ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 [...]
Ở đâ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 sử dụng HABTM.
4. 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à parse. 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
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.
5.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 gọi 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 [...]
: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.
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.