VitualFieldsを使おう!

CakePHP1.3からは、VirtualFieldsが利用できる。
これはSQL でいったら sql_function(hoge) as aliasといったcolmunをそのままフィールドとして利用できる機能だ。

それでは実例を追いながら順次説明していこう。

要求:

グループの一覧を表示したい。
また、グループには何人参加しているかを集計して同時に表示する。

関係

Group hasAndBelongsToMany User

Model

<?php
class Group extends AppModel {

	var $name = 'Group';
	
	var $virtualFields = array(
		'user_count' => 'count(GroupsUser.user_id)',
	);
	var $hasAndBelongsToMany = array(
		'User',
	);
}
?>

Modelのフィールドとして、$virtualFieldsを宣言する。この中にvirtualFieldsとして設定したいものを書いていく。
上記例のように、'エイリアス名' => 'SQLのカラム指定構文'を記述してゆく。

Controller

<?php
class GroupsController extends AppController {
	function index() {
		$this->Group->recursive = 0;
		$this->paginate = array(
			'Group' => array(
				'joins' => array(
					array(
						'type' => 'LEFT',
						'alias' => 'GroupsUser',
						'table' => 'groups_users',
						'conditions' => 'Group.id = GroupsUser.group_id'
					)
				),
				'group' => array(
					'Group.id'
				),
				'fields' => array(
					'Group.id',
					'Group.name',
					'user_count' // vitualFields!
				)
			),
		);
		$this->set('groups', $this->paginate());
		// var_dump($this->viewVars['groups']);
	}
?>

さて、optionsのfieldsやorder、group等でvirtualFieldsを指定するときは、モデル名を含めない
つまり

  • 'Model.virtual_fields'

ではなく、単に

  • 'virtual_fields'

と指定する。
モデル名を含めると、通常のFieldsと解釈され、DBMに「そんなカラムねーよ、ぷんすか」と怒られてしまう。
なんでやねんって思う人はdbo_source.phpで"virtual"で検索してみると呑み込めるかもしれない。
今後、Model名つきで指定できるようになるかは不明だ。
→修正されます。VirtualFieldsで、'Model.name'が指定できないバグを参照してください。

あと重要なのは、options[fields]を指定しない場合、つまりModel.*や*.*を取得する場合、VirtualFieldsも含まれることに注意

View

<table cellpadding="0" cellspacing="0">
<tr>
	<th><?php echo $paginator->sort(__('名前',true),'name');?></th>
	<th><?php echo $paginator->sort(__('参加ユーザ',true),'user_count'); /* Virual Fields!! */ ?></th>
</tr>
<?php

$i = 0;
foreach ($groups as $group):
	$class = null;
	if ($i++ % 2 == 0) {
		$class = ' class="altrow"';
	}
?>
	<tr<?php echo $class;?>>
		<td>
			<?php echo $html->link($group['Group']['name'], array('action' => 'view', $group['Group']['id'])) ?>
		</td>
		<td>
			<?php echo ife($group['Group']['user_count'],sprintf(__('%d人',true),$group['Group']['user_count']),__('なし',true)) ?>
		</td>
	</tr>
<?php endforeach; ?>
</table>

最後にビューでは面白いことになっている。
$paginator->sort()は、orderするFieldが指定できるが、VirtualFieldも指定できるようになった。
しかし、実際に表示するところを見てもらいたい。

<?php
$group['user_count']
?>

ではなく、

<?php
$group['Group']['user_count']
?>

となっている。つまりfind()の返り値としては、Groupモデルとして認識されて格納されているわけだ。
1.2以前でaliasを指定した場合は、

<?php
$group[0]['user_count']
?>

としなければならなかったがこれも不正解となる。

最後に吐き出されるSQLを見てみよう。

SQL

ちなみにuser_countでソートした時のSQLになる。

SELECT
	`Group`.`id`,
	`Group`.`name`,
	(count(`GroupsUser`.`user_id`)) AS `Group__user_count`
FROM
	`groups` AS `Group`
LEFT JOIN
	groups_users AS `GroupsUser` ON(
		`Group`.`id` = `GroupsUser`.`group_id`
	)
WHERE
	1 = 1
GROUP BY
	`Group`.`id`
ORDER BY
	(count(`GroupsUser`.`user_id`)) asc
LIMIT
	20

(見やすいように適当に整形した)

エイリアス名 => モデル名__VirtualFields名
となっているが、ORDER BY句ではエイリアスを使用していない。
何か意図があるかは分からないが、やって欲しいことは十分満たすようだ。

まとめ・今後

VirtualFieldsはクールだ。
集計関数は必ずVirualFieldsとして宣言してしまえば、集計関数がコードのそこら中に散らばってメンテナンスが難しくなることを回避できる。
更にpaginatorやcontainに含めることによって、コーディングが一貫し、非常に楽になる。

それとあっという間に集計関数のソート付きページ付けが実装できるのは非常に気持ちがいい。
是非、今すぐ1.3に移行して、(もしくは移行してる人も、)VirtualFieldsを使ってみて欲しい。

今後はvalidateに応用できないか等、細かい未検証な点があったら随時記事にして行きたいと思う。

おまけ

1.2でpaginatorに集計関数が渡せない!ぷんすかって人に、自分があててたパッチを紹介。
AppControllerにそのまま貼れば(多分)動作します。

http://bin.cakephp.org/view/1057852525

使い方は、$fields = array(
'sum_function(A.hoge) as alias'
)
'as' が必須なのに注意。
後はalias部分を$paginator->sort()に渡せます。