$controller->paginate()で複数の列をソートの条件にする
ということを調べてみた。
CakePHPのバージョンは1.2.5。
使ってるDBMSはMySQL。
複数の列でソートしたい
CakePHPのpaginatorは便利。例えば、Controller内で
$this->set('data', $this->paginate('modelname', $options));
を実行して、View側で
<th> <?= $paginator->sort('hoge', 'columnname1'); ?> </th> <th> <?= $paginator->sort('hoge', 'columnname2'); ?> </th> <th> <?= $paginator->sort('hoge', 'columnname3'); ?> </th>
みたいに記述するだけで、各カラムヘッダにリンクが貼られ、
それぞれをクリックすると、各columnnameでのソートが可能になる。
けど、複数のカラムを組み合わせた条件でソートしたいという瞬間も
(たとえば「入社年月日の降順、氏名の昇順」とか)
この世の中には結構あるわけで、そういうときどうするかを調べてみた。
$model->beforeFind()を使って処理
http://aoyama.accata.com/archives/1813
こちらの方が書いているように、$controller->paginateメソッドは
毎回$model->beforeFindメソッドを実行してくれるので、
任意のmodel内でfunction beforeFind($queryData){} を
オーバーライドすることで、ソート条件(=生成されるSQLのORDER BY句)を
編集することができる。
<?php class MyModel extends AppModel { var $name = 'MyModel'; function beforeFind($queryData) { $queryData['order'] = array( array('入社年月日' => 'desc'), array('氏名' => 'asc'), ); return $queryData; } }
こんな具合。これで、生成されるORDER BY句は
ORDER BY 入社年月日 desc, 氏名 asc
になる。確かになるんだけど。
ソート条件は毎回動的に生成したいお
今回作りたいのは、毎回同じ条件でソートされるものじゃなくて、
ユーザがその都度気分に応じてソート条件を変えられるものに
したかった。上記のようにmodel内のbeforeFindメソッド内で
静的に$queryData['order']を編集していては、paginateどころか
普通に$model->find()を使ってデータを取得したいときも、
毎回同じ条件でソートされることになってしまう。それはいやだ。
ではどうしましょう。
複数のソート条件設定用メソッドを作っとく
MyModelクラス内を以下のようにしておく。
<?php class MyModel extends AppModel { var $name = 'MyModel'; // デフォルトのソート条件は何もなし var $order = array(); function setOrder($order) { // controllerから渡された$orderを変数に持っておく $this->order = $order; } function beforeFind($queryData) { // $queryData['order'] の最初に、そのとき // $this->orderに代入されている検索条件を追加する array_unshift($queryData['order'], $this->order); // 終わったら$this->orderは空にしておく // (じゃないと次回以降の検索時に条件が適用されてしまうので) $this->order = array(); return $queryData; } } ?>
で、controller側では、$this->paginate()を実行する前に、
必要であれば$this->Model->setOrder($order)を実行しておく。
<?php class MyController extends AppController { var $name = 'MyController'; var $uses = array('MyModel'); var $helpers = array('Html', 'Form', 'Paginator'); function hoge() { // paginate前に(必要に応じてソート条件をセット) $order = array( '入社年月日' => 'desc', '氏名' => 'asc' ); $this->MyModel->setOrder($order); // paginate実行 $this->set('data', $this->paginate('MyModel')); } } ?>
setOrder()で渡す配列の中身を、都度好きなように変えれば、
毎回必要な分だけ好きな順序でソートした結果をもって
ページネートできる。
と思ったんだけど。
paginate結果にソートが反映されていない
$this->MyModel->setOrder($order) でセットしたはずの
入社年月日 desc, 氏名 asc のソート条件が結果に反映されない。
なんで?なんぞ?
と思ってcake/cake/libs/controller/controller.php内にある
function paginate を見てみると、controller内で一回$this->paginateを
実行すると、二回Model->find()が呼び出されることを知った。
<?php class Controller extends Object { (中略) if (method_exists($object, 'paginateCount')) { $count = $object->paginateCount($conditions, $recursive, $extra); } else { $parameters = compact('conditions'); if ($recursive != $object->recursive) { $parameters['recursive'] = $recursive; } // ここと $count = $object->find('count', array_merge($parameters, $extra)); } $pageCount = intval(ceil($count / $limit)); if ($page === 'last' || $page >= $pageCount) { $options['page'] = $page = $pageCount; } elseif (intval($page) < 1) { $options['page'] = $page = 1; } $page = $options['page'] = (integer)$page; if (method_exists($object, 'paginate')) { $results = $object->paginate($conditions, $fields, $order, $limit, $page, $recursive, $extra); } else { $parameters = compact('conditions', 'fields', 'order', 'limit', 'page'); if ($recursive != $object->recursive) { $parameters['recursive'] = $recursive; } // ここね。 $results = $object->find($type, array_merge($parameters, $extra)); } (中略)
この二回のfindのうち、一回目はページネートのページ数を求めるためだけに
使われる、検索結果の件数を得る目的のものだ。
したがって、一回目のfindはソート条件とかまったく関係ない話なのであるが、
そのときにもMyModelでオーバーライドしたbeforeFindが実行されて、
<?php class MyModel extends AppModel { (中略) function beforeFind($queryData) { // $queryData['order'] の最初に、そのとき // $this->orderに代入されている検索条件を追加する array_unshift($queryData['order'], $this->order); // 終わったら$this->orderは空にしておく // (じゃないと次回以降の検索時に条件が適用されてしまうので) $this->order = array(); return $queryData; }
しっかりと$this->orderを空にしていたのだった。
つまり、いざ二回目のfind、いわば本当にpaginateの結果を得るための
findを実行するときには、$orderには何の情報も入っていなかったために、
setOrder($order)でセットしたソート条件が表示結果に反映されていなかったのだった。
しかし、対象データの件数を得るためにSELECT文を発行するってのは
どうなんだろう?DBへのアクセスは一回に(最初からちゃんと使う目的でfind)
しておいて、その結果をphpのcount()とかで数えたほうがいいんではなかろうか???
とかいってるけど、その辺、特にベンチとか取ってるわけではないので
この辺にしておく。
うーんじゃぁcountのときはbeforeFindスルーで
今回の場合、setOrderで検索条件をセットしたあとに実行した
一回のpaginate内で実は二回のfindが実行され、その一回目のfindに
呼び出されたbeforeFindによって$orderが初期化されていたのが、
paginateで取得した結果がソートされていない原因だったわけだ。
えっとじゃぁどうしよう。っていうか今回のケースに限らず、
そもそもfind('count')のときって件数さえ取れればいいんだから
ソートは関係ないよな。とか思ったり。。
とりあえず今回は
「MyModelのbeforeFindが実行されたとき、それがfind('count')だったら
$orderには触れない、初期化もしない」
ということにしよう。
<?php class MyModel extends AppModel { (中略) function beforeFind($queryData) { // 実行中のfindの種別が'count'以外だった場合のみ、 // ソート条件を追加し、$orderを初期化する if ($this->findQueryType != 'count' ) array_unshift($queryData['order'], $this->order); $this->order = array(); ) return $queryData; }
もうちっとなんかスマートなやり方があるんではないかとか
思いつつ、とりあえず、これで望む動作をしてくれるようになった。
ので、今回はこれでよしとした。しました。