CakePHP には、紐づく別テーブルの件数が変わるとカウンター用カラムを自動的に更新してくれる、「CounterCache」というビヘイビアがあります。
ドキュメントにある利用例です。
たとえば、記事のリストを表示するときに、記事のコメントの数を表示することができます。 または、ユーザーを表示するときに、彼女が持っている友人/フォロワーの数を表示することもできます。 CounterCache ビヘイビアーは、これらの状況を想定しています。
https://book.cakephp.org/4/ja/orm/behaviors/counter-cache.html
このビヘイビアを使ってみようと思います。
テーブルを作成
まず、テーブルを作成します。
以下のようなテーブル構成とし、記事テーブルがコメント数とタグ数のカラムを持つものとします。
- 記事:コメント = 1:N (hasMany/belongsTo)
- 記事:タグ = N:N (belongsToMany)
/* PostgreSQL */
--
-- 記事
--
CREATE TABLE articles (
id SERIAL PRIMARY KEY, -- ID
title TEXT NOT NULL, -- 記事タイトル
body TEXT, -- 記事本文
name TEXT NOT NULL, -- 記事作者氏名
comment_count INTEGER, -- コメント数
tag_count INTEGER -- タグ数
);
--
-- コメント
--
CREATE TABLE comments (
id SERIAL PRIMARY KEY, -- ID
article_id INTEGER NOT NULL REFERENCES articles(id), -- 記事ID
body TEXT, -- コメント本文
name TEXT NOT NULL -- コメント作者氏名
);
--
-- タグ
--
CREATE TABLE tags (
id SERIAL PRIMARY KEY, -- ID
tag TEXT UNIQUE NOT NULL -- タグ
);
--
-- 記事 <-> タグ(中間テーブル)
--
CREATE TABLE articles_tags (
article_id INTEGER REFERENCES articles(id), -- 記事ID
tag_id INTEGER REFERENCES tags(id), -- タグID
PRIMARY KEY (article_id, tag_id)
);
1. hasMany アソシエーションの場合
記事テーブルのコメント数カラム (articles.comment_count
) を CounterCache で更新します。
なお、これはドキュメントの例と同じです。
1-1. モデルの設定
cake bake
でコードを自動生成します。
$ bin/cake bake model articles
$ bin/cake bake model comments
$ bin/cake bake model tags
$ bin/cake bake model articles_tags
カウント対象の CommentsTable
で、CounterCache ビヘイビアをロードします。
/* src/Model/Table/CommentsTable.php(変更あり) */
public function initialize(array $config)
{
// ... 省略 ...
$this->belongsTo('Articles', [
'foreignKey' => 'article_id',
'joinType' => 'INNER',
]);
// *** 以下 3行を追加 ****
$this->addBehavior('CounterCache', [
'Articles' => ['comment_count']
]);
}
カウンターカラムを持つ ArticlesTable
は cake bake
の自動生成コードのままです。
/* src/Model/Table/ArticlesTable.php(変更なし) */
public function initialize(array $config)
{
// ... 省略 ...
$this->hasMany('Comments', [
'foreignKey' => 'article_id',
]);
}
1-2. 動作確認
save()
だけ試すのに Controller と Template を用意するのも面倒なので、ユニットテストで試してみます。
テストファイルに、以下のようなコードを追加します。
/* tests/TestCase/Model/Table/ArticlesTableTest.php */
public function testCommentCount()
{
$this->Articles->Comments->deleteAll(['id IS NOT NULL']);
$this->Articles->deleteAll(['id IS NOT NULL']);
$data = [
'title' => 'テスト記事タイトル',
'body' => 'テスト記事本文',
'name' => 'テスト記事作者',
'comments' => [
[
'name' => 'テストコメント(1) 作者',
'body' => 'テストコメント(1) 本文',
],
[
'name' => 'テストコメント(2) 作者',
'body' => 'テストコメント(2) 本文',
],
],
];
$entity = $this->Articles->newEntity($data);
$result = $this->Articles->save($entity);
$article = $this->Articles->get($result->id, [
'contain' => ['Comments']
]);
pr($article->toArray());
}
また、テスト用のデータは不要なので、tests/Fixture/ 以下のファイルの init()
で $this->records
の中身を空にしておきます。
/* tests/Fixture/* */
public function init()
{
$this->records = [];
parent::init();
}
実行して save()
後の get()
結果を見てみると、「comment_count」が更新されていることがわかります。
$ vendor/bin/phpunit tests/TestCase/Model/Table/ArticlesTableTest.php --filter testCommentCount
Array
(
[id] => 1
[title] => テスト記事タイトル
[body] => テスト記事本文
[name] => テスト記事作者
[comment_count] => 2
[tag_count] =>
[comments] => Array
(
[0] => Array
(
[id] => 1
[article_id] => 1
[body] => テストコメント(1) 本文
[name] => テストコメント(1) 作者
)
[1] => Array
(
[id] => 2
[article_id] => 1
[body] => テストコメント(2) 本文
[name] => テストコメント(2) 作者
)
)
)
=> TABLE articles;
id | title | body | name | comment_count | tag_count
----+--------------------+----------------+----------------+---------------+-----------
1 | テスト記事タイトル | テスト記事本文 | テスト記事作者 | 2 |
(1 row)
=> TABLE comments;
id | article_id | body | name
----+------------+------------------------+------------------------
1 | 1 | テストコメント(1) 本文 | テストコメント(1) 作者
2 | 1 | テストコメント(2) 本文 | テストコメント(2) 作者
(2 rows)
2. belongsToMany アソシエーションの場合
記事テーブルのタグ数カラム (articles.tag_count
) を CounterCache で更新します。
ドキュメントに、belongsToMany アソシエーションで使うには、という説明があるのですが、例もなく、で、結局使えるのか?と不安になります。(StackOverflow でも解決したのか不明な質問が。。)
https://book.cakephp.org/4/ja/orm/behaviors/counter-cache.html#id2
CounterCache ビヘイビアーは、 belongsTo アソシエーションに対してのみ機能します。 たとえば、 "Comments belongsTo Articles" の場合、Article テーブルの comment_count を生成するために、 CommentsCache ビヘイビアーを CommentsTable に追加する必要があります。これを belongsToMany アソシエーションに対して機能させることは可能ですが、 アソシエーションオプションで設定されたカスタム through テーブルで CounterCache ビヘイビアーを有効にして cascadeCallbacks 設定オプションを true にする必要があります。 カスタム JOIN テーブルを設定する方法は 'through' オプションの使用 を参照してください。
結論としては、以下のとおり利用できました。
2-1. モデルの設定
CounterCache ビヘイビアを、中間テーブル ArticlesTagsTable
でロードします。
(ドキュメントに、中間テーブルで、と書いていないのがわかりづらい・・・)
/* src/Model/Table/ArticlesTagsTable.php(変更あり) */
public function initialize(array $config)
{
// ... 省略 ...
$this->addBehavior('CounterCache', [
'Articles' => ['tag_count'],
]);
}
次に ArticlesTable
で belongsToMany アソシエーションを指定する箇所で、中間テーブルを指定するのに「joinTable
」の代わりに「through
」を使い、「cascadeCallbacks
」を TRUE
にします。
belongsToMany アソシエーションの配列で可能なキー
https://book.cakephp.org/4/ja/orm/associations.html#belongstomany
/* src/Model/Table/ArticlesTable.php (変更あり)*/
public function initialize(array $config)
{
// ... 省略 ...
$this->belongsToMany('Tags', [
'foreignKey' => 'article_id',
'targetForeignKey' => 'tag_id',
// *** 以下の 1 行を削除かコメントアウト ***
// 'joinTable' => 'articles_tags',
// *** 以下の 2 行を追加 ***
'through' => 'articles_tags',
'cascadeCallbacks' => TRUE,
]);
}
なお、TagsTable
は自動生成コードのまま変更ありません。
/* src/Model/Table/TagsTable.php(変更なし) */
public function initialize(array $config)
{
// ... 省略 ...
$this->belongsToMany('Articles', [
'foreignKey' => 'tag_id',
'targetForeignKey' => 'article_id',
'joinTable' => 'articles_tags',
]);
}
2-2. 動作確認
同じく、ユニットテストで試します。
テストファイルに、以下のようなコードを追加します。
/* tests/TestCase/Model/Table/ArticlesTableTest.php */
public function testTagCount()
{
$data = [
'title' => 'テスト記事タイトル',
'body' => 'テスト記事本文',
'name' => 'テスト記事作者',
'tags' => [
[
'tag' => 'タグ (1)',
],
[
'tag' => 'タグ (2)',
],
],
];
$entity = $this->Articles->newEntity($data);
$result = $this->Articles->save($entity);
$article = $this->Articles->get($result->id, [
'contain' => ['Tags']
]);
pr($article->toArray());
}
実行して save()
後の get()
結果を見てみると、「tag_count」が更新されていることがわかります。
$ vendor/bin/phpunit tests/TestCase/Model/Table/ArticlesTableTest.php --filter testTagCount
Array
(
[id] => 1
[title] => テスト記事タイトル
[body] => テスト記事本文
[name] => テスト記事作者
[comment_count] =>
[tag_count] => 2
[tags] => Array
(
[0] => Array
(
[id] => 1
[tag] => タグ (1)
[_joinData] => Array
(
[article_id] => 1
[tag_id] => 1
)
)
[1] => Array
(
[id] => 2
[tag] => タグ (2)
[_joinData] => Array
(
[article_id] => 1
[tag_id] => 2
)
)
)
)
=> TABLE articles;
id | title | body | name | comment_count | tag_count
----+--------------------+----------------+----------------+---------------+-----------
1 | テスト記事タイトル | テスト記事本文 | テスト記事作者 | | 2
(1 row)
=> TABLE tags;
id | tag
----+----------
1 | タグ (1)
2 | タグ (2)
(2 rows)
=> TABLE articles_tags;
article_id | tag_id
------------+--------
1 | 1
1 | 2
(2 rows)
補足
ドキュメントの最初に、以下のような注意書きがあります。
カウンターの値は、エンティティーが保存または削除されるたびに更新されます。 updateAll() または deleteAll() を使用するか、作成した SQL を実行すると、 カウンターは更新 されません。
https://book.cakephp.org/4/ja/orm/behaviors/counter-cache.html#id1
これは、CounterCache ビヘイビアは 内部的 には、save()
時に afterSave イベント として、カウンターカラムを UPDATE
していることによります。(delete()
時は afterDelete イベント)。
updateAll()
や deleteAll()
では afterSave/afterDelete イベントが発生しないので、カウンターは更新されないことになります。
https://book.cakephp.org/4/ja/orm/saving-data.html#Cake\ORM\Table::updateAll