Khi sử dụng ActiveRecord để load dữ liệu, đôi lúc chúng ta sẽ bắt gặp những trường hợp mọi thứ không hoạt động như những gì ta mong muốn. Đó là do tuỳ vào từng điều kiện, ActiveRecord sẽ lựa chọn hoặc kết hợp các phương thức eager loading lại với nhau để thực thi truy vấn một cách hiệu quả, nhưng đó không phải lúc nào cũng là phương án tốt nhất. Vì vậy, việc hiểu rõ cách hoạt động của từng phương thức eager loading trong ActiveRecord sẽ giúp bạn có được phương án tối ưu nhất cho các bài toán thực tế.
preload
Phương thức đầu tiên mà chúng ta nhắc đến là preload
, hãy cùng tìm hiểu nó qua ví dụ sau:
User.preload(posts: :comments)
SELECT `users`.* FROM `users`
SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 21, 31, 91, 111, 119, 129)
SELECT `comments`.* FROM `comments` WHERE `comments`.`post_id` IN (1, 11, 21, 31, 41, 51, 61, 71, 81, 91, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191, 201, 211, 231, 241, 251, 261, 271, 281, 291, 311, 331, 341, 351, 361, 371, 401, 411, 431, 439, 449)
Với preload
, ActiveRecord sẽ load dữ liệu thông qua các câu query riêng lẻ:
- Câu query đầu tiên để lấy ra
users
- Câu query thứ 2 lấy ra
posts
của cácusers
tương ứng - Câu query cuối cùng sẽ lấy tất cả
comments
trong từngposts
includes
Cũng với ví dụ trên nhưng chúng ta sẽ dùng includes
:
User.includes(posts: :comments)
SELECT `users`.* FROM `users`
SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 21, 31, 91, 111, 119, 129)
SELECT `comments`.* FROM `comments` WHERE `comments`.`post_id` IN (1, 11, 21, 31, 41, 51, 61, 71, 81, 91, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191, 201, 211, 231, 241, 251, 261, 271, 281, 291, 311, 331, 341, 351, 361, 371, 401, 411, 431, 439, 449)
Có vẻ như không gì khác so với khi dùng preload
, vậy tại sao ActiveRecord lại tạo ra 2 methods này? Hãy cùng xem ví dụ dưới đây để thấy được sự khác biệt:
User.includes(posts: :comments).where(posts: {id: 1})
SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`avatar` AS t0_r2, `users`.`email` AS t0_r3, `users`.`encrypted_password` AS t0_r4, `users`.`address` AS t0_r5, `users`.`phone` AS t0_r6, `users`.`memo` AS t0_r7, `posts`.`id` AS t1_r0, `posts`.`title` AS t1_r1, `posts`.`content` AS t1_r2, `posts`.`thumbnail` AS t1_r3, `posts`.`views` AS t1_r4, `posts`.`point` AS t1_r5, `posts`.`user_id` AS t1_r6, `posts`.`serial_id` AS t1_r7, `posts`.`created_at` AS t1_r8, `posts`.`updated_at` AS t1_r9, `posts`.`status` AS t1_r10, `posts`.`description` AS t1_r11, `posts`.`slug` AS t1_r12, `comments`.`id` AS t2_r0, `comments`.`content` AS t2_r1, `comments`.`user_id` AS t2_r2, `comments`.`post_id` AS t2_r3, `comments`.`created_at` AS t2_r4, `comments`.`updated_at` AS t2_r5 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` LEFT OUTER JOIN `comments` ON `comments`.`post_id` = `posts`.`id` WHERE `posts`.`id` = 1
Ở trên chúng ta sử dụng includes
kết hợp với điều kiện ở bảng quan hệ (posts
). Mọi thứ hoạt động bình thường nhưng câu query không còn như trước. Thay vì load dữ liệu ở từng bảng, ActiveRecord đã sử dụng LEFT JOIN
để lấy tất cả dữ liệu bằng một câu query duy nhất.
Bây giờ hãy viết lại ví dụ trên nhưng sử dụng preload
:
User.preload(posts: :comments).where(posts: {id: 1}).load
User Load (0.9ms) SELECT `users`.* FROM `users` WHERE `posts`.`id` = 1
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'posts.id' in 'where clause'
Mọi thứ không hoạt động và chúng ta sẽ nhận được lỗi như trên. Nguyên nhân là với preload
, ActiveRecord sẽ luôn luôn chạy từng câu query riêng để lấy dữ liệu trên từng bảng. Nghĩa là bạn sẽ không thể kết hợp preload
với điều kiện khác trên các bảng quan hệ.
eager_load
Bây giờ hãy cùng xem cách mà eager_load
làm việc:
User.eager_load(posts: :comments)
SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`avatar` AS t0_r2, `users`.`email` AS t0_r3, `users`.`encrypted_password` AS t0_r4, `users`.`address` AS t0_r5, `users`.`phone` AS t0_r6, `users`.`memo` AS t0_r7, `posts`.`id` AS t1_r0, `posts`.`title` AS t1_r1, `posts`.`content` AS t1_r2, `posts`.`thumbnail` AS t1_r3, `posts`.`views` AS t1_r4, `posts`.`point` AS t1_r5, `posts`.`user_id` AS t1_r6, `posts`.`serial_id` AS t1_r7, `posts`.`created_at` AS t1_r8, `posts`.`updated_at` AS t1_r9, `posts`.`status` AS t1_r10, `posts`.`description` AS t1_r11, `posts`.`slug` AS t1_r12, `comments`.`id` AS t2_r0, `comments`.`content` AS t2_r1, `comments`.`user_id` AS t2_r2, `comments`.`post_id` AS t2_r3, `comments`.`created_at` AS t2_r4, `comments`.`updated_at` AS t2_r5 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` LEFT OUTER JOIN `comments` ON `comments`.`post_id` = `posts`.`id`
Bạn có thể thấy, eager_load
sẽ chỉ dùng một câu query duy nhất để lấy tất cả dữ liệu, bất kể là có thêm điều kiện trên các bảng quan hệ hay không:
User.eager_load(posts: :comments).where(posts: {id: 1})
SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`avatar` AS t0_r2, `users`.`email` AS t0_r3, `users`.`encrypted_password` AS t0_r4, `users`.`address` AS t0_r5, `users`.`phone` AS t0_r6, `users`.`memo` AS t0_r7, `posts`.`id` AS t1_r0, `posts`.`title` AS t1_r1, `posts`.`content` AS t1_r2, `posts`.`thumbnail` AS t1_r3, `posts`.`views` AS t1_r4, `posts`.`point` AS t1_r5, `posts`.`user_id` AS t1_r6, `posts`.`serial_id` AS t1_r7, `posts`.`created_at` AS t1_r8, `posts`.`updated_at` AS t1_r9, `posts`.`status` AS t1_r10, `posts`.`description` AS t1_r11, `posts`.`slug` AS t1_r12, `comments`.`id` AS t2_r0, `comments`.`content` AS t2_r1, `comments`.`user_id` AS t2_r2, `comments`.`post_id` AS t2_r3, `comments`.`created_at` AS t2_r4, `comments`.`updated_at` AS t2_r5 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` LEFT OUTER JOIN `comments` ON `comments`.`post_id` = `posts`.`id` WHERE `posts`.`id` = 1
Câu query lúc này giống hoàn toàn so với trường hợp sử dụng includes
trong ví dụ ở trên.
Như vậy có thể khẳng định, eager_load
chính là includes
khi kết hợp thêm điều kiện trên các bảng quan hệ. Nhưng chúng vẫn có một khác biệt nhỏ, hãy xem qua ví dụ sau:
User.eager_load(posts: :comments).where("`posts`.`id` = 1")
Mọi thứ hoạt động bình thường, câu query không có gì thay đổi, tuy nhiên với includes
:
User.includes(posts: :comments).where("`posts`.`id` = 1")
Chúng ta sẽ chỉ nhận được lỗi tương tự như trường hợp sử dụng preload
ở trên:
User Load (12.2ms) SELECT `users`.* FROM `users` WHERE (`posts`.`id` = 1)
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'posts.id' in 'where clause'
Nguyên nhân là với các điều kiện được viết bằng raw query, ActiveRecord sẽ không thể biết được sẽ phải join đến bảng nào để lấy dữ liệu. Lúc này chúng ta phải chỉ định rõ thông qua references
:
User.includes(posts: :comments).references(:posts).where("`posts`.`id` = 1")
Bây giờ mọi thứ sẽ lại hoạt động bình thường và includes
lại biến thành eager_load
Summary
Qua các ví dụ bên trên, chắc hẳn bạn cũng đã phần nào hiểu được cách hoạt động cũng như điểm giống và khác nhau giữa các phương thức eager loading trong ActiveRecord. Đây là những kiến thức cần thiết bạn cần nắm được trước khi quyết định sẽ lựa chọn phương thức cho các bài toàn cụ thể. Ở phần sau, chúng ta sẽ đi tìm hiểu về chủ đề này.