(2015年までの)odaillyjp blog

イベント参加記録とプログラミング系の雑記

パーフェクト Rails を読みました(6章)

この記事は『パーフェクト Rails を読みました』シリーズの6回目の記事です。
前回の記事は『パーフェクト Rails を読みました(5章) - Programming log - Shindo200』です。

だいぶ時間がかかったのですが『パーフェクト Ruby on Rails』の6章を読み終えましたので、記録を残しておきます。

パーフェクト Ruby on Rails

パーフェクト Ruby on Rails

ちなみに、6章では Doorkeeper のようなイベント情報を管理するアプリケーションを作りながらWebアプリケーション開発で気をつけないといけないことを学んでいきました。

備忘録

N+1問題

Doorkeeper のようなイベント情報を管理するWebアプリケーションを作るとします。イベント情報ページに参加者の名前を表示させたいので、以下のような処理を書きました。

app/controllers/events_controller.rb

class EventsController < ApplicationController
  def show
    @event = Event.find(params[:id])
    # ユーザとイベントの間に Ticket という中間テーブルがある
    @tickets = @event.tickets.order(:created_at)
  end
end

app/views/events/show.html.erb

<ul>
  <% @tickets.each do |ticket| %>
    <li><%= ticket.user.name %></li>
  <% end %>
</ul>

この実装でイベント情報ページにアクセスすると、まずはイベント情報自体を取得するために1回のSQLが発行されます。つぎに中間テーブルから参加者一覧を取得するために1回、参加者ごとのデータを取得するためにN回(N=参加者数)SQLが発行されます。

  Ticket Load (0.1ms)  SELECT  "tickets".* FROM "tickets"  WHERE "tickets"."event_id" = ? ORDER BY "tickets"."created_at" ASC  [["event_id", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 2]]
  User Load (0.1ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 3]]
  User Load (0.1ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 4]]
  User Load (0.1ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 5]]
  User Load (0.1ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 6]]
  User Load (0.2ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 7]]
  User Load (0.2ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 8]]
  User Load (0.2ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 9]]
  # 以下省略

これでは参加者が多いときにパフォーマンスが悪くなってしまいます。このように、

  • 一覧を取得するためにSQLを1回発行する
  • 一覧にある各データの関連データを取得するためにSQLをN回発行する(N=一覧のレコード数)

という処理によってパフォーマンスが悪くなる問題をN+1問題と呼ぶそうです。この問題は、ActiveRecord では includes メソッドを使うことで回避することができます。

app/controllers/events_controller.rb

class EventsController < ApplicationController
  def show
    @event = Event.find(params[:id])
    # ユーザとイベントの間に Ticket という中間テーブルがある
    @tickets = @event.tickets.includes(:user).order(:created_at)
  end
end

しかし、開発に不慣れで知らずのうちにN+1問題を埋め込んでしまうことがあります。そこで、この問題を検知するために bullet という gem を使います。

bullet を使うとN+1問題が発生したときにログを出力したり、JavaScriptアラートを出したりなどの通知をしてくれるようになります。

$ cat log/bullet.log
2014-06-08 19:29:45[WARN] user: Main
localhost:3000http://localhost:3000/events/1
N+1 Query detected
  Ticket => [:user]
  Add to your finder: :include => [:user]
N+1 Query method call stack
  /Users/Odailly/src/rails/perfect_rails/awesome_events/app/views/events/show.html.erb:85:in `block in _app_views_events_show_html_erb___3747956587630597072_70215768103240'
  /Users/Odailly/src/rails/perfect_rails/awesome_events/app/views/events/show.html.erb:83:in `_app_views_events_show_html_erb___3747956587630597072_70215768103240'

Airbrake

Airbrake はアプリケーションのエラーを管理したり、エラー通知を開発者に送るサービスです。

個人で使うにはそこそこいい値段がするので、ここでは試せずにいます。
参考までにということで……

あとがき

『パーフェクト Ruby on Rails』とはあまり関係がない話なのですが、明日(6月9日)から Rails エンジニアになります。頑張ります。『パーフェクト Ruby on Rails』は引き続き読んでいきますので、今後もよろしくお願いします。