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条件を実現しています。