Giải quyết N+1 trong rails

N+1 là gì?

N+1 query là một vấn đề hiệu suất phổ biến trong các ứng dụng Ruby on Rails. Nó xảy ra khi bạn truy vấn một bảng để lấy dữ liệu, sau đó truy vấn bảng khác để lấy dữ liệu bổ sung cho mỗi đối tượng trong bảng đầu tiên.

Ví dụ: Bạn có bảng posts và bảng comments

class Post < ActiveRecord::Base 
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

Bạn muốn lấy danh sách tất cả các bài đăng cùng với danh sách tất cả các bình luận của chúng, bạn có thể viết mã như sau:

@posts = Post.limit(5) 
@posts.each do |post| 
  @post.comments
end
# Total: 6 query 
# 1 query cho post 
# 5 query cho mỗi lần each

Đoạn code này sẽ tạo ra hai truy vấn SQL:
Một truy vấn để lấy tất cả các bài đăng, và một truy vấn cho mỗi bài đăng để lấy tất cả các bình luận của nó. Điều này có thể dẫn đến hiệu suất kém, đặc biệt nếu bạn có nhiều bài đăng hoặc bình luận.

Cách giải quyết N+1

1. Sử dụng includes hoặc eager_load

Phương thức includes hoặc eager_load cho phép bạn tải trước dữ liệu từ relation khi bạn truy cập từ một bảng.

Ví dụ:

@posts = Post.includes(:comments) # Total: 1 query
@posts = Post.eager_load(:comments) # Total: 1 query

2. Sử dụng preload

preload là một phương pháp giúp giảm thiểu vấn đề n+1 query trong Rails. Khi bạn sử dụng preload, Rails sẽ thực hiện hai truy vấn riêng biệt: một truy vấn để lấy tất cả các dữ liệu cần thiết và một truy vấn để lấy dữ liệu liên quan. Sau đó, Rails sẽ kết hợp dữ liệu từ hai truy vấn này mà không cần thực hiện thêm bất kỳ truy vấn nào nữa, giảm thiểu số lần truy vấn đến cơ sở dữ liệu.

Ví dụ:

@posts = Post.preload(:comments)
@posts = Post.preload(:comments).where(published: true)

@posts = Post.all
ActiveRecord::Associations::Preloader.new.preload(@posts, :comments)

3. Sử dụng joins

Sử dụng joins để kết hợp các bảng và truy vấn dữ liệu cùng một lúc. Điều này giúp tránh việc thực hiện nhiều truy vấn riêng lẻ.

Ví dụ:

@posts = Post.joins(:comments).select('posts.*, comments.body') # rails 6
@posts = Post.joins(:comments)
             .select(posts: {}, comments: [:body]) # rails 7

4. Sử dụng pluck hoặc select

Thay vì lấy toàn bộ đối tượng, sử dụng pluck hoặc select để chỉ lấy những cột dữ liệu cần thiết.

Ví dụ:

# Sử dụng pluck
@post_ids = Post.where(created_at: 1.week.ago..).pluck(:id)
@comments = Post.joins(:comments)
                .where(created_at: 1.week.ago..)
		.pluck('comments.id', 'comments.body')

# Sử dụng select
@comments = Comment.select(:id, :body).where(id: @post_ids)
@posts = Post.joins(:comments).select('posts.*, comments.body') # rails 6
@posts = Post.joins(:comments)
             .select(posts: {}, comments: [:body]) # rails 7

5. Sử dụng scope

Khi bạn muốn sử dụng preload với một phạm vi (scope) trong Rails, bạn có thể kết hợp preload với phạm vi đó để tối ưu hóa truy vấn dữ liệu của mình.

Ví dụ:

class Post < ActiveRecord::Base 
  has_many :comments

  scope :published, -> { where(published: true) }
end

@posts = Post.eager_load(:comments).published
@posts = Post.published.preload(:comments)

@posts_pushlished = Post.published
ActiveRecord::Associations::Preloader.new.preload(
  @posts_pushlished,
  :comments
)

@posts = Post.published.includes(:comments)
@posts = Post.published.joins(:comments)

Kết luận

Bằng cách sử dụng các phương pháp như includes, eager_load, preload, joins, pluck, và select, bạn có thể giải quyết vấn đề N+1 query trong Rails một cách hiệu quả. Điều này không chỉ cải thiện hiệu suất ứng dụng của bạn mà còn giúp tối ưu hóa truy vấn cơ sở dữ liệu và tăng tốc độ tải trang.


0 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like
Cài đặt Rails 7 với Vite + Stimulus + Tailwind
Read More

Cài đặt Rails 7 với Vite + Stimulus + Tailwind

Nếu bạn đang tìm kiếm một giải pháp phát triển web hiệu suất cao và tiện lợi, Vite sẽ là lựa chọn đáng xem xét. Còn với Rails 7, bạn sẽ phải cân nhắc xem tính năng importmap có đủ thuyết phục để bạn chuyển đổi không.