読者です 読者をやめる 読者になる 読者になる

CakePHP : 同カラムに対する複数のLIKEを連結する

PHP PostgreSQL プログラミング CakePHP

前回の続き。

前回はPostrgreSQLから配列を取り出す方だったが、次はそれを使う方。

対象のテーブル

データ

product_id code tag publisher
101 123456 書籍,技術書,Java オライリー
102 234567 書籍,技術書,PHP ソフトバンククリエイティブ
103 345678 書籍,技術書,PHP ソフトバンクパブリッシング
104 456789 CD,アニメ,アイカツ ランティス

NGフィルタ

target filter_string
tag CD
tag Java
publisher ソフトバンククリエイティブ

データに対して、NGフィルタをかけたい、というもの。
結果としては、

product_id code tag publisher
103 345678 書籍,技術書,PHP ソフトバンクパブリッシング

こうなることを想定する。

ストアドを使って一撃でやってもいいのだが、メンテが大変なので間にPHPを挟んで、NGフィルタの取得と、その結果をWHERE句に含めた取得クエリの2回発行することにする。

CakePHP のWHERE句指定方法

CakePHPでは、SQLそのものの作成はモデルクラスに任せることになっている。
このため、SELECT句やFROM句、WHERE句の配列をモデルクラスに渡すことまでがコントローラというロールの限界であり、これ以上のところに踏み込んではいけない。

だが、このWHERE句がややこしくてちょっとハマった。
今回のNGフィルタでは NOT LIKE 〜〜〜 AND NOT LIKE 〜〜〜 AND ・・・という風にANDで接続していけばいい。
この配列を作る。

結論から言うと、今回の場合求められる配列は以下のようになる。

<?php
$conditions = array(
                  array('tag NOT LIKE'=>'%CD%'),
                  array('tag NOT LIKE'=>'%Java%'),
                  array('publisher NOT LIKE'=>'%ソフトバンククリエイティブ%'),
              );


明示的に書かない限り、ANDで連結されるようになっている。
また、演算の種類を省略すると、イコールになる。
今回はANDは省略し、NOT LIKE を指定している。


下記のようなシンプルな形もあるのだが、

<?php
$conditions = array(
                  'tag NOT LIKE'=>'%CD%',
                  'tag NOT LIKE'=>'%Java%',   // ←キーの重複
                  'publisher NOT LIKE'=>'%ソフトバンククリエイティブ%',
              );

今回は同じカラムに対して同じ判定方法で複数の条件があるため、キーが重複してしまい、後者を使うことができない。

なので、前者を作るために、前回の記事の内容を使う。

WHERE句のもととなる配列を作る

さきほどのこのクエリを使って、

SELECT target, json_agg(filter_string) AS json_filter
FROM filters
GROUP BY target


こうする

target json_filter
tag ["CD","Java"]
publisher ["ソフトバンククリエイティブ"]


そうなったら、こうする

<?php
$conditions=array();
foreach($filters as $filter){
    $target=$filter['Filter']['target'];
    foreach(json_decode($filter['Filter']['json_filter']) as $filterString){
        $conditions[]=array($target.' NOT LIKE'=>'%'.$filterString.'%');
    }
}


すると$conditionsはこうなって完成

<?php
$conditions = array(
                  array('tag NOT LIKE'=>'%CD%'),
                  array('tag NOT LIKE'=>'%Java%'),
                  array('publisher NOT LIKE'=>'%ソフトバンククリエイティブ%'),
              );

実際に取得する

こうだ

<?php
$result = $this->Data->find('all', array(
                'fields' => array('Data.*'),
                'conditions' => $conditions,
            ));

以上だ!

余談

PHPでは、array()を用いた配列の宣言時にデータを入れる場合、最終要素の後にカンマを余分にいれてもエラーにならない。
一見ややこしい仕様だが、これには実用上かなり助けられている。

たとえば、以前こうなっていたデータが、

<?php
$conditions = array(
                  array('tag NOT LIKE'=>'%CD%'),
                  array('tag NOT LIKE'=>'%Java%'),
                  array('publisher NOT LIKE'=>'%ソフトバンククリエイティブ%')  //←カンマがないことに注目
              );


誰かの編集でこうなったとする

<?php
$conditions = array(
                  array('tag NOT LIKE'=>'%CD%'),
                  array('tag NOT LIKE'=>'%Java%'),
                  array('publisher NOT LIKE'= '%ソフトバンククリエイティブ%'),  //←カンマがあることに注目
                  array('publisher NOT LIKE'=>'%ランティス%')
              );


これらの2ファイルでdiffを取ると、意味上では1行の変更で1要素の追加であるにもかかわらず、カンマが追加されていることによって、感覚的に変更されていない3番目の要素も変更した行となってしまう。

バージョン管理システムやdiffツールで変更があった箇所を精査する際、この元から書いておけばよかったカンマ1個のために、精査する行が増えてしまう。
さらに、よくある変更ではあるので、なんだカンマだけの変更か、と軽く見てしまうと、実はその行にもう1箇所変更があり、文法エラーが新たに混入したことに気づけない。

上記のため、筆者は必ず最後の要素でもカンマを書き込んでいる。