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 を実行するとカウンターは更新されないことに注意してください