Rails

【Rails】N+1問題をアラート表示してくれるgem「bullet」を初心者向けにまとめてみた

Rails

 

今回は、N+1問題をアラート表示してくれるgem「bullet」について解説したいと思います。N+1問題はアプリケーションのパフォーマンス低下に繋がってしまうため、動きさえすれば良いというテストアプリケーション以外はぜひ導入を検討していただけたらと思います。

N+1問題とは

まずはN+1問題の概要について簡単におさらいしておきます。

簡単に言うと、N+1問題とはデータベースへのアクセス回数が余計に多くなってしまう現象です。これは、モデル間のアソシエーションを利用する場合に発生します。

例を見ていきましょう。

usersテーブルとpostsテーブルがあり、それらは1対多の関係で結びついています。

クエリの発行回数

 

そしてpostsテーブルからレコードを全件取得し、取得した投稿に紐づくユーザーの名前を表示したいとします。N+1問題を考慮しなかった場合、記述は以下のようになります。

Post.all.each do |post|
  post.user.name
end

 

上記の記述を実行すると4回ものクエリが発行してしまいます。これではユーザー数が増えれば増えるほどクエリの発行回数が多くなってしまい、パフォーマンス的に良くありません。

SELECT  `posts`.* FROM `posts`
SELECT  `users`.* FROM `users` WHERE `users`.id = 1 LIMIT 1
SELECT  `users`.* FROM `users` WHERE `users`.id = 2 LIMIT 1
SELECT  `users`.* FROM `users` WHERE `users`.id = 3 LIMIT 1

 

代表的なN+1問題を解決するメソッドはincludesメソッドになります。

includesメソッドはPostモデルと関連するUserモデルを一括で読み込んだ上で、each文を実行してくれます。

includesメソッドを使用した時のクエリの発行回数

 

以下のようにincludesメソッドを使用した記述に修正することで、クエリの発行回数を2回に抑えることができ、パフォーマンス改善が見込めます。

Post.all.includes(:user).each do |post|
  post.user.name
end
SELECT  `posts`.* FROM `posts`
SELECT  `users`.* FROM `users` WHERE `users`.`id` IN(1, 2, 3)
上記のincludesメソッドはpreloadメソッドに置き換えることも可能です。
Rails
【Rails】パフォーマンス低下に繋がるN+1問題とは?解決策と併せて解説!アプリケーションのパフォーマンス低下に繋がる問題「N+1問題」について解説しています。練習用の小さなアプリケーションではあまり気にする必要はありませんが、本格的にアプリケーションを開発する際は、パフォーマンスは非常に重要になってきますので、ぜひincludesメソッドを使いこなせるように練習しておきましょう。...

gem「bullet」とは

では本題のgem「bullet」について見ていきましょう。

bulletとは、N+1問題が起きている箇所を特定・アラート表示してくれるgemです。

テーブル数が増えてくると、N+1問題が起きているのにも関わらず見逃してしまうケースが多発します。それを避けるためにも、導入しておくべきgemの1つだと考えています。

The Bullet gem is designed to help you increase your application’s performance by reducing the number of queries it makes. It will watch your queries while you develop your application and notify you when you should add eager loading (N+1 queries), when you’re using eager loading that isn’t necessary and when you should use counter cache.

Bullet gemは、実行するクエリの数を減らすことで、アプリケーションのパフォーマンスを向上させるように設計されています。アプリケーションの開発中にクエリを監視し、積極的な読み込み(N + 1クエリ)を追加する必要がある場合、不要な積極的な読み込みを使用する場合、およびカウンターキャッシュを使用する必要がある場合に通知します。

参照:https://github.com/flyerhzm/bullet

bulletを実際に使ってみよう

では簡単なアプリケーションを作成しながらbulletの使い方を学んでいきましょう。

アプリケーションの作成にはscaffoldを使用するので、scaffoldが曖昧な方はこちらの記事に目を通しておいてください。

Rails
【Rails】秒速でアプリケーション開発できるscaffoldの使い方を簡単にまとめてみた手っ取り早くアプリケーションを開発することができるscaffold(スキャフォールド)の使い方について簡単に解説しています。新しい機能実装をテストしてみたい、すぐにでもアプリケーションを立ち上げたいといった場合に便利な機能なので、ぜひ覚えておきましょう。...

アプリケーションの作成

まずは以下のコマンドでアプリケーションを作成します。同時にデータベースも作成しておきましょう。

% rails new test_bullet -d mysql
% cd test_bullet
% rails db:create

今回開発するアプリケーションの環境は以下のようになります。

  • アプリケーション名:test_bullet
  • Rails:6.0.3.2
  • Ruby:2.6.5
  • DB:MySQL

 

次にscaffoldの機能を使い、PostモデルとCommentモデルに関する土台を作成します。土台作成後はマイグレートも忘れずに行ましょう。

% rails g scaffold post name:string
% rails g scaffold comment name:string post_id:integer
% bundle exec rails db:migrate
# マイグレート後のschema.rb

ActiveRecord::Schema.define(version: 2021_12_27_083332) do

  create_table "comments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
    t.string "name"
    t.integer "post_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "posts", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

end

 

先ほど作成したPostモデルとCommentモデルのアソシエーションも組んでおきましょう。PostモデルとCommentモデルは1対多の関係になるので、以下のように記述することができます。

# post.rb

class Post < ApplicationRecord
  has_many :comments
end
# comment.rb

class Comment < ApplicationRecord
  belongs_to :post
end

 

続いてirbを立ち上げ、データベースに値を入れていきましょう。(投入するデータ数が少ないため、今回はコンソール上で直接値を入れています)

% rails c
irb > post1 = Post.create(:name => 'first')
irb > post2 = Post.create(:name => 'second')
irb > post1.comments.create(:name => 'first')
irb > post1.comments.create(:name => 'second')
irb > post2.comments.create(:name => 'third')
irb > post2.comments.create(:name => 'fourth')
irb > exit
postsテーブル
commentsテーブル

 

次に「app/views/posts/index.html.erb」を以下のように編集し、あえてN+1問題が起きるよう記述します。<%= post.comments.map(&:name) %>がN+1問題が発生している箇所になります。

<p id="notice"><%= notice %></p>

<h1>Posts</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Comments</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.name %></td>
        <td><%= post.comments.map(&:name) %></td>
        <td><%= link_to 'Show', post %></td>
        <td><%= link_to 'Edit', edit_post_path(post) %></td>
        <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Post', new_post_path %>

 

ここまででどのような挙動になるか一旦確かめてみます。「rails s」でサーバーを立ち上げ、「http://localhost:3000/posts」にアクセスしてみましょう。先ほどコンソール上でデータベースに投入した値(nameとcomment)が表示されていれば、事前準備完了です。

表示

bulletの導入

ではここから実際にbulletを導入し、N+1問題を検知していきます。

Gemfileのdevelopment環境とtest環境に「gem ‘bullet’」を記載し、「bundle install」を実行しましょう。

group :development, :test do
  gem 'bullet'
end
% bundle install

 

続いて以下のコマンドでbulletの初期設定を行います。対話スクリプトが起動しますが、今回はtest環境ではbulletを使用しないため「n」を入力して進めます。

% bundle exec rails g bullet:install

Running via Spring preloader in process 42961
Enabled bullet in config/environments/development.rb
Would you like to enable bullet in test environment? (y/n)

=> nを入力

 

config/environments/development.rbを開き、以下のコードが追加されている事を確認しましょう。ここのtrue/falseを切り替えることでN+1問題のアラート設定を行うことができます。

Rails.application.configure do
  config.after_initialize do
    Bullet.enable        = true
    Bullet.alert         = true
    Bullet.bullet_logger = true
    Bullet.console       = true
  # Bullet.growl         = true
    Bullet.rails_logger  = true
    Bullet.add_footer    = true
  end

  # 省略

end
記述記述の意味
Bullet.enablebulletを有効化するかどうか。falseの場合はbulletが無効になります。
Bullet.alertN+1問題の検知時、ブラウザにJavaScriptのポップアップアラートを表示するかどうか。
Bullet.bullet_loggerN+1問題の検知時、bullet専用のログであるlog/bullet.logを出力するかどうか。
Bullet.consoleN+1問題の検知時、ブラウザのconsole.logにアラートを表示するかどうか。
Bullet.growlN+1問題の検知時、かつGrowlがインストールされている場合にGrowlメッセージを表示するかどうか。デフォルトはfalse。
Bullet.rails_loggerN+1問題の検知時、ターミナルのログにアラート表示するかどうか。
Bullet.add_footerN+1問題の検知時、ブラウザ左下にアラートを表示するかどうか。

 

これで自動的にbulletがN+1問題が起きている箇所を検知し、アラートを表示してくれるようになりました。再度「rails s」でサーバーを立ち上げ、「http://localhost:3000/posts」にアクセスしてみましょう。

JavaScriptのポップアップアラート
アラート表示
# ターミナル上のログ

user: yoshiharu.chiba
GET /posts
USE eager loading detected
  Post => [:comments]
  Add to your query: .includes([:comments])
Call stack
  /Users/yoshiharu.chiba/Desktop/test_bullet/app/views/posts/index.html.erb:18:in `map'
  /Users/yoshiharu.chiba/Desktop/test_bullet/app/views/posts/index.html.erb:18:in `block in _app_views_posts_index_html_erb__1211332998519677680_70255496990820'
  /Users/yoshiharu.chiba/Desktop/test_bullet/app/views/posts/index.html.erb:15:in `_app_views_posts_index_html_erb__1211332998519677680_70255496990820'
# log/bullet.log

2021-12-27 22:39:27[WARN] user: yoshiharu.chiba
GET /posts
USE eager loading detected
  Post => [:comments]
  Add to your query: .includes([:comments])
Call stack
  /Users/yoshiharu.chiba/Desktop/test_bullet/app/views/posts/index.html.erb:17:in `map'
  /Users/yoshiharu.chiba/Desktop/test_bullet/app/views/posts/index.html.erb:17:in `block in _app_views_posts_index_html_erb__4455284572466763881_70286658634000'
  /Users/yoshiharu.chiba/Desktop/test_bullet/app/views/posts/index.html.erb:14:in `_app_views_posts_index_html_erb__4455284572466763881_70286658634000'

 

ではposts_controller.rbをN+1問題が起きないように編集します。posts_controller.rbのindexアクションを以下のように書き換えてください。

def index
  @posts = Post.includes(:comments)

  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @posts }
  end
end

 

もう一度ブラウザをリロードしてみましょう。

Image from Gyazo

 

アラート表示が消えましたね。

このようにgem「bullet」を用いることで、パフォーマンスに大きく影響するN+1問題をアラート表示してくれます。

bulletを導入したけど「アラートがうまく表示されない」「修正したのにアラートが消えない」といった場合は、ブラウザのキャッシュが悪さをしている場合があります。その場合は、ブラウザのキャッシュ機能をOFFにしておきましょう。

bulletを使いこなす

bulletには様々なオプションがあり、それらを使うには明示的に記述する必要があります。このオプションはconfig/environments/development.rbに追記することができます。

config.after_initialize do
  Bullet.enable = true
  Bullet.sentry = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.growl = true
  Bullet.xmpp = { :account  => 'bullets_account@jabber.org',
                  :password => 'bullets_password_for_jabber',
                  :receiver => 'your_account@jabber.org',
                  :show_online_status => true }
  Bullet.rails_logger = true
  Bullet.honeybadger = true
  Bullet.bugsnag = true
  Bullet.airbrake = true
  Bullet.rollbar = true
  Bullet.add_footer = true
  Bullet.skip_html_injection = false
  Bullet.stacktrace_includes = [ 'your_gem', 'your_middleware' ]
  Bullet.stacktrace_excludes = [ 'their_gem', 'their_middleware', ['my_file.rb', 'my_method'], ['my_file.rb', 16..20] ]
  Bullet.slack = { webhook_url: 'http://some.slack.url', channel: '#default', username: 'notifier' }
end

 

また修正する必要がない部分でアラート表示が出てしまう場合にも、明示的に記述することでアラートを無視することができます。こちらも同様にconfig/environments/development.rbに記載します。

# 記述例

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

 

使えるオプションや具体的な回避方法についてはGithubを覗いてみてください。

bulletを使用する上での注意点

bulletは非常に便利なgemですが、何点か注意して使用しなければなりません。

  • 全てのN+1問題を検出できるわけではない
  • gemの導入は慎重に行う
  • N+1問題の発見をbulletに頼らない

全てのN+1問題を検出できるわけではない

bulletは大抵のN+1問題を検知しますが、複雑な原因のN+1問題は検出できない場合があります。そのためbulletに頼りすぎず、普段からN+1問題を意識してコードを書く必要があります。

gemの導入は慎重に行う

bulletに限らず何かしらのgemを導入することで、アップデート対応や他の開発メンバーのキャッチアップが必要になってきます。そのためむやみやたらに入れるのではなく、きちんと検討した上でgemを導入するようにしましょう。

N+1問題の発見をbulletに頼らない

N+1問題をアラートで分かりやすく表示してくれるbulletは便利ですが、そういったサポートツールを使わずに自身で発見することが開発の上では望ましいです。そのため、N+1問題の全てをbulletに頼ることは避けましょう。

まとめ

  • N+1問題とはデータベースへのアクセス回数が余計に多くなってしまう現象のこと
  • bulletとはN+1問題が起きている箇所を特定・お知らせしてくれるgem
  • bulletは使いたいオプションを明示的に示すことでカスタマイズが可能
  • bulletの使用には何点か注意するべきポイントがある

参考文献

https://github.com/flyerhzm/bullet

 

 

今回はN+1問題をアラート表示してくれるgem「bullet」について解説しました。個人開発や練習用のアプリケーションでは導入の必要はありませんが、大規模開発になるとパフォーマンスは非常に重要になってくるので、ぜひ「bullet」というgemがあるということを頭に入れておきましょう。