+8

Kiểm tra query n+1 với gem Bullet trong rails

Gem Bullet được thiết kế để giúp bạn tăng hiệu suất của ứng dụng bằng cách giảm số lượng truy vấn nó làm. Nó sẽ xem các truy vấn của bạn trong khi bạn phát triển ứng dụng của bạn và thông báo cho bạn khi nào bạn nên thêm tải mong muốn (N + 1 truy vấn), khi bạn đang sử dụng tải mong muốn mà không cần thiết và khi nào bạn nên sử dụng bộ nhớ cache truy cập.

1. Cài Đặt gem Bullet

Bạn có thể cài đặt nó như là một đá quý:

gem install bullet

Hoặc thêm nó vào một Gemfile (Bundler):

gem 'bullet', group: 'development'

2. Cấu hình

Bullet sẽ không làm bất cứ điều gì trừ khi bạn cấu hình cho nó. Thêm vào config / environment / development.rb initializer với đoạn code như sau

config.after_initialize do
    Bullet.enable = true
    Bullet.alert = true
    Bullet.bullet_logger = true
    Bullet.console = true
    Bullet.rails_logger = true
    Bullet.add_footer = true
  end

Chú thích:

  • Bullet.enable = true: kích hoạt Bullet gem, false thì không làm gì cả
  • Bullet.alert = true: Hiện cảnh báo query n+1 trên trình duyệt, còn flase thì ngược lại 😄
  • Bullet.bullet_logger = true: Dăng nhập vào tệp nhật ký Bullet (Rails.root / log / bullet.log)
  • Bullet.console = true: Cảnh báo đăng nhập vào trình duyệt của bạn console.log (trình duyệt Safari / Webkit hoặc Firefox với Firebug cài đặt)
  • Bullet.rails_logger = true: Thêm cảnh báo trực tiếp vào bản ghi Rails
  • Bullet.add_footer = true: Thêm chi tiết ở góc dưới cùng bên trái của trang. Nhấp đúp vào footer hoặc sử dụng nút close để ẩn chân footer

3. Ví dụ

Mình có 2 model là user và order được thiết kế như sau

class User < ApplicationRecord
  has_many :orders, dependent: :destroy
end

class Order < ApplicationRecord
  belongs_to :user
  scope :sort_by_id, ->{order :id}
end

Trong controller Order mình muốn lấy ra tất cả order để quản lí

class Admin::OrdersController < ApplicationController
  def index
    @orders = Order.sort_by_id
  end
end

Trong view admin/orders/index.html.erb

<h1><%= t "admin.order.title" %></h1>
<div class="table-responsive">
  <table class="table">
    <thead>
      <tr>
        <th><%= t "admin.order.person" %></th>
        <th><%= t "admin.order.phone" %></th>
        <th><%= t "admin.order.address" %></th>
        <th><%= t "admin.order.totala" %></th>
        <th><%= t "admin.order.status" %></th>
        <th><%= t "admin.order.create" %></th>
        <th><%= t "admin.order.update" %></th>
      </tr>
    </thead>
    <tbody>
      <%= render partial: "order", collection: @orders %>
    </tbody>
  </table>
</div>

Trong view admin/orders/_order.html.erb

    <tr>
    <td><%= order.user.name %></td>
    <td><%= order.phone %></td>
    <td><%= order.address %></td>
    <td><%= order.total_amount %></td>
    <td>
    <span class="label label-warning">
      <%= order.status %>
    </span>
    </td>
    <td><%= order.created_at.to_date %></td>
    <td><%= order.updated_at.to_date %></td>
</tr>

Khi chạy trên trình duyệt thì sẽ hiện ra cảnh báo

user: pho
GET /admin/orders
USE eager loading detected
  Order => [:user]
  Add to your finder: :includes => [:user]
Call stack
  /home/pho/Documents/fdf_35/app/views/admin/orders/_order.html.erb:2:in `_app_views_admin_orders__order_html_erb__3528259263961660782_69985335319940'
  /home/pho/Documents/fdf_35/app/views/admin/orders/index.html.erb:19:in `_app_views_admin_orders_index_html_erb___3757855983818732562_69985337821920'

Câu query mà nó truy vấn khi n+1

Started GET "/admin/orders" for 127.0.0.1 at 2018-02-10 20:55:15 +0700
Processing by Admin::OrdersController#index as HTML
  User Load (0.2ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1
  Rendering admin/orders/index.html.erb within layouts/application
  Order Load (0.4ms)  SELECT `orders`.* FROM `orders` ORDER BY `orders`.`id` ASC
  User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]
  CACHE User Load (0.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1  [["id", 1], ["LIMIT", 1]]

Để giái quyết vấn đề này thì ta cần sửa lại như sau

class Admin::OrdersController < ApplicationController
  def index
     @orders = Order.includes(:user).sort_by_id
  end
end

Khi đó câu query sẽ load lại như sau

Started GET "/admin/orders" for 127.0.0.1 at 2018-02-10 21:31:21 +0700
   (185.6ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
   (24.4ms)  SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC
Processing by Admin::OrdersController#index as HTML
  User Load (3.8ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1
  Rendering admin/orders/index.html.erb within layouts/application
  Order Load (33.4ms)  SELECT `orders`.* FROM `orders` ORDER BY `orders`.`id` ASC
  User Load (0.5ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
  Rendered collection of admin/orders/_order.html.erb [42 times] (34.6ms)
  Rendered admin/orders/index.html.erb within layouts/application (711.7ms)

Và khi load lại trang thì ta sẽ không thấy thông báo query n+1 nữa.

4. Tổng kết

Gem Bullet rất hiệu quả khi ta muốn kiểm tra xem trang website của mình có bị query n+1 đúng không nào, hi vọng qua bài viết này bạn có thể tự kiếm tra được trang web của mình có bị n+1 hay không và giải quyết nó để cho tốc độ load trang website của mình được nhanh hơn, cảm ơn các bạn đã đoc bài viết của mình

Tài liệu tham khảo: https://github.com/flyerhzm/bullet


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí