Một số câu hỏi để hiểu rõ hơn về joins, includes, preload và eager_load trong ActiveRecord
Các phương thức joins, includes, preload và eager_load của ActiveRecord đều vô cùng hữu ích, nhưng cũng rất nguy hiểm nếu sử dụng không đúng cách. Hiểu được việc sử dụng nó khi nào và ở đâu – và cả khi nào nên kết hợp lại – có thể giúp bạn rất nhiều khi phát triển ứng dụng. Dưới đây, tôi sẽ chỉ ...
Các phương thức joins, includes, preload và eager_load của ActiveRecord đều vô cùng hữu ích, nhưng cũng rất nguy hiểm nếu sử dụng không đúng cách. Hiểu được việc sử dụng nó khi nào và ở đâu – và cả khi nào nên kết hợp lại – có thể giúp bạn rất nhiều khi phát triển ứng dụng.
Dưới đây, tôi sẽ chỉ cho các bạn ở đâu và khi nào sử dụng các phương thức.
Trường hợp lý tưởng để sử dụng joins?
Nếu bạn chỉ muốn dùng bảng liên kết để lọc dữ liệu, không lấy dữ liệu – joins là mục tiêu của bạn. Ví dụ dưới đây lấy các bài post có comment được viết bời Derek. Ta không lấy dữ liệu về comment, vậy nên joins là lựa chọn phù hợp:
Post.joins(:comments).where(:comments => {author: 'Derek'}).map { |post| post.title } Post Load (1.2ms) SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "comments"."author" = $1 => ["One weird trick to better Rails apps", "1,234 weird tricks to faster Rails apps", "You wouldn't believe what happened to this Rails developer after 14 days"]
Joins có tránh được N+1 queries không?
Không. Bản thân joins không tải dữ liệu có liên kết vào bộ nhớ: việc lấy dữ liệu từ các bảng liên quan có thể gây ra truy vấn N+1. Ví dụ, đếm số comment trong mỗi bài post trên, ta phải lấy dữ liệu từ bảng Comment:
Post.joins(:comments).where(:comments => {author: 'Derek'}).map { |post| post.comments.size } Post Load (1.2ms) SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "comments"."author" = $1 (1.0ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1 (3.0ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1 (0.3ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1 (1.0ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1 (2.1ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1 (1.4ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1 => [3,5,2,4,2,1]
Includes có tránh được N+1 queries không?
Có. includes sẽ tải (1) tất cả các bản ghi cha và (2) tất cả các bản ghi liên quan được tham chiếu thông qua includes. Trong ví dụ dưới đây, việc sử dụng includes chỉ tạo ra một câu truy vấn. Nếu không có includes, sẽ có một câu truy vấn bổ sung để đếm số lượng bình luận ở mỗi post:
Post.includes(:comments).map { |post| post.comments.size } Post Load (1.2ms) SELECT "posts".* FROM "posts" Comment Load (2.0ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 3, 4, 5, 6) => [3,5,2,4,2,1]
Includes có sinh ra các câu truy vấn riêng cho từng bản ghi liên quan?
Không. includes sẽ không sử dụng truy vấn riêng. Nếu bạn có mệnh đề where hay order gọi đến bảng liên kết, LEFT OUTER JOIN được sử dụng với một truy vấn riêng.
Một truy vấn hay hai truy vấn sẽ nhanh hơn?
Đào sâu vào bản chất của ActiveRecord, tôi không tin rằng ActiveRecord quyết định việc sử dụng hai truy vấn hay một truy vấn dựa trên hiệu năng. Nếu bạn đang thấy hiệu năng thấp với truy vấn includes, tôi đề xuất việc sử dụng tool như Scout DevTrace và kiểm tra cách mà ActiveRecord đang sử dụng khi chạy includes. Nếu hai truy vấn đang được sử dụng, bạn có thể tạo một truy vấn với LEFT OUTER JOIN bằng cách thêm references vào liên kết ActiveRecord:
Post.includes(:comments).references(:comments).map { |post| post.comments.size }
Điều gì sẽ xảy ra khi tôi thêm điều kiện cho liên kết được includes?
ActiveRecord sẽ trả lại tất cả bản ghi cha và chỉ những bản ghi liên quan khớp với điều kiện. Ví dụ, dưới đây sẽ trả về tất cả dữ liệu của Post có comment bởi Derek, và chỉ đếm những comment của Derek:
Post.includes(:comments).references(:comments).where(comments => {author: 'Derek'}).map { |post| post.comments.size }
Includes có tránh được mọi N+1 queries không?
Không. Nếu bạn truy cập dữ liệu trong nested relationship, dữ liệu đó không được tải từ trước. Ví dụ, một câu truy vấn bổ sung sẽ được thêm để lấy Comment#likes với mỗi comment:
<% post.comments.each do |comment| %> <%= comment.likes.map { |like| like.user_avatar_url } <% end %>
Tôi có thể tránh N+1s trong nested relationships không?
Có. Bạn có thể tải các nested relationship bằng includes:
Post.includes(comments => :likes).references(:comments).map { |post| post.comments.size }
Tôi có nên luôn luôn tải dữ liệu từ nested relationships không?
Không. Rất dễ để tạo một số lượng đáng kể dữ liệu. Ví dụ, một Comment có thể có hàng ngàn bản ghi Like, sẽ khiến câu truy vấn bị chậm và tốn bộ nhớ. Tool như Scout DevTrace có thể giúp bạn xác định cách tiếp cận nhanh hơn.
Việc kết hợp joins và preload có phổ biến không?
Nếu tôi cần tất cả bản ghi có liên kết - không chỉ những bản ghi phù hợp với điều kiện liên kết - Tôi sẽ kết hợp preload và joins. Ví dụ như:
- Tìm tất cả dữ liệu Post có Comment tác giả là Derek
- Xử lý những bản ghi của Post và tổng số comment của mỗi post. includes sẽ chỉ lấy dữ liệu Comment được viết bởi Derek, không phải tất cả comment của mỗi post.
Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id").where(:comments => {author: 'Derek'}).preload(:comments).map { |post| post.comments.size }
Tôi có nên sử dụng eager_load không?
Có. Nếu thấy việc dùng includes bị chậm khi có 2 truy vấn, sử dụng eager_load sẽ ép nó thành một truy vấn bằng LEFT OUTER JOIN.
Tôi có thể kết hợp eager_load với joins không?
Có. Trong đoạn ví dụ sau:
Post.joins(:comments).eager_load(:comments).map { |post| post.comments.size }
ActiveRecord sẽ làm những điều sau:
- Trả lại Array các dữ liệu Post với các comments.
- Tải các comments liên kết với mỗi Post. Đó là includes với INNER JOIN và LEFT OUTER JOIN.
- Nếu tôi chỉ muốn lọc dữ liệu, sử dụng joins.
- Nếu tôi truy cập vào những dữ liệu liên kết, bắt đầu với includes.
- Nếu includes bị chậm khi sử dụng hai truy vấn riêng biệt, tôi sẽ sử dụng eager_load để ép nó thành truy vấn đơn và so sánh hiệu năng.
Nguồn tham khảo: http://blog.scoutapp.com/articles/2017/01/24/activerecord-includes-vs-joins-vs-preload-vs-eager_load-when-and-where