CounterCacheとは
例えば、記事に紐づくコメント数や、いいね数などhasManyで紐づくデータの数を記事のリストと同時に表示したいはずです。
そのようなときはSELECT COUNT(*)で取得する方法が考えられます。
しかし、記事データを取得する際に毎回SELECT COUNT(*)クエリが発行されるのははっきりいって地獄ですよね![]()
しかも、いいね数順にソートしたいときにもいいね数にインデックスを貼ることができないのでさらにパフォーマンスが悪化することが考えられますね...
そのような場合、**記事テーブルにコメント数いいね数カラムをもたせる(キャッシュさせる)**という方法がよく取られます。
あえて正規化を崩すような設計ですが、わざわざ結合や集計をすることなく取得することができ、さらにインデックスを貼ることもできるのでパフォーマンスの向上が見込まれます![]()
記事テーブルにカウント数をもたせる際のデメリット
パフォーマンスは向上しましたが、正規化を崩しているため、当然そのためのデメリットが存在します。
データが追加、削除されるたびに確実にカウント数が更新されるようにして置かなければなりません。
自分でそれを実装するときには処理の漏れがないように...トランザクションは...とかとか面倒なことを考えなれけばいけません
...がでもCakePHPなら大丈夫、CounterCacheという便利なものがあります。![]()
実際にやってみる
それでは記事のタイトルどおり、ユーザーに紐づくフォロー数とフォロワー数をカウントしてみます。
使用するテーブルはそれぞれこんな感じです。
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で生成したときはおそらくこんな感じのテーブルになっているはずです。
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とのリレーションを別名で追加します。
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はビヘイビアーとして定義されています。
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',
]);
}
}
これだけですべての設定は完了です![]()
あとは。エンティティーが保存または削除されるたびにカウンターが更新されます。
ただし、updateAll()、deleteAll() を使用するか、作成した SQL を実行するとカウンターは更新されないことに注意してください![]()