12/08/2018, 13:47

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:

data 600.jpg

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ả:

data 600.jpg

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:

  1. Sắp xếp các order theo tên tác giả authors name.
  2. 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+

0