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
使うに必要なのはPost
にcomments_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の沈黙に対して:渡されるデータはいじらない!バリデーションでこけさせてユーザーに直してもらおう。