has_manyのhas_manyのcountで効率よくパフォーマンスを出す

counter_cacheはできるだけ使いたくないんですよ。

無駄なカラムを持ちたくないということで、頑張りました。

対策としては、最初で全てのcountを手にするのが一番いいですね。

countは重たいので、先に何とかして値を入手したい・・・。

今回のサンプルでは、Blogがあって、Commentをたくさん持っていて、さらにCommentに対して複数のReactionをつけれるようなものです。 それのgroup化したReactionの数が欲しいという状況でした。

サンプル

class Blog < ActiveRecord::Base
  has_many :comments, dependent: :destroy
  has_many :comment_reactions, through: :comments

  def comment_reactions_count_map
    comment_reactions.group(:comment_id, :reaction_id).count←これが今回の肝
=> {[1, 1]=>7,
 [1, 315]=>1,
 [1, 332]=>1,
 [1, 708]=>1,
 [1, 825]=>1,
 [1, 864]=>1,
 [1, 1038]=>1,
 [1, 1044]=>1,
 [1, 1182]=>1,
 [1, 1229]=>1,
 [1, 1288]=>1,
 [2, 57]=>1,
 [2, 82]=>1,
 [2, 220]=>1,
  end
end
class Comment < ActiveRecord::Base
  belongs_to :blog
  has_many :comment_reactions
  has_many :reactions, through: :comment_reactions
end
class CommentReaction < ActiveRecord::Base
  belongs_to :comment
  belongs_to :reaction
end
class Reaction < ActiveRecord::Base
  has_many :comment_reactions
  has_many :comments, through: :comment_reactions
end

controller側

def show
    @blog = Blog.find(params[:id])
    @count_map = @blog.comment_reactions_count_map←これでIDをセットする。
end

view側はこんな感じになりますね。

サンプルなんで、適当です。

show.html.slim

  = render partial: "blog/comments", collection: @blog.comments.includes(:reactions), as: :comment

_blog/comments.html.slim

  - if comment.reactions.present?
    - comment.reactions.group(:id).each do |reaction|
      = image_tag reaction.image.url
      = @count_map[[comment.id, reaction.id]]←これでcountの値をもらう

こんな感じでどうでしょうか。

パフォーマンスはexplainでコツコツ攻略するのも大事ですが、可能ならば、sqlのクエリを減らすのがいいですね。