Improving the Performance of Your Rails App With Eager Loading
Bài đăng này đã không được cập nhật trong 3 năm
Active Records và ORM là những công cụ vô cùng mạnh mẽ và hữu ích trong Ruby on Rails, nhưng chỉ thật sự khi chúng ta biết làm thế nào để sử dụng sức mạnh đó. Bài viết dưới đây sẽ giúp chúng ta tối ưu được query tới database sử dụng eager loading khi làm việc với ORM.
Les's take an example
Tạo một ứng dụng rail demo:
rails new blog
cd blog
rails g scaffold Author name:string
rails g scaffold Post title:string body:text author:references
Như vậy chúng ta đã tạo một ứng dụng blog
với 2 model là Author
và Post
Khởi tạo database và chạy ứng dụng:
rake db:migrate
rails s
Chúng ta có quan hệ Posts
thuộc về một Author
, và một Author
có nhiều Posts
. Các model được định nghĩa như dưới đây:
# Post Model
class Post < ActiveRecord::Base
belongs_to :author
end
# Author Model
class Author < ActiveRecord::Base
has_many :posts
end
Và giờ là lúc chúng ta đi vào nội dung chính hướng tới. Trong Posts
controller, chúng ta sẽ chỉ chú trọng vào phương thức index
.
# Controller
class PostsController < ApplicationController
def index
@posts = Post.order(created_at: :desc)
end
end
Tương ứng với nó là Posts
index view, bạn có thể thấy nó một chút khác nhưng hãy chú ý đến chỉ một dòng đặc biệt với lời gọi post.author.name
.
<tbody>
<% @posts.each do |post| %>
<tr>
<td><%= post.title %></td>
<td><%= post.body %></td>
<td><%= post.author.name %></td>
</tr>
<% end %>
</tbody>
Chúng ta sẽ tạo một vài dữ liệu để test, bạn có thể vào http://localhost:3000/posts/new
vàhttp://localhost:3000/authors/new
để tạo dữ liệu, hoặc sử dụng rails c
như dưới đây:
authors = Author.create([{ name: 'John' }, { name: 'Doe' }, { name: 'Manish' }])
Post.create(title: 'I love Tuts+', body: '', author: authors.first)
Post.create(title: 'Tuts+ is Awesome', body: '', author: authors.second)
Post.create(title: 'Long Live Tuts+', body: '', author: authors.last)
Và giờ quay trở lại với trang index
của Posts
localhost:3000/posts
bạn sẽ thấy:
Mọi thứ xem dường như là ổn, không error và hiển thị tất cả các posts
với tên tác giả tương ứng. Nhưng nếu nhìn vào log hiển thị ở console bạn sẽ thấy hàng loạt các truy xuất data được ứng dụng gọi để lấy dữ liệu hiển thị ra:
Post Load (0.6ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC
Author Load (0.5ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT 1 [["id", 3]]
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT 1 [["id", 2]]
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT 1 [["id", 1]]
Hãy tưởng tượng ở đây chúng ta chỉ có 3 Posts
và số query là 4, nhưn hãy tưởng tượng có tới 3000 post được hiển thị, như vậy số query sẽ là 3000+1, đó sẽ là một vấn đề rất lớn ảnh hưởng tới hiệu xuất của chương trình, vấn đề đó chính là N+1
.
Tại sao chúng ta lại gặp phải vấn đề này?
Bởi vì bởi mặc định trong Ruby on Rails, ORM sử dụng lazy loading được sử dụng, điều đó có nghĩa là khi nào cần dữ liệu thì chương tình mới load ra. Trong trường hợp của chúng ta, đầu tiên là controller gọi đến tất cả các post được load ra:
def index
@posts = Post.order(created_at: :desc)
end
Rồi sau đó trong view, chúng ta tạo một vòng lặp các post, mỗi post chúng ta lại gửi một query để lấy ra tên tác giả Author của mỗi post, do đó tạo ra vấn đề N+1 query:
<% @posts.each do |post| %>
<tr>
.
.
.
<td><%= post.author.name %></td>
</tr>
<% end %>
Làm sao để giải quết vấn đề này?
Để giải quyết vấn đề này, Rails đưa ra một tính năng được gọi là eager loading
Eager loading giúp bạn preload các dữ liệu quan hệ (authors) cho tất cả các posts từ database. Điều này giúp tăng performance nhờ giảm số lượng queries, và cung cấp trước dữ liệu bạn muốn cho việc hiển thị. Ba phương thức được cung cấp cho cùng một mục đính đó là
preload()
eager_load()
includes()
Với 3 phương thức này, sẽ tùy thuộc vào từng trường hợp để chúng ta có thể sử dụng.
Bạn có thể hỏi phương thức nào để sử dụng trong ví dụ của chúng ta, hãy bắt đầu với phương thức thứ nhất preload()
.
def index
@posts = Post.order(created_at: :desc).preload(:author)
end
Tải lại trang posts index và xem kết quả:
Kết quả hiển thị không có gì khác nhưng hãy xem console log, ta chỉ thấy 2 query đến database thay vì 4 query như ở trên.
SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC
SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (3, 2, 1)
Preload sử dụng 2 queries để load dữ liệu chính và dữ liệu quan hệ. Cách này thực sự tốt hơn để giải quyết vấn đề N+1 queries, nhưng chưa thực sự là tốt. Bởi vì nó vẫn chia tách các queries, và sẽ có vấn đề trong viễn cảnh sau:
- Sắp xếp các order theo tên tác giả authors name.
- Tìm những posts bởi tác giả có tên "John" only
References
3 ways to do eager loading (preloading) in Rails 3 & 4 Example from envatoTuts+
All rights reserved