今回は、N+1問題をアラート表示してくれるgem「bullet」について解説したいと思います。N+1問題はアプリケーションのパフォーマンス低下に繋がってしまうため、動きさえすれば良いというアプリケーション以外は、ぜひ導入を検討していただけたらと思います。
N+1問題とは
まずはN+1問題の概要について簡単におさらいしておきましょう。
N+1問題とは簡単に言うと、データベースへのアクセス回数が余計に多くなってしまう現象です。これは、モデル間のアソシエーションを利用する場合に発生します。
以下の場合だと、クエリ(検索文)が4回発行されていることになります。これだとアプリケーションのパフォーマンスが低下してしまいますね。

しかしincludesメソッドを使用することで、パフォーマンスを改善することができます。includesメソッドを使うと、以下のようにレコードを一気に取得することができるので非常に便利です。

N+1問題について詳しく知りたい方はこちらの記事を参考にしてください。

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を実際に使ってみよう
では簡単なアプリケーションを作成しながらbulletの使い方を学んでいきましょう。
アプリケーションの作成には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
先ほど作成したpostモデルとcommentモデルのアソシエーションも組んでおきましょう。postモデルとcommentモデルは1対多の関係になるので、以下のように記述することができます。
↓ post.rbとcomment.rb
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
続いてコンソールを立ち上げ、データベースに値を入れていきましょう。
$ 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
次に「app/views/posts/index.html.erb」を以下のように編集し、N+1問題が起きるよう記述します。
<p id="notice"><%= notice %></p>
<h1>Posts</h1>
<table>
<thead>
<tr>
<th>Name</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を導入していきます。
Gemfileに「gem ‘bullet’」を記載し、「bundle install」を実行しましょう。
gem "bullet"
$ bundle install
インストールが完了したら、以下のコマンドでbulletが使用できるようにします。
$ bundle exec rails g bullet:install
これで自動的にbulletがN+1問題が起きている箇所を特定し、ブラウザでアラートを表示してくれるようになりました。「rails s」でサーバーを立ち上げ、「http://localhost:3000/posts」にアクセスしてみましょう。

では「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
これでもう一度ブラウザをリロードしてみましょう。

アラート表示が消えましたね!
このように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
まとめ
- N+1問題とは、データベースへのアクセス回数が余計に多くなってしまう現象のこと
- bulletとは、N+1問題が起きている箇所を特定・お知らせしてくれるgemのこと
- bulletは、使いたいオプションを明示的に示すことでカスタマイズが可能
参考文献
Github:https://github.com/flyerhzm/bullet
今回はN+1問題をアラート表示してくれるgem「bullet」について解説しました。個人開発や練習用のアプリケーションでは導入の必要はありませんが、大規模開発になるとパフォーマンスは非常に重要になってくるので、ぜひ「bullet」というgemがあるということを頭に入れておきましょう。