FuelPHPのORMで集計をする

DBクラスを使っての集計の仕方は検索すればでてくるけど、ORMだけでする方法がなかなか無かったので検証してみました。

# そもそもなぜORMでかというと

集計クエリを発行する方法として、検索すると以下のような方法がよく出てきます。

```php
$result = \DB::select(\DB::expr('SUM(`score`)'))->from('answers')->exectute();
```

もちろん、うまくいきますが、
この```Model_Answer```クラスが論理削除モデルだったらどうなるかというと、

```php
$result = \DB::select(\DB::expr('SUM(`score`)'))
	->where(array(
		array('deleted_at','IS',null)
	))
	->from('answers')
	->exectute();
```

なんて感じになって、論理削除モデルでせっかくdeleted_atを意識しなくてよくなってたのにもうめんどくさい！

さらに、
[FuelPHPのORMで子モデルを作成する](http://qiita.com/goosys/items/d8f5eb1e81213d9a0256)
とかでクエリーを追加しているモデルでやろうとすると条件文が増えてしまうし、条件が変更になったらこちらも修正しないといけないので大変！

できればモデルでの定義をそのまま使って集計したい！

ということです。

# とりあえずやってみる

```php

class Model_Question extends Model
{
...
	public function calc_total()
	{
		$options = array(
			'select' => array(
				array(DB::expr('COUNT(*)'), 'num_answers') ,
				array(DB::expr('FLOOR( AVG(`score`) )'), 'average_answers')
			),
			'where' => array(
				'question_id' => $this->id ?: -1
			),
			'group_by' => array(
				'question_id',
			)
		);
		
		$total = Model_Answer::find('first',$options);
		$result = ( $total )? array_diff_assoc($total->to_array(true), $total->to_array()): array();
		
		return $result;
	}
```

※クイズ（Model_Question）に解答（Model_Answer）が複数紐付いている状態です。

実行結果と流れたSQLはこんな感じです。（適宜改行しています）

```php
[projects@d2ee29bda854 project]$ oil console
>>> Model_Question::find('first')->calc_total()
array (
  'num_answers' => '9',
  'average_answers' => '65',
)

>>> DB::last_query()
SELECT COUNT(*) AS `t0_c0`, FLOOR( AVG(`score`) ) AS `t0_c1`, `t0`.`id` AS `t0_c2` 
FROM `answers` AS `t0` 
WHERE `t0`.`question_id` = '2' 
GROUP BY `t0`.`question_id` 
ORDER BY `t0`.`id` ASC 
LIMIT 1
```

うまくいきました。

# 論理削除モデルだと

そして、論理削除モデルにしてみるとこうなります。（```calc_total()```は変えてません）

```php
[projects@d2ee29bda854 project]$ oil console
>>> Model_Question::find('first')->calc_total()
array (
  'num_answers' => '8',
  'average_answers' => '70',
)

>>> DB::last_query()
SELECT COUNT(*) AS `t0_c0`, FLOOR( AVG(`score`) ) AS `t0_c1`, `t0`.`id` AS `t0_c2` 
FROM `answers` AS `t0` 
WHERE `t0`.`question_id` = '2' AND `t0`.`deleted_at` IS null 
GROUP BY `t0`.`question_id` 
ORDER BY `t0`.`id` ASC 
LIMIT 1
```

やっほー！
なんてシンプル！

# 最後の１行は何なんだと

思われるでしょうと思いますので、補足です。

```php
[projects@d2ee29bda854 project]$ oil console
>>> $options = array( ... ) //上と同じものを入れたとします
>>> $total = Model_Answer::find('first',$options);
>>> $total
Model_Answer::__set_state(array(
   '_is_new' => false,
   '_frozen' => false,
   '_sanitization_enabled' => false,
   '_data' =>
  array (
    'id' => '10',
  ),
   '_custom_data' =>
  array (
    'num_answers' => '1',
    'average_answers' => '99',
  ),
   '_original' =>
...
))
```

こんな感じで、モデルの```properties```に定義されていなカラムは```_custom_data```という所に格納されていて、普通に```to_array()```しても出てきません。

が、第一引数にtrueを入れるとあっさりでてきます。
※もちろん、個別にgetすれば普通にでてきますが、今は一覧で欲しかったのです。

```php
>>> $total->to_array()
array (
  'id' => '10',
)

>>> $total->to_array(true)
array (
  'num_answers' => '1',
  'average_answers' => '99',
  'id' => '10',
)

>>> $total->average_answers
99
>>> $total->get('average_answers')
99
```

このidはおそらくPrimaryKeyが全て出てきてしまうような気がしたので、これらを除いて```_custom_data```の部分だけ配列にしてもらいたかったので、```array_diff_assoc```で差分をとっています。

# おわり

もちろん、sumでももっと複雑な集計でもできると思いますし、リレーションしているモデルがあればそれも今までどおり使えるはずです。（未検証）

要はみんなもっとORM使おうよ！っていうまとめ