Eager loading và Lazy loading trong Rails
Bài đăng này đã không được cập nhật trong 7 năm
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 đã 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
Lazy loading
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.
Eager loading
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 đó.
3 cách để sử dụng Eager loading
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ì:
- includes và preload cùng thực hiện câu 2 query giống nhau.
- eager_load khác include và preload ở 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 preload và includes 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 preload và includes 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.
Kết luận
- preload: Thực hiện từng câu query riêng biệt.
- 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.
- 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/
All rights reserved