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ó 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
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ì:
- 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.
- 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/