12/08/2018, 15:07

Eager loading và Lazy loading trong Rails

Giả sử chúng ta có User và Product có mối quan hệ 1-n , chúng ta hãy tiến hành tạo một ví dụ để thử nghiệm bằng các câu lệnh sau: rails new eager_lazy_loading cd eager_lazy_loading rails g model User email:string rails g model Product name:string user:references rake db:migrate Sau khi đã ...

Giả sử chúng ta có UserProduct có mối quan hệ 1-n, chúng ta hãy tiến hành tạo một ví dụ để thử nghiệm bằng các câu lệnh sau:

rails new eager_lazy_loading
cd eager_lazy_loading
rails g model User email:string
rails g model Product name:string user:references
rake db:migrate

Sau khi đã thực hiện đầy đủ các câu lệnh trên, ta tiến hành thêm mối quan hệ giữa User và Product và file seed để tạo dữ liệu mẫu như sau:

# app/models/user.rb
class User < ApplicationRecord
  has_many :products
end

# app/models/product.rb
class Product < ApplicationRecord
  belongs_to :user
end

# app/db/seeds.rb
3.times do |i|
  User.create email: "abc#{i + 1}@gmail.com"
end

User.all.each do |user|
  Product.create user_id: user.id, name: "Product#{user.id}"
end

# Sau khi thêm các dòng lệnh trên, vào terminal gõ:
rake db:seed

Khi chúng ta muốn hiển thị ra danh sách mọi Product của tất cả User kèm với một số điều kiện nào đó cho từng Product. Xem ví dụ sau:

users  = User.all
users.each do |user|
 product = Product.where email:  user.id
end

Cùng nhau xem cách Rails thực hiện đoạn lệnh trên:

User Load (0.9ms)  SELECT "users".* FROM "users"
Product Load (0.2ms)  SELECT  "products".* FROM "products" WHERE "products"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
Product Load (0.1ms)  SELECT  "products".* FROM "products" WHERE "products"."user_id" = ? LIMIT ?  [["user_id", 2], ["LIMIT", 1]]
Product Load (0.1ms)  SELECT  "products".* FROM "products" WHERE "products"."user_id" = ? LIMIT ?  [["user_id", 3], ["LIMIT", 1]]

Đoạn lệnh trên đã thực hiện rất nhiều câu queries, một câu lệnh để lấy ra tất cả user, và N câu lệnh query để thực hiện dựa trên số lượng user. Do đó, xét về mặt performance thì đoạn lệnh trên không ổn. Vậy thì cùng xem Eager loading nào.

Cùng thực hiện đoạn lệnh sau:

users = User.includes(:products)

Và cách Rails thực hiện đoạn lệnh trên:

User Load (0.1ms)  SELECT "users".* FROM "users"
Product Load (0.1ms)  SELECT "products".* FROM "products" WHERE "products"."user_id" IN (1, 2, 3)

Trong đoạn lệnh trên chúng ta chỉ thực hiện 2 câu queries, một câu lệnh để lấy ra tất cả user, và câu lệnh còn lại lấy ra tất cả Product thuộc user đó.

Có ba cách để sử dụng Eager loading là: includes, preload và eager_load, cùng xem ví dụ sau để biết sự khác biệt của 3 cách này:

# Sử dụng bằng includes
User.includes(:products)

-- Câu query đc thực hiện --
User Load (0.2ms)  SELECT "users".* FROM "users"
Product Load (0.3ms)  SELECT "products".* FROM "products" WHERE "products"."user_id" IN (1, 2, 3)
-------------------------------------------

# Sử dụng bằng preload
User.preload(:products)

-- Câu query đc thực hiện --
User Load (0.2ms)  SELECT "users".* FROM "users"
Product Load (0.3ms)  SELECT "products".* FROM "products" WHERE "products"."user_id" IN (1, 2, 3)
-------------------------------------------

# Sử dụng bằng eager_load
User.eager_load(:products)

-- Câu query đc thực hiện --
 SQL (0.3ms)  SELECT "users"."id" AS t0_r0, "users"."email" AS t0_r1, "users"."created_at" AS t0_r2, "users"."updated_at" AS t0_r3, "products"."id" AS t1_r0, "products"."name" AS t1_r1, "products"."user_id" AS t1_r2, "products"."created_at" AS t1_r3, "products"."updated_at" AS t1_r4 FROM "users" LEFT OUTER JOIN "products" ON "products"."user_id" = "users"."id"
 -------------------------------------------

Dựa trên đoạn lệnh vừa thực hiện trên thì:

  • includespreload cùng thực hiện câu 2 query giống nhau.
  • eager_load khác includepreload ở chỗ gộp 2 câu query lại làm một và chạy 1 lần duy nhất dựa trên cú pháp ** LEFT OUTER JOIN**

Cùng thực hiện ví dụ tiếp theo để thấy sự khác biệt:

# Sử dụng bằng includes
User.includes(:products).where("products.name = ?", "Product1")

-- Câu query đc thực hiện --
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: products.name: SELECT "users".* FROM "users" WHERE (products.name = 'Product1')
-------------------------------------------

# Sử dụng bằng preload
Use.preload(:products).where("products.name = ?", "Product1")

-- Câu query đc thực hiện --
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: products.name: SELECT "users".* FROM "users" WHERE (products.name = 'Product1')
-------------------------------------------

# Sử dụng bằng eager_load
User.eager_load(:products).where("products.name = ?", "Product1")

-- Câu query đc thực hiện --
SQL (0.2ms)  SELECT "users"."id" AS t0_r0, "users"."email" AS t0_r1, "users"."created_at" AS t0_r2, "users"."updated_at" AS t0_r3, "products"."id" AS t1_r0, "products"."name" AS t1_r1, "products"."user_id" AS t1_r2, "products"."created_at" AS t1_r3, "products"."updated_at" AS t1_r4 FROM "users" LEFT OUTER JOIN "products" ON "products"."user_id" = "users"."id" WHERE (products.name = 'Product1')
-------------------------------------------

Dựa trên đoạn lệnh vừa thực hiện thì:

  • Chỉ có eager_load thực hiện được, còn preloadincludes thì không. Lý do là vì eager_load đã có LEFT OUTER JOIN rồi nên trong câu SQL sẽ nhận biết được PRODUCTS là gì trong câu lệnh where, còn preloadincludes thì không

Tiếp tục một ví dụ nữa để xem sự khác biệt giữa preload và includes:

# Sử dụng bằng includes
Use.includes(:products).where("products.name = ?", "Product1").references(:products)

-- Câu query đc thực hiện --
SQL (0.2ms)  SELECT "users"."id" AS t0_r0, "users"."email" AS t0_r1, "users"."created_at" AS t0_r2, "users"."updated_at" AS t0_r3, "products"."id" AS t1_r0, "products"."name" AS t1_r1, "products"."user_id" AS t1_r2, "products"."created_at" AS t1_r3, "products"."updated_at" AS t1_r4 FROM "users" LEFT OUTER JOIN "products" ON "products"."user_id" = "users"."id" WHERE (products.name = 'Product1')
-------------------------------------------

# Sử dụng bằng preload
Use.preload(:products).where("products.name = ?", "Product1").references(:products)

-- Câu query đc thực hiện --
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: products.name: SELECT "users".* FROM "users" WHERE (products.name = 'Product1')
-------------------------------------------

Dựa trên đoạn lệnh vừa thực hiện thì:

  • includes đã thực hiện câu lệnh giống với eager_load, còn preload vẫn tiếp tục lỗi.
  • includes sẽ giống với eager_load nếu có thêm điều kiện references, nếu không có references thì nó sẽ giống với preload.
  1. preload: Thực hiện từng câu query riêng biệt.
  2. includes: Vừa có thể thực hiện từng câu query riêng biệt giống preload, vừa có thể thực hiện gộp nhiều câu queries giống eager_load nếu có references.
  3. eager_load: Thực hiện gộp thành một câu query duy nhất bằng LEFT OUTER JOIN. Nguồn: http://blog.arkency.com/2013/12/rails4-preloading/
0