Eager Loading (nâng cao) trong Rails
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 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.
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