読者です 読者をやめる 読者になる 読者になる

もた日記

くだらないことを真面目にやる

Railsメモ(19) : BulletでN+1問題を検出する

Ruby on Rails

Bulletの設定


github.com

N+1問題を検出するためにBulletというgemを試してみる。
Gemfileに下記行を追加してbundle installする。

group :development, :test do
  gem 'bullet'
end


Bulletはconfig/environments/development.rbに設定を追加しないと動作しないのでファイルを編集する。
用意されている基本的な設定は以下の通り。

config.after_initialize do
  Bullet.enable = 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.stacktrace_includes = [ 'your_gem', 'your_middleware' ]
  Bullet.slack = { webhook_url: 'http://some.slack.url', foo: 'bar' }
end

全てを追加する必要はないので、今回は必要そうな部分だけ追加してみる。

config.after_initialize do
  Bullet.enable = true # Bulletを有効化
  Bullet.alert = true # JavaScriptのポップアップアラートを有効化
  Bullet.bullet_logger = true # Rails.root/log/bullet.logに出力
  Bullet.console = true # ブラウザのconsole.logに出力
  Bullet.rails_logger = true # Railsのログに結果を出力
  Bullet.add_footer = true # ページの左下に結果を表示
end


以降はその他の設定。
Bulletには検出する問題をタイプごとに無効化する設定も存在する。

# Each of these settings defaults to true

# Detect N+1 queries
Bullet.n_plus_one_query_enable     = false

# Detect eager-loaded associations which are not used
Bullet.unused_eager_loading_enable = false

# Detect unnecessary COUNT queries which could be avoided
# with a counter_cache
Bullet.counter_cache_enable        = false

問題ないことがわかっている場合など通知してほしくない場合はホワイトリストの設定ができる。

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

コントローラー単位で通知をスキップする場合は以下のように記述する。

class ApplicationController < ActionController::Base
  around_action :skip_bullet

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

Bulletの使い方


gemをインストールして設定ファイルに追記すればアプリのページ表示時に問題があれば通知されるようになるので、試しにN+1問題があるページにアクセスしてみる。
1番目の図がBullet.alert = trueにしたときに表示されるダイアログで、2番目の図がBullet.add_footer = trueにしたときに左下に表示されるフッターである。
メッセージを見るとわかるようにN+1問題が検出され、この問題を解決するためには:includes => [:artists]を追加すればよいとある。

f:id:wonder-wall:20150816165552p:plain

f:id:wonder-wall:20150816165712p:plain

ということで指示通りにapp/controllers/songs_controller.rbincludesを追加してみる。

class SongsController < ApplicationController
  def index
    @q = Song.ransack(params[:q])
    #@songs = @q.result.page(params[:page]) # 編集前
    @songs = @q.result.includes(:artists).page(params[:page]) # 編集後
  end
end

編集後、再アクセスしてみるとBulletによる警告が表示されなくなり、実行されているSQLも102から4つに減ったことが確認できる。

f:id:wonder-wall:20150816171333p:plain

参考:問題があったモデル、コントローラー、ビュー


$ rails c
Loading development environment (Rails 4.2.3)
[1] pry(main)> show-models
Artist
  id: integer
  name: string
  created_at: datetime
  updated_at: datetime
  has_many :song_artists
  has_many :songs (through :song_artists)
Song
  id: integer
  title: string
  display_artist: string
  ranking: integer
  year: integer
  created_at: datetime
  updated_at: datetime
  has_many :artists (through :song_artists)
  has_many :song_artists
SongArtist
  id: integer
  song_id: integer
  artist_id: integer
  created_at: datetime
  updated_at: datetime
  belongs_to :artist
  belongs_to :song
class SongsController < ApplicationController
  def index
    @q = Song.ransack(params[:q])
    @songs = @q.result.page(params[:page])
  end
end
<table class="table table-striped table-hover">
  <thead>
    <tr>
      <th><%= sort_link(@q, :title) %></th>
      <th><%= sort_link(@q, :display_artist) %></th>
      <th><%= sort_link(@q, :ranking) %></th>
      <th><%= sort_link(@q, :year) %></th>
    </tr>
  </thead>
  <tbody>
    <% @songs.each do |song| %>
    <tr>
      <td><%= song.title %></td>
      <td>
        <% song.artists.each do |artist| %>
          <% song.display_artist.gsub!(/#{artist.name}/) {link_to artist.name, artist_path(artist)} %>
        <% end %>
        <%= song.display_artist.html_safe %>
      </td>
      <td><%= song.ranking %></td>
      <td><%= song.year %></td>
    </tr>
    <% end %>
  </tbody>
</table>
<%= paginate @songs %>

パーフェクト Ruby on Rails

パーフェクト Ruby on Rails