先週、怖話.jpのランキングを重いサブクエリを書いて実装しましたが、Rails3レシピブック(Recipe 093 カウンタキャッシュを利用する)を見ていたらcounter_cacheというので簡単に速くなりそうだったのでやってみた。

ASCIIcasts - “Episode 23 - Counter Cache Column”

要はfoo has many barsという関係の時にbarのbelongs_toに:counter_cache => trueと書いて、fooのDBにbars_countというカラムを追加すればいいらしい。

怖い話(story)に付いている怖い(scare)とコメント(comment)の集計にcounter_cacheを使ってみた。

class Scare < ActiveRecord::Base
  belongs_to :story, :counter_cache => true
end
class Comment < ActiveRecord::Base
  belongs_to :story, :counter_cache => true
end
class AddCounterCacheToStories < ActiveRecord::Migration
  def self.up
    add_column :stories, :comments_count, :integer, :null => false, :default => 0
    add_column :stories, :scares_count, :integer, :null => false, :default => 0

    Story.all.each do |story|
      Story.update_counters(story.id, :comments_count => story.comments.count)
      Story.update_counters(story.id, :scares_count => story.scares.count)
    end
  end

  def self.down
    remove_column :stories, :comments_count
    remove_column :stories, :scares_count
  end
end

counter_cacheはfooのcreateとdestroyのコールバックとして数を増減するというだけの動作なので、後付けする場合はupdate_countersメソッドで集計し直してやる必要がある。

class Story < ActiveRecord::Base
  scope :order_by_ranking, joins(:user).order("view + (select count(*) from scares where scares.story_id = stories.id) * #{Scare::RANKING_WEIGHT} + (select count(*) from comments where comments.story_id = stories.id) * #{Comment::RANKING_WEIGHT} desc, stories.id desc").includes(:user)
end

お陰でこんな醜悪なクエリが、

class Story < ActiveRecord::Base
  # scares_count and comments count is using counter_cache.
  scope :order_by_ranking, joins(:user).order("view + scares_count * #{Scare::RANKING_WEIGHT} + comments_count * #{Comment::RANKING_WEIGHT} desc, stories.id desc").includes(:user)
end

ホッとするシンプルなクエリに。

しかし、キャッシュ系によくあることですが、カウンタキャッシュを使っているということをちゃんと意識してないと端からみたらわかり辛いことになるので、現在は一人開発ですが、将来の自分を含めた他人に向けて注意を喚起するコメントを残しておくことにしました。

Comments


Option