Improving the Performance of Your Rails App With Eager Loading
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 ...
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+