counter_cacheはいいぞ。 #いいぞ

counter_cacheって?

アプリの大きさや目的に関わらず結構早い段階からいろんなものを数えたくなる。ユーザー何人?記事についてるタグ何個?

1歩目はその数値を表示するたびに数える方法。ただRailsになってくるとそれは毎回データベースを参照しないといけなくなる上、カウントのクエリが(特に条件付きで、複雑な場合)速くもない。読み取りが多いアプリだと、ページを表示する時にやらないといけないことはできるだけ減らしてサクサクと動かしたい。

2歩目は数値をコラムに保存して、その数値を上下させる。これで読み取りのところは準備された数値を読み取るだけで、数えクエリのボトルネックがない。ただ忘れがち。タグを追加した時にちゃんとその数値を動かさないといけない。タグを外した時も同じく。

毎回毎回その処理をちゃんと走らせないといけない。覚えないといけない。めんどくさい。自動化したい。そしてファウラー氏の言葉も借ります:「何かを毎回やらないといけないと覚える必要があるのは、よくないAPIの象徴。」よくないAPIは作りたくない。

Railsは優しくそのためのツールを提供してくれてる。それがcounter_cache。自動的にものを数えて、自動的に更新して。書く側は「やって」と指定するだけ。

どうやって?

例えば記事のPostモデルとそれにつくCommentがあるとしよう。

class Post < ActiveRecord
  has_many :comments, dependent: :destroy
end
class Comment < ActiveRecord
  belongs_to :post, counter_cache: true
end

使うに必要なのはPostcomments_countというコラムを追加すること。別の名称でもいいけど、そうすればcounter_cacheの値はtrueじゃなくそのコラム名を入れる。(counter_cache: :feedback_countとか)

これで完成!全部動く!ハッピー!

じゃタイトルの罠って何やねん

上記の2歩目の段階から3歩目に進もうとしよう。つまりコラムがすでにあってそれに数値が入っている。ただ今までは手動で動かしてたのでその数値があっているかどうかはわからない。ただそれを考えずに、数え直さず、先にcounter_cache入れちゃった。

最初は何も問題ないが、あるとき昔からある記事のコメントの数がずれちゃってることに気づく。数え直させたい。とりあえず記事のコメントを数え直させるように。

target = Post.forty_two
target.update(comments_count: target.comments.count)

すると…

 (0.2ms) BEGIN
SQL (0.3ms) UPDATE `posts` SET `updated_at` = '2017-09-22 04:04:21' WHERE `posts`.`id` = 42
 (0.2ms) COMMIT

ちゃんとupdateが走ったね。

あ。

ちょっと待って。

更新されてないやん!なんで?

別のやり方でも全く同じ結果。ビックリマークつけても。何も言われず、何もエラー吐かず、ただそのコラムが更新から外される。

だめじゃん!

そうでもない。まぁ勝手に、何も言わずに外されるのは確かにだめやけど、それに関してはRailsにイシュー切ってあげてください。​

上の問題に関しては、できないということは、Rails的にやってほしくないということ。でもやろうとしてることは別に珍しい訳でもないので、やり方はあるはず。
そしてある。​

Post.reset_counters(42, :comments)

やった!注意しないといけないのは、カウンターキャッシュの関数は全部クラスメソッド。つまりインスタンスの中からID渡さずにやりたいのなら、ちょっとチートしないと。

def reset_comment_count
  Post.reset_counters(id, :comments)
end

という感じでPostに追加すればもちろんできる。やるべきかどうかは置いといて。

今回の件から得るべき教訓は

ちゃんとドキュメントを見よう!そしてRailsの沈黙に対して:渡されるデータはいじらない!バリデーションでこけさせてユーザーに直してもらおう。