12/08/2018, 11:58

Eager loading in rails 4

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

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

image

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             </div>
            
            <div class=
0