【CakePHP】CounterCacheでユーザーのフォロー数とフォロワー数をカウントする

# CounterCacheとは

例えば、記事に紐づくコメント数や、いいね数など`hasMany`で紐づくデータの数を記事のリストと同時に表示したいはずです。

そのようなときは`SELECT COUNT(*)`で取得する方法が考えられます。
しかし、記事データを取得する際に毎回`SELECT COUNT(*)`クエリが発行されるのははっきりいって地獄ですよね:confounded:
しかも、いいね数順にソートしたいときにもいいね数にインデックスを貼ることができないのでさらにパフォーマンスが悪化することが考えられますね...

そのような場合、**記事テーブルにコメント数いいね数カラムをもたせる(キャッシュさせる)**という方法がよく取られます。
あえて正規化を崩すような設計ですが、わざわざ結合や集計をすることなく取得することができ、さらにインデックスを貼ることもできるのでパフォーマンスの向上が見込まれます:laughing:

### 記事テーブルにカウント数をもたせる際のデメリット
パフォーマンスは向上しましたが、**正規化を崩している**ため、当然そのためのデメリットが存在します。
データが追加、削除されるたびに確実にカウント数が更新されるようにして置かなければなりません。
自分でそれを実装するときには処理の漏れがないように...トランザクションは...とかとか面倒なことを考えなれけばいけません:weary:...がでもCakePHPなら大丈夫、[CounterCache](https://book.cakephp.org/3/ja/orm/behaviors/counter-cache.html)という便利なものがあります。:joy:


# 実際にやってみる

それでは記事のタイトルどおり、ユーザーに紐づくフォロー数とフォロワー数をカウントしてみます。
使用するテーブルはそれぞれこんな感じです。

### UsersTable
| 列名 | データ | 長さ |
|:-:|:-:|:-:|
|  ID | INT  |  11 |
|  username |  VARCHAR | 20  |
| password | VARCHAR | 255 | 
| follow_count | INT | 11 | 
| follower_count | INT | 11 |

`follow_count`と`follower_count`が今回のキモですね。
`CounterCache`により更新されるフィールドは`INT`型である必要があります。

### FollowsTable
| 列名 | データ | 長さ |
|:-:|:-:|:-:|
| user_id  | INT  | 11  |
| follow_id  | INT  |  11 |

`user_id`がフォローしたユーザーのIDで`follow_id`がフォローされたユーザーのIDとなります。

例えば、次のようなデータがあった場合、

| user_id | follow_id |
|:-:|:-:|
|  1 |  2 |
|  2 |  1 |
|  3 |  1 |

ユーザー1はユーザー2のフォロワーという関係になり、また互いに相互フォローとなります。
この時、ユーザー1の`UsersTable`は`follow_count`が1、`follower_count`が2となりますね。

### FollowsTable

さっそく`FollowersTable`を実装します。
`bin/cake bake model Follows`で生成したときはおそらくこんな感じのテーブルになっているはずです。

```FollowsTable.php
namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\ORM\TableRegistry;

class FollowsTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('follows');
        $this->setDisplayField('id');
        $this->setPrimaryKey('id');

        $this->belongsTo('Users', [
            'foreignKey' => 'user_id',
            'joinType' => 'INNER',
        ]);
    }
    
    // Validationは省略
}
```

`CouterCache`を利用するためには、更新するフィールドとアソシエーリョンを構築する必要があります。
すでに`UsersTable`とのアソシエーリョンは設定されていますが、このままだとフォロワー数をカウントすることができません。`UsersTable`とのリレーションを別名で追加します。

```diff
class FollowsTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('follows');
        $this->setDisplayField('id');
        $this->setPrimaryKey('id');

        $this->belongsTo('Users', [
            'foreignKey' => 'user_id',
            'joinType' => 'INNER',
        ]);
+
+       $this->belongsTo('FollowUsers', [
+            'className' => 'Users',
+            'foreignKey' => 'follow_id',
+            'joinType' => 'INNER',
+        ]);
    }
}
```

同一テーブルに対して複数のアソシエーリョンを構築するときには、別の名前で登録して`className`でアソシエーリョン先のテーブルを指定します。

### CounterCacheビヘイビアーを有効にする

アソシエーリョンが完了したところで、`CounterCache`を有効にしましょう！
`CounterCache`はビヘイビアーとして定義されています。

```diff
class FollowsTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('follows');
        $this->setDisplayField('id');
        $this->setPrimaryKey('id');
+
+       $this->addBehavior('CounterCache', [
+            'Users' => ['follow_count'],
+            'FollowUsers' => ['follower_count']
+        ]);

        $this->belongsTo('Users', [
            'foreignKey' => 'user_id',
            'joinType' => 'INNER',
        ]);

       $this->belongsTo('FollowUsers', [
            'className' => 'Users',
            'foreignKey' => 'follow_id',
            'joinType' => 'INNER',
        ]);
    }
}
```

これだけですべての設定は完了です:wink:

あとは。エンティティーが保存または削除されるたびにカウンターが更新されます。
ただし、`updateAll()`、`deleteAll()` を使用するか、作成した SQL を実行すると**カウンターは更新されない**ことに注意してください:confounded: