LoginSignup
2
0

More than 3 years have passed since last update.

CakePHPのCounterCacheで関連件数を自動カウント

Last updated at Posted at 2020-06-02

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']
    ]);
}

カウンターカラムを持つ ArticlesTablecake 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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0