Eager Loading (nâng cao) trong Rails
Bài đăng này đã không được cập nhật trong 7 năm
Rails hỗ trợ các phương thức khác nhau (includes, preload, joins, etc.) sử dụng để load một lượng dữ liệu lớn và giảm thiểu số lần truy xuất vào database. Những cấu trúc cơ bản được sử dụng để tải trước một phần dữ liệu. Trong ví dụ, giả sử ứng dụng Rails gồm 3 models: posts, users, và comments.
rails g model user name:string
rails g model post user:references title:string body:text
rails g model comment user:references post:references message:string
rake db:migrate
# app/models/user.rb
class User < ActiveRecord::Base
has_many :posts
has_many :comments
end
# app/models/post.rb
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments
end
# app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :user
end
require 'faker'
users = Array.new(80) do
User.create(name: Faker::Name.name)
end
posts = Array.new(80) do
Post.create(user: users.sample, title: Faker::Lorem.sentence, body: Faker::Lorem.paragraph)
end
128.times do
users.each do |user|
posts.each do |post|
Comment.create(user: user, post: post, message: Faker::Lorem.sentence)
end
end
end
Bây giờ giả sử cần tạo một trang hiển thị post cùng một số comment:
rails generate controller posts
# config/routes.rb
Rails.application.routes.draw do
root to: 'posts#index'
end
# app/controllers/posts_controller.rb
...
def index
@posts = Post.all
end
...
- # app/views/posts/index.html.haml
- @posts.each do |post|
= post.title
= post.body
= post.user.name
- post.comments.each do |comment|
= comment.message
= comment.user.name
Để load được kết quả cho hàng trăm câu truy vấn này cần khoảng 2000ms
. Vì vậy, với một số lượng khổng lồ các câu truy vấn, ta sẽ load trước những dữ liệu đi kèm post bằng cách sử dụng includes()
:
@posts = Post.includes(:user, comments: :user)
Điều này đã làm giảm số lượng các câu truy vấn và thời gian tải giảm xuống còn 500ms
. Ứng dụng của chúng ta có thể không cần hiển thị tất cả các comment, nhưng có lẽ cần 1 cặp 2 comment. Để làm được điều này ta cần đặt một limit(n) khi looping các comment, tuy nhiên như vậy sẽ phá vỡ nguyên tắc eager loading. Ngoài ra, để đặt limit, ta có thể sử dụng slice (dùng Ruby thay vì gọi SQL), nhưng điều đó vẫn thực hiện lưu toàn bộ dữ liệu comment vào bộ nhớ. Không sao, chúng ta còn lựa chọn thứ 3:
# app/models/comment.rb
class Comment < ActiveRecord::Base
...
scope :recent, -> (count = 2) {
subselect <<-SQL
SELECT COUNT(*)
FROM comments AS rcomments
WHERE rcomments.post_id = comments.post_id AND rcomments.id > comments.id
SQL
where(":count > (#{subselect})").order(id: "DESC")
}
end
# app/models/post.rb
class Post < ActiveRecord::Base
...
has_many :recent_comments, -> { recent }, class_name: "Comment"
end
Hoán đổi comments
cho recent_comments
trong view và controller và thời gian load là 50ms
. Đây là bước cải thiện performance 40x
từ khi khởi tạo trang.
Tối ưu hóa
Cách làm trên hoạt động tốt với lượng dữ liệu nhỏ với các truy vấn SQL vào dữ liệu bậc hai. Khi bắt đầu post, sẽ có hàng nghìn comment chạy đến hơn 2000ms
. Theo phân tích cho thấy, chỉ một số bổ sung đơn giản sẽ không cải thiện đáng kể hiệu suất. May thay, PostgreSQL (và nhiều cơ sở dữ liệu khác) hỗ trợ một phương pháp nhanh hơn nhiều bằng cách sử dụng "cửa sổ chức năng" (window functions):
scope :recent, -> (count = 2) {
rankings = "SELECT id, RANK() OVER(PARTITION BY post_id ORDER BY id DESC) rank FROM comments"
joins("INNER JOIN (#{rankings}) rankings ON rankings.id = comments.id")
.where("rankings.rank < :count", count: count.next)
.order(id: "DESC")
}
Nguồn: https://ksylvest.com/posts/2014-12-20/advanced-eager-loading-in-rails
All rights reserved