Eager loading in rails 4
Bài đăng này đã không được cập nhật trong 9 năm
1. Eager loading là gì?
Eager loading is a way to find objects of a certain class and a number of named associations. It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100 posts that each need to display their author triggers 101 database queries. Through the use of eager loading, the number of queries will be reduced from 101 to 2.
2. Nói lời tạm biệt với N + 1 query
Cùng xem xét một ví dụ sau:
class Employee < ActiveRecord::Base
belongs_to :team
end
class Team < ActiveRecord::Base
has_many :employees
end
employees = Employee.limit(10)
employees.each do |employee|
puts employee.title.name
end
Nhìn vào đọc code trên, có vẻ như là ổn, tuy nhiên nếu xét về performance, thì điều đó thật tệ. Cùng xem những câu query đã thực hiện:
SELECT `employees`.* FROM `employees` LIMIT 10
SELECT `titles`.* FROM `titles` WHERE `titles`.`deleted_at` IS NULL AND `titles`.`id` = 1 LIMIT 1
SELECT `titles`.* FROM `titles` WHERE `titles`.`deleted_at` IS NULL AND `titles`.`id` = 2 LIMIT 1
SELECT `titles`.* FROM `titles` WHERE `titles`.`deleted_at` IS NULL AND `titles`.`id` = 3 LIMIT 1
SELECT `titles`.* FROM `titles` WHERE `titles`.`deleted_at` IS NULL AND `titles`.`id` = 4 LIMIT 1
SELECT `titles`.* FROM `titles` WHERE `titles`.`deleted_at` IS NULL AND `titles`.`id` = 5 LIMIT 1
SELECT `titles`.* FROM `titles` WHERE `titles`.`deleted_at` IS NULL AND `titles`.`id` = 6 LIMIT 1
SELECT `titles`.* FROM `titles` WHERE `titles`.`deleted_at` IS NULL AND `titles`.`id` = 7 LIMIT 1
SELECT `titles`.* FROM `titles` WHERE `titles`.`deleted_at` IS NULL AND `titles`.`id` = 8 LIMIT 1
SELECT `titles`.* FROM `titles` WHERE `titles`.`deleted_at` IS NULL AND `titles`.`id` = 9 LIMIT 1
SELECT `titles`.* FROM `titles` WHERE `titles`.`deleted_at` IS NULL AND `titles`.`id` = 10 LIMIT 1
Rõ ràng chúng ta cần truy xuất vào database nhiều lần để lấy dữ liệu, điều này làm giảm performance của application xuống rất nhiều. Đặc biệt với việc truy xuất dữ liệu lớn thì đó là bài toán lớn cần được tối ưu.
Eager loading sinh ra để giải quyết vấn đề này. Xem xét cùng ví dụ trên và áp dụng eager loading:
employees = Employee.includes(:title).limit(10)
employees.each do |employee|
puts employee.title.name
end
Cùng xem những query đã thực hiện:
SELECT `employees`.* FROM `employees` LIMIT 10
SELECT `titles`.* FROM `titles` WHERE `titles`.`deleted_at` IS NULL AND `titles`.`id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Thật tuyệt vời vì số lượng query đã giảm đi đáng kể, từ N + 1
query và giờ chỉ còn 2
query. Điều này tương đương với time truy xuất databse giảm xuống. Và đương nhiên, hiệu năng của application của chúng ta cũng sẽ tăng lên.
Như vậy, chúng ta có thể sử dụng eager loading
để xử lý N + 1
query.
2. Những cách sử dụng eager loading
Chúng ta có 3 cách để sử dụng eager loading
:
includes()
preload()
eager_load()
Thực tế hầu hết developer đã và đang sử dụng includes()
, dường như includes()
khá là quen thuộc khi áp dụng eager loading
để truy xuất dữ liệu từ database nếu như sử dụng rails với activerecord. Tuy nhiên bạn có biết tại sao đôi lúc chúng ta cần những câu query nhỏ, đẹp hoặc đôi khi lại là một câu query lớn chưa? Và bạn có biết là preload()
hoặc eager_load()
sẽ giúp chúng ta giải quyết điều đó không? Vậy chúng ta sẽ cùng làm rõ một số khía cạnh của eager loading
mà bạn chưa thực sự quen.
Cùng xem xét lại ví dụ trên với 2 models employee
, team
và có cùng associations như trên. Xét các query sau:
Team.includes(:employees) (q1)
->
Team Load (0.8ms) SELECT `teams`.* FROM `teams`
Employee Load (0.6ms) SELECT `employees`.* FROM `employees` WHERE `employees`.`team_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33)
Team.preload(:employees) (q2)
->
Team Load (0.6ms) SELECT `teams`.* FROM `teams`
Employee Load (0.6ms) SELECT `employees`.* FROM `employees` WHERE `employees`.`team_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33)
Team.eager_load(:employees) (q3)
->
SQL (7.7ms) SELECT `teams`.`id` AS t0_r0, `teams`.`name` AS t0_r1, `teams`.`group_id` AS t0_r2, `teams`.`sync_key` AS t0_r3, `teams`.`created_at` AS t0_r4, `teams`.`updated_at` AS t0_r5, `employees`.`id` AS t1_r0, `employees`.`email` AS t1_r1, `employees`.`display_name` AS t1_r2, `employees`.`card_number` AS t1_r3, `employees`.`position` AS t1_r4, `employees`.`uid` AS t1_r5, `employees`.`contract_type` AS t1_r6, `employees`.`identity_id` AS t1_r7, `employees`.`join_date` AS t1_r8, `employees`.`resigned_date` AS t1_r9, `employees`.`deleted_at` AS t1_r10, `employees`.`user_type` AS t1_r11, `employees`.`avatar` AS t1_r12, `employees`.`sync_key` AS t1_r13, `employees`.`created_at` AS t1_r14, `employees`.`updated_at` AS t1_r15, `employees`.`team_id` AS t1_r16, `employees`.`title_id` AS t1_r17 FROM `teams` LEFT OUTER JOIN `employees` ON `employees`.`team_id` = `teams`.`id`
Dễ để nhận thấy sự khác biệt giữa các query trên:
- Giống nhau giữa
q1
vàq2
: đều sử dụng các query riêng biệt để lấy dữ liệu - Khác biệt rõ ràng giữa
q1
,q2
vàq3
:q3
nhóm lại thành một câu query lớn để lấy dữ liệu - Tất nhiên số lượng query của chúng ta đã giảm xuống rất nhiều
Cùng tiếp tục với việc thêm các điều kiện where("employees.position = ?", "manager")
khi query:
Team.includes(:employees).where("employees.position = ?", "manager")
->
Team Load (1.0ms) SELECT `teams`.* FROM `teams` WHERE (employees.position = 'manager')
Mysql2::Error: Unknown column 'employees.position' in 'where clause': SELECT `teams`.* FROM `teams` WHERE (employees.position = 'manager')
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'employees.position' in 'where clause': SELECT `teams`.* FROM `teams` WHERE (employees.position = 'manager')
Team.preload(:employees).where("employees.position = ?", "manager")
->
Team Load (1.0ms) SELECT `teams`.* FROM `teams` WHERE (employees.position = 'manager')
Mysql2::Error: Unknown column 'employees.position' in 'where clause': SELECT `teams`.* FROM `teams` WHERE (employees.position = 'manager')
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'employees.position' in 'where clause': SELECT `teams`.* FROM `teams` WHERE (employees.position = 'manager')
Team.eager_load(:employees).where("employees.position = ?", "manager")
->
SQL (1.0ms) SELECT `teams`.`id` AS t0_r0, `teams`.`name` AS t0_r1, `teams`.`group_id` AS t0_r2, `teams`.`sync_key` AS t0_r3, `teams`.`created_at` AS t0_r4, `teams`.`updated_at` AS t0_r5, `employees`.`id` AS t1_r0, `employees`.`email` AS t1_r1, `employees`.`display_name` AS t1_r2, `employees`.`card_number` AS t1_r3, `employees`.`position` AS t1_r4, `employees`.`uid` AS t1_r5, `employees`.`contract_type` AS t1_r6, `employees`.`identity_id` AS t1_r7, `employees`.`join_date` AS t1_r8, `employees`.`resigned_date` AS t1_r9, `employees`.`deleted_at` AS t1_r10, `employees`.`user_type` AS t1_r11, `employees`.`avatar` AS t1_r12, `employees`.`sync_key` AS t1_r13, `employees`.`created_at` AS t1_r14, `employees`.`updated_at` AS t1_r15, `employees`.`team_id` AS t1_r16, `employees`.`title_id` AS t1_r17 FROM `teams` LEFT OUTER JOIN `employees` ON `employees`.`team_id` = `teams`.`id` WHERE (employees.position = 'manager')
Thật ngạc nhiên khi mà eager loading
sử dụng includes()
hoặc preload()
đã không run, lỗi đã được raise. Ngược lại eager_load()
hoạt động hoàn hảo. Nhưng chờ chút, các bạn hãy cùng xem, nếu thêm conditions references("employees")
nối tiếp vào 2 query bị lỗi ở trên thì như thế nào nhỉ. Cùng xem kết quả nhé:
Team.includes(:employees).where("employees.position = ?", "manager").references("employees")
->
SQL (1.0ms) SELECT `teams`.`id` AS t0_r0, `teams`.`name` AS t0_r1, `teams`.`group_id` AS t0_r2, `teams`.`sync_key` AS t0_r3, `teams`.`created_at` AS t0_r4, `teams`.`updated_at` AS t0_r5, `employees`.`id` AS t1_r0, `employees`.`email` AS t1_r1, `employees`.`display_name` AS t1_r2, `employees`.`card_number` AS t1_r3, `employees`.`position` AS t1_r4, `employees`.`uid` AS t1_r5, `employees`.`contract_type` AS t1_r6, `employees`.`identity_id` AS t1_r7, `employees`.`join_date` AS t1_r8, `employees`.`resigned_date` AS t1_r9, `employees`.`deleted_at` AS t1_r10, `employees`.`user_type` AS t1_r11, `employees`.`avatar` AS t1_r12, `employees`.`sync_key` AS t1_r13, `employees`.`created_at` AS t1_r14, `employees`.`updated_at` AS t1_r15, `employees`.`team_id` AS t1_r16, `employees`.`title_id` AS t1_r17 FROM `teams` LEFT OUTER JOIN `employees` ON `employees`.`team_id` = `teams`.`id` WHERE (employees.position = 'manager')
Team.preload(:employees).where("employees.position = ?", "manager").references("employees")
->
Team Load (1.1ms) SELECT `teams`.* FROM `teams` WHERE (employees.position = 'manager')
Mysql2::Error: Unknown column 'employees.position' in 'where clause': SELECT `teams`.* FROM `teams` WHERE (employees.position = 'manager')
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'employees.position' in 'where clause': SELECT `teams`.* FROM `teams` WHERE (employees.position = 'manager')
Dễ nhận thấy includes()
đã run và thực sư là query rất giống với eager_load()
khi không có thêm conditions. Trong khi đó preload()
vẫn không run .
Như vậy chúng ta có thể thấy includes()
là trung gian giữa preload()
hoặc eager_load
, tùy thuộc vào có hay không điều kiện đi kèm với một preloaded table.
preload()
sử dụng các query riêng biệt để lấy dữ liệu.
eager_load
nhóm thành một câu query lớn để lấy dữ liệu.
Chúng có thể sử dụng references
với includes()
nếu như chúng ta có một điều kiện cho một eager loaded table.
3. Eager loading polymorphic
Cùng xem xét ví dụ:
class User < ActiveRecord::Base
has_many :reviews
end
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :reviewable, polymorphic: true
end
class Shop < ActiveRecord::Base
has_many :reviews, as: :reviewable
end
Bạn sẽ không thể thực hiện eager load
vì những nguyên nhân sau:
- Activerecord sẽ không thể thực hiện được join bảng nếu không có đầy đủ thông tin.
- Thực tế chúng ta không có bảng dữ liệu nào là
reviewable
Vậy để giải quyết vấn đề đó chúng ta association giữa review
và shop
như sau:
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :reviewable, polymorphic: true
# For Rails < 4
belongs_to :shop, foreign_key: "reviewable_id", conditions: "reviews.reviewable_type = 'Shop'"
# For Rails >= 4
belongs_to :shop, ->{where reviews: {reviewable_type: ""Shop""}}, foreign_key: "reviewable_id"
end
Và chúng ta có thể sử dụng eager loading
:
Review.includes(:shop).where(shops: {shop_type: "cafe"})
4. Tổng kết
Eager loading thực sự là một công cụ bổ ích khi thực hiện tối ưu performace của application.
Bên cạnh đó xin giới thiêu với các bạn một vài công cụ để giúp cho việc optimize performace và clean your code:
- Gem hỗ trở kiểm tra
N+1
query Bullet - Gem hỗ trở kiểm tra thời gian thực hiện load trang RACK-MINI-PROFILER
All rights reserved