+1

Gem bullet và config

Giới thiệu

  1. N+1 query là gì? Giả sử chúng ta có 2 model với quan hệ parent-child, chúng ta cần truy vấn database để load dữ liệu của model "con" thông qua model "cha". Hầu hết việc truy vấn mặc định sử dụng lazy-loading, đồng nghĩa với việc các truy vấn sẽ tìm tới bản ghi "cha" rồi thực hiện từng truy vấn đối với các bản ghi "con". Điều này dẫn đến sẽ thực hiện N+1 truy vấn vào database làm giảm tốc độ load dữ liệu, và có thể làm tràn bộ nhớ. Để kiểm soát vấn đề này, chúng ta sử dụng gem bullet để đưa ra các cảnh báo đối với query N+1 từ đó dễ dàng fix chúng về việc chỉ truy vấn 1 lần để tăng hiệu suất load dữ liệu.
  2. Bullet gem Bullet gem là một công cụ mạnh trong việc refactor code, hiệu quả với việc tăng tốc độ query vào database trong quá trình development. Bởi vì tính năng thông báo đối với N+1 query, một trong những lỗi phổ biến khi lập trình liên quan đến database nếu người lập trình không kiểm soát tốt nó.

Cài đặt

Chúng ta có thể cài đặt bằng terminal

gem install bullet

hoặc add trong Gemfile

gem 'bullet', group: 'development'

Configuration

Bullet sẽ không làm gì cả trừ khi bạn config cho nó. Trong môi trường config/environments/development.rb chúng ta làm theo đoạn code sau:

config.after_initialize do
    Bullet.enable = true
    Bullet.bullet_logger = true
    Bullet.raise = true # raise an error if n+1 query occurs
    Bullet.unused_eager_loading_enable = false
  end

đơn giản, config trên làm chức năng, bật bullet, tạo log thông báo khi có n+1, check lỗi n+1 query, đối với config Bullet.unused_eager_loading_enable cảnh báo khi chúng ta sử dụng việc fix query N+1 đối với việc load dữ liệu của model con không qua model cha. Nhưng thực tế thì lại không có case nào đòi hỏi việc load dữ liệu model con cả. Điều đó dẫn đến việc fix là dư thừa. Và config này là để check điều đó.

Whitelist

Đôi khi có một số đoạn code bạn không cần quan tâm đến N+1 query, và không muốn bullet thông báo về nó, thì bạn có thể đưa nó vào whitelist. Ví dụ như

Bullet.add_whitelist :type => :n_plus_one_query, :class_name => "Post", :association => :comments
Bullet.add_whitelist :type => :unused_eager_loading, :class_name => "Post", :association => :comments
Bullet.add_whitelist :type => :counter_cache, :class_name => "Country", :association => :cities

Hoặc bạn cũng có thể sử dụng:

class ApplicationController < ActionController::Base
  around_action :skip_bullet

  def skip_bullet
    Bullet.enable = false
    yield
  ensure
    Bullet.enable = true
  end
end

Log

Khi có lỗi liên quan đến bullet ta có thể xem log của nó để biết thêm chi tiết, lỗi đó là lỗi gì log/bullet.log

Đối với lỗi N+1 query

Giả sử bạn có đoạn code

def index
  @comments = Post.first.comments
end

ở đây, post và comment có mối quan hệ 1 nhiều. Nếu viết như trên chắc chắn bị báo lỗi N+1 Query

2009-08-25 20:40:17[INFO] N+1 Query: PATH_INFO: /posts;    model: Post => associations: [comments]·
Add to your finder: :include => [:comments]
2009-08-25 20:40:17[INFO] N+1 Query: method call stack:·
/Users/richard/Downloads/test/app/views/posts/index.html.erb:11:in `_run_erb_app47views47posts47index46html46erb'
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb'
/Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'

Cách sửa là includes comment trước khi thực hiện truy vấn

def index
  @comments = Post.first.includes(:comments).comments
end

Đối với Unused eager loading:

Giả sử bạn có đoạn code

def index
  @post_first = Post.first.includes(:comments)
end

Nhận thấy 1 điều là việc sử dụng includes ở đây là dư thừa vì chúng ta không cần lấy comments để làm gì cả. Và lỗi sẽ là:

2009-08-25 20:53:56[INFO] Unused eager loadings: PATH_INFO: /posts;    model: Post => associations: [comments]·
Remove from your finder: :include => [:comments]

Fix đơn giản là remove includes đi.

Bullet trong môi trường test

Phần lớn chúng ta sử dụng môi trường test để test lại tính năng vừa code, và để check những lỗi của bullet khi chạy test thì chúng ta phải config nó:

# spec/spec_helper.rb
if Bullet.enable?
  config.before(:each) do
    Bullet.start_request
  end

  config.after(:each) do
    Bullet.perform_out_of_channel_notifications if Bullet.notification?
    Bullet.end_request
  end
end

Ok. chỉ đơn giản vậy, bạ cứ thử có N+1 Query trong code xem, chạy test nó sẽ cảnh báo cho bạn liền.

Kết Luận

Để tăng tốc độ truy vấn, chúng ta cần quan tâm tới vấn đề N+1 query trong quá trình lập trình, và bullet là một người bạn hữu ích giúp bạn kiểm soát điều đó.

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í