余計なクエリ数を減らしたい。counter_cacheの検討からcounter_cultureの導入まで。

## 起こっていた問題
Railsでインスタグラムを模したアプリを作っています。
データベース構造はこんな感じです。

[![Image from Gyazo](https://i.gyazo.com/3ff5532b162755ce3983340414e8fc9a.png)](https://gyazo.com/3ff5532b162755ce3983340414e8fc9a)

このアプリのトップページ（投稿一覧）でこんなメソットを実装していたところ、

```slim
  - if post.images.count >= 2
    - # 処理
```
クエリがこんなことになってしまいました^^;

[![Image from Gyazo](https://i.gyazo.com/2ac982cf4d976aca9d99e1ed8a3f5a41.jpg)](https://gyazo.com/2ac982cf4d976aca9d99e1ed8a3f5a41)

imagesのcountが大量に回っている...^^;

そういえば、Railsには`counter_cache`という機能がデフォルトであって、こういうときに役立つんだよ、と聞いたことがあったので、調べてみたのが今回の始まりです。

なお、実行環境は以下の通りです。

- `Rails 5.2.3`
- `Ruby 2.6.0`

## counter_cacheについて

まず、上記の問題を解決する手段として、

- [counter_cache](https://railsguides.jp/association_basics.html#belongs-to%E3%81%AE%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-counter-cache)（Railsのデフォルトの機能）を使う
- [counter_culture](https://github.com/magnusvk/counter_culture)(gem)を使う
- [ゴリゴリコードを書く](https://qiita.com/moonkilsan/items/9e694b0cc3fc5809a68b)

の３種類があることがわかりました。（３つ目は教えていただきました！ありがとうございます！！）

まず、Railsのデフォルトの機能の`counter_cache`なのですが、以下のように実装します。

```Ruby:models/image.rb
class Image < ApplicationRecord
  belongs_to :post, counter_cache: true
end
```
従属している(belongs_to)がついている方のモデルに、`counter_cache: true`を記載します。

```Ruby:models/post.rb
class Post < ApplicationRecord
  has_many :images
end
```
has_manyの方のモデルはそのままなのですが、こちらには、`子テーブルの名前_count`というカラムを作ります。今回の場合は`images_count`というカラムを`posts`テーブルに作ります。

```
$ rails g migration AddColumnToPost images_count:integer
```

こうすることで、`images_count`カラムには、postが持っているimageの数が記録される様になります。

ビューでは、こんなふうに書けば、いちいち`images`テーブルを呼び出さずにクエリ数も減るはず...

```slim
- if post.images_count >= 2
  -# 処理 
```

...と、ここまで書いて気がついた。**すでにpostのデータがある場合はどうすればいいんだ。。。**:sob:

はい、`counter_cache`の弱点はまさにそれで、すでに`posts`テーブルにデータがある場合、`images_count`の値は自動更新されません。

[先人たちが色々苦労されて修正された様子](https://blog.valerauko.net/2017/09/22/counter_cache-no-wana/)も見つけたのですが、構造が謎すぎて自分には使いこなせそうにありませんでした。。。

そこで、さらに調べていく中で出会ったのが次に紹介する`counter_culture`です。

### counter_cache補足
なお、`counter_cache`については、デフォルトのカラム名を変更したりなど、色々なオプションがありましたので、詳しくはこちらをご覧ください。

[Railsガイド - 4.1.2.3 :counter_cache](https://railsguides.jp/association_basics.html#belongs-to%E3%81%AE%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3-counter-cache)

## counter_cultureについて
`counter_culture`は上記の`counter_cache`の弱点も補強してくれるような高機能のgemです。

▶︎公式ドキュメントは[こちら](https://github.com/magnusvk/counter_culture)

### 導入方法

#### gemfile・migration
導入方法は以下の通りです。まず、`Gemfile`に以下を記載し`bundle install`します。

```Ruby:Gemfile
gem 'counter_culture'
```

その後、以下のコマンドでマイグレーションを生成します。

```
$ rails generate counter_culture Post images_count
```
こちらのコマンドの結果、以下のようなマイグレーションが作成されます。

```Ruby:XXXXXXXXXXXXXXX_add_images_count_to_posts.rb
class AddImagesCountToPosts < ActiveRecord::Migration[5.2]
  def self.up
    add_column :posts, :images_count, :integer, null: false, default: 0
  end

  def self.down
    remove_column :posts, :images_count
  end
end
```
同様の構造を持つマイグレーションファイルであれば、必ずコマンドで作成しないといけない、というわけではないようです＾＾

`rails db:migrate`します。

#### model
モデルファイルには、それぞれ以下の様に記載します。

```Ruby:models/image.rb
class Image < ApplicationRecord
  belongs_to :post
  counter_culture :post
end
```

```Ruby:models/post.rb
class Post < ApplicationRecord
  has_many :images
end
```

#### その他
そして、すでに`posts`テーブルにたくさんデータがあるなどの場合、コンソールで以下の様に実行します。

```
$ rails c
pry(main)> Image.counter_culture_fix_counts
```
こうすることで、`posts`テーブルの`images_count`のカラムの中身がアップデートされます。

最後に、ビューでこの様に書けば...

```slim
- if post.images_count >= 2
  -# 処理 
```

無事、クエリ数が減りました！！:smiley:

[![Image from Gyazo](https://i.gyazo.com/0542bea043522531ed1b22be97ef909f.png)](https://gyazo.com/0542bea043522531ed1b22be97ef909f)

## 補足・参考資料等
`counter_culture`にはオプションが色々ある様でしたので、詳しく知りたい方は公式ドキュメントをご覧ください。

- [counter_culture](https://github.com/magnusvk/counter_culture)

また、今回参考にさせていただいた記事は以下の通りです。

- [Rails向け高機能カウンタキャッシュ gem ‘counter_culture’ README（翻訳）](https://techracho.bpsinc.jp/hachi8833/2017_08_03/43698)
- [関連レコード数の集計（カウンターキャッシュ）](https://qiita.com/kanekomasanori@github/items/8155dff193e961828d02)

### その他・感想等
今回の実装前に、N＋1問題も発生していたので、その解決方法も参考までにご共有いたします。

- [gem bulletを入れてみた（N+1問題に自分で気づけない人のために）
](https://qiita.com/tanutanu/items/7157446d7c3bfe377d63)

クエリ数を減らしたいが今年の目標の一つだったのですが、SQLをゴリゴリ勉強する前に、手早く減らせる方法が見つかってよかったです＾＾

今後は、SQLの勉強を頑張っていきたいです。