←
Quay lại
01/04/2026
•
6 phút
•
has_one hay belongs_to?
Có bao giờ bạn băn khoăn nên tạo quan hệ 1-1 cho hai model trong Rails như thế nào chưa? Model này nên
has_one hay belongs_to model kia? Hôm nay mình gặp đúng trường hợp giống vậy. Hãy cùng mình tìm hiểu cách giải quyết nhé!Vấn đề
Blog này là nơi đăng tải những bài viết linh tinh cũng như nhật ký đọc sách của mình. Về mặt dữ liệu, nó có một model tên
Xét ra thì một quyển sách có thể có nhiều bài review nhưng giờ mình chỉ cần một thôi và một bài review thì khó mà áp dụng cho nhiều sách nhỉ, nên quan hệ sẽ là
Post đại diện cho bài viết (như bài này chẳng hạn) chứa các thông tin như tên, tag, trạng thái,... trong một cột tương ứng, riêng nội dung mình dùng ActionText để hỗ trợ định dạng văn bản. Một model khác tên Book cho sách. Sau khi đọc xong một cuốn sách mình sẽ viết review cho nó. Ban đầu mình lưu vào một cột riêng kiểu text nhưng mình cũng muốn định dạng văn bản giống các bài viết nên nghĩ tới việc dùng luôn Post là review. Vậy là giờ Book và Post quan hệ với nhau.Xét ra thì một quyển sách có thể có nhiều bài review nhưng giờ mình chỉ cần một thôi và một bài review thì khó mà áp dụng cho nhiều sách nhỉ, nên quan hệ sẽ là
1🡘1. Thế thì cái nào sẽ belongs_to cái nào? Bạn thử suy nghĩ và trả lời trước đã nhé.Phân tích
Sách có một bài review hay sách thuộc về một bài review nào đó? Trước khi tìm câu trả lời mình nhắc bạn nhớ điều này:
Sách này có một bài review, nghe cũng khá thuận đúng không? Nhưng nếu sách không có bài review thì cũng chả sao. Cuốn sách có trước, review có sau. Còn nếu nghĩ bài review có một sách, không có sách thì bài review không còn là bài review nữa, vì khi đó nó review cho cái gì? Vậy sách là chủ thể chính còn bài review chỉ là phái sinh. Nếu đặt khoá ngoại là
Còn một hướng khác suy nghĩ về hậu quả. Giả sự đặt
Vào một ngày đẹp trời nào đó mình muốn review Phim hay một thú vui nào đó thì sao? Model mới sẽ có cột
Suy cho cùng khoá ngoại nên là
Quá trình phân tích trên tuy hơi rắc rối nhưng dần dà bạn sẽ quen và tạo được quy trình suy nghĩ, áp dụng sẽ nhanh hơn. Câu trả lời trước đó của bạn có giống với mình không? Nếu không có thể để lại bình luận để mình tham khảo với nhé.
belongs_to luôn nằm ở model có bảng chứa khoá ngoại còn has_one tất nhiên nằm ở model còn lại. Vậy bài toán trở thành nên đặt khoá ngoại tên gì và ở đâu? Là book_id hay post_id?Sách này có một bài review, nghe cũng khá thuận đúng không? Nhưng nếu sách không có bài review thì cũng chả sao. Cuốn sách có trước, review có sau. Còn nếu nghĩ bài review có một sách, không có sách thì bài review không còn là bài review nữa, vì khi đó nó review cho cái gì? Vậy sách là chủ thể chính còn bài review chỉ là phái sinh. Nếu đặt khoá ngoại là
post_id ngụ ý sách phụ thuộc vào bài review thì không đúng còn book_id thì trỏ tới quyển sách mà bài review đó đang viết về. Không có book_id review trở thành bài viết bình thường.Còn một hướng khác suy nghĩ về hậu quả. Giả sự đặt
post_id ở Book, nếu sau này mình đọc lại sách và muốn viết một bài review khác (kiểu lần 2 đọc sách mới nhận ra điều khác) thì khi đó phải làm sao? Sách đã gắn cố định với post_id và không thể gắn thêm một post_id khác, nhưng nếu đặt book_id ở Post thì có thể tạo thêm một review khác với book_id như cũ.Vào một ngày đẹp trời nào đó mình muốn review Phim hay một thú vui nào đó thì sao? Model mới sẽ có cột
post_id, nó làm phân mảnh dữ liệu. Khi muốn liệt kê tất cả bài review phải gom hết post_id trong các model. Trong khi nếu lưu khoá ngoại ở Post mình chỉ cần khai báo đa hình (Polymorphic Association) với cặp khoá reviewable_id, reviewable_type là có thể liệt kê hết review rồi.Suy cho cùng khoá ngoại nên là
book_id và đặt ở model Post. Thành ra Post belongs_to Book và Book has_one Post.Quá trình phân tích trên tuy hơi rắc rối nhưng dần dà bạn sẽ quen và tạo được quy trình suy nghĩ, áp dụng sẽ nhanh hơn. Câu trả lời trước đó của bạn có giống với mình không? Nếu không có thể để lại bình luận để mình tham khảo với nhé.
Code dễ đọc
Mình muốn nói thêm về một điểm thú vị của Rails/Ruby nữa. Nó luôn hỗ trợ để mình viết code dễ đọc nhất. Dưới đây là code quan hệ:
class Post < ApplicationRecord belongs_to :book, optional: true # có là review, không thì là blog end class Book < ApplicationRecord has_one :post, dependent: :nullify # xoá sách vẫn giữ bài review end
Để lấy bài review của một quyển sách, code cần viết là
book.post. Code chạy đúng nhưng đọc lên nghe không thuận miệng lắm, nếu là book.review sẽ hay hơn. Rails hỗ trợ chuyện đó. has_one cho phép đặt tên quan hệ tuỳ ý miễn trỏ đúng class là được.has_one :review, class_name: "Post", dependent: :nullify
Nó cũng tạo sẵn các method hỗ trợ khác như
book.create_review(title: 'Code', content: 'Abc'). Đọc rất xuôi tai và dễ hình dung đúng không?Scope Association
Chưa dừng lại ở đó, tiếp tục với vấn đề khác là trên trang
Bạn có nghĩ tới ý tưởng viết một instance method tên
/books mình chỉ muốn hiện ra bài review công khai chứ không phải bản nháp (Post có hai trạng thái: published và draft). Bạn sẽ làm gì? Hãy suy nghĩ rồi hẵng đọc tiếp nhé.Bạn có nghĩ tới ý tưởng viết một instance method tên
published_review trong Book không?def published_review review if review&.published? end
Cũng được thôi, nếu thế ở trang index bạn muốn lấy ra 10 cuốn sách kèm review thì sao? Bạn gọi
Book.limit(10).each { |b| b.published_review }. Thế thì "giẫm" phải mìn N+1 rồi. Rất tệ đúng không? Cũng nên lưu ý là method này luôn truy vấn database và lưu review vào RAM nhé.Rails cho phép truyền block chứa method Relation (method
where, scope,...) vào. Ở đây sẽ là -> { published } với published là scope của Post.has_one :published_review, -> { published }, class_name: "Post"Khi dùng Scope Association như này Rails sẽ tự lọc luôn ở tầng database giúp giảm bớt RAM hơn cách trên. Muốn lấy hết
published_review của sách để tránh N+1 chỉ cần includes vào là xong:books = Book.finished.includes(:published_review)
books.each { |book| book.published_review } # không bị N+1Bên trên là một kiến thức nhỏ giúp bạn thiết kế Rails app mượt mà hơn. Nếu bạn vẫn còn thấy băn khoăn thì hãy chia sẻ để cùng nhau giải đáp nhé. 🍵