LoginSignup
13
17

More than 5 years have passed since last update.

FuelPHPのORMで集計をする

Last updated at Posted at 2015-07-05

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

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

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

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

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

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

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

さらに、
FuelPHPのORMで子モデルを作成する
とかでクエリーを追加しているモデルでやろうとすると条件文が増えてしまうし、条件が変更になったらこちらも修正しないといけないので大変!

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

ということです。

とりあえずやってみる


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はこんな感じです。(適宜改行しています)

[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()は変えてません)

[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

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

最後の1行は何なんだと

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

[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すれば普通にでてきますが、今は一覧で欲しかったのです。

>>> $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使おうよ!っていうまとめ

13
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
17