綺麗に死ぬITエンジニア

CakePHP 3のORM matchingメソッドで複数の条件を指定する方法

2016-08-30

CakePHP 3系にて、アソシエーションを利用する場合に便利なmatchingメソッド。

便利に活用させていただいていたのですが、利用していく上で、複数の条件を指定する場合において少し悩んだので、備忘録として。

使い方

まずは通常の使い方から。

matchingは、多対多(belongsToMany)の関係を持つ2つのテーブルにおいて、片一方のテーブルに紐づくデータによって、もう片一方のテーブルから取得するデータをフィルタリングするメソッドです。

例えば、ブログの記事情報を格納するPostsテーブルとブログのタグ情報を格納するTagsテーブルが存在する場合、この2つのテーブルは多対多の関係(ブログは複数のタグを持ち、タグは複数のブログに割り当てられる)となりますが、特定のタグを持つブログのみを絞り込んで取得したい場合などに、このmatchingメソッドの出番となります。

特定のタグを持つブログのみを絞り込んで取得したい場合は、以下のようなコードで取得できます。

<?php
// コントローラやテーブルのメソッド内で
$tagName = 'CakePHP';

$query = TableRegistry::get('Posts')->find();
$query->matching('Tags', function (Query $q) use ($tagName) {
  return $q->where(['Tags.name' => $tagName]);
});

上記のコードで、"CakePHP"というタグを持つPostsだけに絞り込まれるようになります。

複数の検索値のうち、少なくともどれか一つにマッチするデータを取得する

例えば、"CakePHP"と"PHP"というタグの、少なくともどちらか一方を持っているブログ記事を取得するには、以下のようにします。

<?php
$tagNames = ['CakePHP', 'PHP'];

$query = TableRegistry::get('Posts')->find();
$query->matching('Tags', function (Query $q) use ($tagNames) {
  return $q->where(['Tags.name IN' => $tagNames]);
});

検索条件を配列で保持し、IN句を用いることで比較的簡単に実装可能です。2つ以上の条件の場合においても、配列の要素数を増やすだけで動作します。

複数の検索値のうち、全てにマッチするデータを取得する

例えば、"CakePHP"と"PHP"というタグの、両方ともを持っているブログ記事を取得するには、以下のようにします。

<?php
$tagNames = ['CakePHP', 'PHP'];

$query = TableRegistry::get('Posts')->find();
$query
  ->matching('Tags', function (Query $q) use ($tagNames) {
    return $q->where(['Tags.name IN' => $tagNames]);
  })
  ->group(['Posts.id'])
  ->having([
    $this->query()->newExpr()->eq('COUNT(DISTINCT Tags.name)', count($tagNames))
  ]);

検索条件を配列で保持し、IN句、GROUP BY句及びHAVING句を用いることで実装可能です。2つ以上の条件の場合においても、配列の要素数を増やすだけで動作します。

複数の関係における検索値をAND条件で取得したい

例えば、Postsと多対多の関係にあるテーブルがTagsとCategories(カテゴリー情報)の2つあり、その両方の検索結果にてAND条件で絞り込みたい場合、単にmatchingメソッドを2度実行するだけで実装可能です。

<?php
$tagNames = ['CakePHP', 'PHP'];
$categoryNames = ['IT', 'コンピュータ'];

$query = TableRegistry::get('Posts')->find();
$query->matching('Tags', function (Query $q) use ($tagNames) {
  return $q->where(['Tags.name IN' => $tagNames]);
})->matching('Categories', function (Query $q) use ($categoryNames) {
  return $q->where(['Categories.name IN' => $categoryNames]);
});

上記のコードでは、「タグに"CakePHP"もしくは"PHP"を持ち、なおかつカテゴリに"IT"もしくは"コンピュータ"を持つブログ記事」を取得します。

複数の関係における検索値をOR条件で取得したい

Postsと多対多の関係にあるテーブルがTagsとCategoriesの2つあり、その両方の検索結果にてOR条件で絞り込みたい場合、例えば「タグに"PHP"、もしくはカテゴリーに"IT"を持つブログ記事を取得する」などの条件をmatchingメソッドを用いて一度に実行するのは、いろいろ調べてみましたが不可能なようです。(可能である場合は是非教えていただきたい)

実装したい場合は、matchingメソッドを使わずにクエリビルダでSQLを生成していくか、サブクエリを用いて実行する必要があります。サブクエリを用いて実行する例を以下に示します。

<?php
$tagName = 'PHP';
$categoryName = 'IT';

$postsTable = TableRegistry::get('Posts');

$conditions = [];
$conditions[] = ['Posts.id IN' =>
                 $postsTable->find()
                 ->select(['Posts.id'])
                 ->matching('Tags', function (Query $q) use ($tagName) {
                   return $q->where(['Tags.name' => $tagName]);
                 })
                ];
$conditions[] = ['Posts.id IN' =>
                 $postsTable->find()
                 ->select(['Posts.id'])
                 ->matching('Categories', function (Query $q) use ($categoryName) {
                   return $q->where(['Categories.name' => $categoryName]);
                 })
                ];

$query = $postsTable->find();
$query->where(['OR' => $conditions]);

サブクエリで該当のブログ記事のID一覧を取得し、そのIDを条件にOR条件で取得しなおすことで、複数の関係におけるmatchingメソッドのOR条件を実現しています。

参考サイト

筆者について

フリーランスエンジニアとして活動している、「もりやませーた」です。

筆者のTwitterはこちら。記事に関するご意見等はTwitterの方へお寄せください。

その他業務に関するお問い合わせは、こちらのページをご覧ください。

PHP SQL