Các kiểu liên kết Rails trong Ruby

3887
relational database data table related symbol vector illustration concept flat

Rail 5 đang ngày càng đến gần, để chuẩn bị đón chào phiên bản mới này, hãy cùng ôn lại những điểm cơ bản. 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)

Ta sẽ đi vào tất cả các kiểu liên kết chung, và một số tùy chỉnh sâu hơn. Bạn có thể đọc mã nguồn tại GitHub.

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

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

Với phần demo này, bản dùng thử 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:

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):

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ất nhiên, bạn cũng có thể nói:

Áp dụng migration:

Đừ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:

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

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:

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:

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:

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

và mọi thứ sẽ làm việc hoàn hảo.

Hãy chú ý, với liên kết has_many, vẫn còn có tùy chỉnh :class_name: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:

Đ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ể:

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:

Bạn còn có thể set tùy chọn :optional cho quan hệ đơn lẻ:

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ệ:

Đơn giản với user:

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:

Giờ đến table trung gian:

Chú ý tên của table trung gian – Rails muốn tên này gồm tên của hai table (eventsusers). 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:

Đế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: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:

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

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

Tweak hai model khác:

Ở đâ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:

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:

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

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à UserCommentGameComment. 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:

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:

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

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:

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

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

Giờ đến model User và Game:

: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:

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.

Techtalk via sitepoint