関連レコード数の集計（カウンターキャッシュ）

Railsで関連レコード数を集計するには以下の２つの方法が使えます

- counter_cache（Railsの機能)
- counter_culture（高機能なGem）

前者の`counter_cache`はRails3からの機能で、以下のように設定することにより関連するテーブルのレコード数を簡単にカウントさせることができます

例えばArticleモデルとCommentモデルの以下の様な親子関係があった場合、

```comment.rb
class Comment < ActiveRecord::Base
  belongs_to :article, :counter_cache => true
end
```
```article.rb
class Article < ActiveRecord::Base
  has_many :comments
end
```

Commentが作成されると以下の様なSQLが発行され、関連するArticleの`comments_count`が+1されます

```
UPDATE `articles` SET `comments_count` = COALESCE(`comments_count`, 0) + 1 WHERE `articles`.`id` = 1
```

通常は`親モデル_count`という名前のカラムにカウント値が入りますが、モデル内で設定している`:counter_cache => true`に文字列もしくはシンボルを渡すと、指定したカラムにカウント数を更新します

```comment.rb
class Comment < ActiveRecord::Base
    belongs_to :article, counter_cache: :admin_comments_count
end
```

Commentが削除された場合は当然、カウントは-1されます

これらのcounter_cacheはSUMをしていないという仕組み上、既にデータがあるところに導入するためには初期のカウント数を入れなければ実データとの差異が出てしまいます

counter_cultureにはそんな時のためにカウント数を調整する`counter_culture_fix_counts`というメソッドが用意されています`

### それぞれの特徴

## counter_cache
- テーブルのカウントが同一トランザクション内で行われるため、デッドロックが発生する可能性がある（結構頻発するという話も）
- カウント対象テーブルが深い階層だと対応できない（1階層のみ対応可）

## counter_culture
- テーブルのカウントが対象テーブルの更新と別トランザクションで行われるため、デッドロックは発生しない
- テーブルのカウントが対象テーブルの更新と別トランザクションで行われるため、値の不整合が生じる場合がある
- カウント対象テーブルとカウントを書き込むテーブルのリレーション階層が深い場合でも、モデルのアソシエーションで辿ることができれば対応できる
- カウント数を書き込むカラム名を動的に変更可能
- カウント数を書き込むカラム名の指定にブロックを渡せるため、条件によってカウントさせることができる

後者の`counter_culture`はgemで、`counter_cache`よりも高機能で、大変柔軟に対応できるようになっています

### counter_cultureの使い方

#### 最初に準備すること

Gemfileにgemの設定を追記します


```Gemfile
gem 'counter_culture'
```

カウント値を格納するカラムをつくります

ジェネレータがあるので以下のようにジェネレータを使ってカラムを作ることができます

```
rails generate counter_culture Article comments_count
```


ジェネレータで生成しなくても、以下のようなカラムであれば問題ありません

```
add_column :articles, :comments_count, :integer, :null => false, :default => 0
```

#### 通常のカウント

モデルで以下のように定義します

```comment.rb
class Comment < ActiveRecord::Base
  belongs_to :article
  counter_culture :article
end
```

```article.rb
class Article < ActiveRecord::Base
  has_many :comments
end
```

#### カウントの保存カラム名を変える場合

カラム名を以下のように指定します

```
class Comment < ActiveRecord::Base
  belongs_to :article
  counter_culture :article, column_name: 'admin_comments_count'
end

class Article < ActiveRecord::Base
  has_many :comments
end
```

#### カウントの保存カラムを動的に変更する場合

カラム名にブロックを渡して保存カラム名を動的に変更できます

```comment.rb
class Comment < ActiveRecord::Base
    belongs_to :article
    counter_culture :article, column_name: -> (model) { "#{model.comment_type_name}_comments_count" }

    def comment_type_name
      comment_type == 1 ? 'admin' : 'user'
    end
end
```

動的に変更したカラム名にカウントした場合、後で対象テーブルの値を変更した場合にも自動でカウント数を調整してくれます
例えば上記の例の場合、comment_typeを1から2に変更した場合、以下のようなSQLが発行されてadmin_comments_countが-1されてuser_comments_countが+1されます

```
UPDATE `articles` SET `user_comments_count` = COALESCE(`user_comments_count`, 0) + 1 WHERE `articles`.`id` = 1
UPDATE `articles` SET `admin_comments_count` = COALESCE(`admin_comments_count`, 0) - 1 WHERE `articles`.`id` = 1
```

#### 条件によってカウントする/しないを分ける場合

動的にカウントカラムを変更する方法と同じくブロックを渡して対応できます
カウントしない場合には`nil`を渡すことにより条件によってカウントさせないということが可能です

```comment.rb
class Comment < ActiveRecord::Base
    belongs_to :article
    counter_culture :article, column_name: -> (model) { model.comment_type_name == 1 ? 'admin_comments_count' : nil }

    def comment_type_name
      comment_type == 1 ? 'admin' : 'user'
    end
end
```
```article.rb
class Article < ActiveRecord::Base
  has_many :comments
end
```

#### 階層が深い関連のカウント

階層が深い場合は渡すテーブル名のシンボルを配列にして、カウント対象テーブル側から見たリレーションの階層で指定することで対応可能です

```comment_like.rb
class CommentLike < ActiveRecord::Base
    belongs_to :comment
    counter_culture [:comment, :article], column_name: -> (model) { "#{model.gender_name}_likes_count" }

    def gender_name
      gender == 1 ? 'male' : 'female'
    end
end
```

```comment.rb
class Comment < ActiveRecord::Base
  belongs_to :article
  counter_culture :article, column_name: -> (model) { model.comment_type_admin? 'admin_comment_count' : nil }
end
```

```article.rb
class Article < ActiveRecord::Base
  has_many :comments
end
```

### カウント数の調整

counter_cultureにはカウント数を調整するメソッドが用意されており、既にデータがあるところに導入する場合やデータにズレが生じた時のために`counter_culture_fix_counts`というメソッドが用意されています
Comment.counter_culture_fix_countsと実行すると、カウンターキャッシュを最新状態でアップデートしてくれます

しかしこのメソッドは動的にcolumn_nameを設定しているカラムに対してはサポートされていないようなのでそういった場合は自分で実装する必要がありそうです


### counter_cultureを使うときの注意点
counter_cultureは対象データ更新のトランザクションの外でカウント処理を行うため、テストを書く際に工夫が必要となります
例えばRSpecでDatabaseCleanerを使っている場合には`DatabaseCleaner.strategy = :trunsaction`の設定だとテストが通りません
これは、strategy = trunsactionだとテスト内でトランザクションが完了しないので、カウンターキャッシュによるカウントアップがされないということだと思います
これは、`DatabaseCleaner.strategy = :truncation`にすることで解決できます

全ての設定を`DatabaseCleaner.strategy = :truncation`にするのは都合が悪いことがあると思うので、以下のようにすることでRspec上でメタデータを指定した場合のみtruncationで動かすことができます

まず、spec_helperで`truncation: true`のメタデータを受け取った場合にはtruncationモードで動くように追記します


```spec_helper.rb
config.before(:each) do
  DatabaseCleaner.strategy = example.metadata[:truncation] ? :truncation : :transaction
  DatabaseCleaner.start
end
```

テスト時には対象のテストで`truncation: true`を渡して対応します

```spec_helper.rb
describe Article, truncation: true do
  ここにテストを書きます
end
```