はじめに
Ruby on Railsを使っているとサクサクコードが書けて楽しいですね。
文法さえいくつか覚えてしまえばまるでプログラミングを意識せずに書きたいことをサクサク書いていけます。
Databaseに対してどういう命令を出しているかも意識せずに・・・
この代表的な問題点がいわゆるN+1問題です。
N+1問題の解決策などはこのあたりに詳しく書いてありますが、今回はcountメソッドを使った場合に起きる問題に焦点を当てて解説します。
問題点: Array?ActiveRecord::Associations?
配列の数を数えるメソッドとして、
.length
.size
.count
などがあります。
これらを使うことで、簡単に要素の数を返してくれます。
今回の問題点は .count
を使った際にせっかく includes
などを利用してもN+1問題が発生してしまうことの詳細です。
例えば、記事に対するコメント数を出す場合に
class Article < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :article
end
というモデルがあったとします。
とある一覧画面に記事の一覧とコメントの数を列挙しようとした際に、何も考えずに書くと
~~略
def index
@articles = Article.all
end
~~略
※ページングも何も考慮してません
~~略
<%= @articles.each do |article| %>
<div class="article_panel">
<div class="article_title"><%= article.title %></div>
<div class="comment_count"><%= article.comments.count %></div>
</div>
<% end %>
~~略
おめでとうございます。コレであなたもN+1問題へ無事突入しました。
一般的な解決策としては app/controllers/articles_controller.rb
で includes
をすることなのですが、
~~略
def index
@articles = Article.includes(:comments).all
end
~~略
これだけだと app/views/articles/index.html.erb
で .count
メソッドを使っているため、解決しません。
.count
メソッドは ActiveRecord::Associations::CollectionProxy にも定義されており、対象のテーブルに COUNT(*)
句のクエリを発行してしまいます。
残念ながらここまででは、N+1は未解決です。
解決策
~~略
def index
@articles = Article.includes(:comments).all
end
~~略
includes に加えて、
~~略
<%= @articles.each do |article| %>
<div class="article_panel">
<div class="article_title"><%= article.title %></div>
<div class="comment_count"><%= article.comments.size %></div>
</div>
<% end %>
~~略
.count
を .size
(もしくは .length
)に書き換えることで、N+1問題は解決します。
結論
せっかくincludeしたActiveRecord::Associationsに対しての数を数えるときは、 .size
もしくは .length
を使いましょう。
そうすることで COUNT(*)
句のクエリは発行されず、要素の数だけを数えて返してくれます。
※ちなみにですが、 comments.sum(:like)
などを使った際にも SUM(comments.like)
などでクエリが発行されますので注意が必要です。
おわりに
Ruby on Rails は本当に気軽にかける素晴らしいフレームワークです。
我々の会社でも数多くのプロダクトでこのフレームワークを使うことによる開発速度の恩恵を受けてきました。
正しく理解し、正しく使うことが非常に重要です。
何に対してどんな命令を出すことで、どこのリソースに対してどんな負荷がかかるのかを意識し、この記事がN+1退治に役立てれば幸いです。