1 minute read

使用工具找出隱藏的 N+1

Rails 檢測 N+1 最常見的有幾個套件 Bulletn_plus_one_controlprosopite 等等的 gem。

Bullet

Bullet 在 rspec 常常沒辦法攔截到 N+1,即便頁面上攔截成功,這邊我們直接舉一個例子

RSpec Bullet

# config/environments/test.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.bullet_logger = true
  Bullet.raise = true # raise an error if n+1 query occurs
end

# spec/rails_helper.rb
RSpec.configure do |config|
  config.include_context "bullet", bullet: true
  config.alias_example_to :bulletify, bullet: true
end
class Post < ApplicationRecord
  belongs_to :user
end
class PostsController < ApplicationController

  def index
    @posts = Post.all
  end
end
# view index 頁面
<% @posts.each do |post| %>
  <%= post.user.eamil %>
<% end %>

測試中的 Bullet 並沒有抓到這個 N+1 !

# spec/controllers/post_spec.rb
require 'rails_helper'

describe PostsController, type: :controller do
  describe "GET #index" do
    render_views

    let!(:user) { create(:user) }
    let!(:posts) { create_list(:post, 5, user: user) }

    bulletify { get :index }
  end
end

RSpec Bullet

n_plus_one_control 協助找出 N+1

# spec/controllers/post_spec.rb
require 'rails_helper'

describe PostsController, type: :controller do
  describe "GET #index" do
    render_views

    context "N+1", :n_plus_one do
      # 使用 populate 方法來產生資料
      populate { |n|
        user = create(:user)
        posts = create_list(:post, n, user: user)
      }

      it 'performs constant number of queries' do
        expect { get :index }.to perform_constant_number_of_queries
      end
    end
  end
end

這時候他就告訴我們當你有3篇文章時,你的頁面執行了4次查詢;而當你有3篇文章時,它執行了2次查詢

RSpec N_plus

Prosopite

初始化設置

# config/environments/development.rb

config.after_initialize do
  Prosopite.rails_logger = true
end
 controllers/application_controller.rb
unless Rails.env.production?
  before_action do
    Prosopite.scan
  end

  after_action do
    Prosopite.finish
  end
end

設定非常簡便使用起來我比較喜歡這個,在 log 中就能明顯的看到紅色的 error log

Prosopite

在測試中也可以使用

# config/environments/test.rb

config.after_initialize do
  Prosopite.rails_logger = true
  Prosopite.raise = true
end
require 'rails_helper'

describe PostsController, type: :controller do
  describe "GET #index" do
    render_views

    context "N+1" do
      let!(:user) { create(:user) }
      let!(:posts) { create_list(:post, 5, user: user) }

      it 'trigger N+1 queries on the index page' do
        get :index
      end
    end
  end
end

Prosopite rspec

總結

不管是不是用了套件輔助,在設計階段也可能寫出效能較差的 queries ,只要記得有看 log 的習慣,就能即使修正發生的效能問題。

以上 N+1 文章的參考資料

Rails 實戰聖經 Avoid N+1 queries on rails 這本書裡面介紹了非常詳細,必看!!! 5 ways to fix the latest-comment n+1 problem squash-n-plus-one-queries-early-with-n-plus-one-control-test-matchers-for-ruby-and-rails