【CakePHP3】Collection、max(),min()の挙動と使い方詳解

2020/02/16

author

masyus

CakePHP3のResultSetでも継承されているCollectionクラスはextract()やfilter()等の便利な関数を備えており、ORMによりデータベースからSELECTしてきたデータの加工や抽出に一役買ってくれます。

その中でも今回はmax()min()の挙動について紹介します。いずれも、特定のカラムにおける最大値・最小値を所持するレコードをEntityとして返す等する便利な関数です。検証したCakePHPのバージョンは3.6.13、PHPは7.3です。

max(),min()の使い方

例えばMySQLのarticlesテーブルにpage_view_countというカラムがあったとして、全記事からページビュー数が最大のものを1件だけピックアップしたいとします。

$maxPageViewArticle = $this->Articles
    ->find()
    ->order(['page_view_count' => 'DESC'])
    ->limit(1)
    ->first();

上記のように実装可能ですが、max()を使うと下記のようにも実装できます。

$maxPageViewArticle = $this->Articles
    ->find()
    ->max('page_view_count');

シンプルではありますが、発行されるSELECT文は一度全件を返すため実行時間が長くなる可能性がある点に気を遣いましょう。可能でしたらwhere()を使いSELECT対象を絞る等してください。

また、max()を使った時にfirst()が不要になる理由は、CakePHP3のSkeletonにおけるCollectionTrait.phpにて定義されたmax()の内部処理でfirst()を実行した結果を返すためです。

// ... 省略 ...

    /**
     * {@inheritDoc}
     */
    public function max($callback, $type = \SORT_NUMERIC)
    {
        return (new SortIterator($this->unwrap(), $callback, \SORT_DESC, $type))->first();
    }

// ... 省略 ...

この仕様はmin()でも同様になります。参考から当該コードを辿れますので、興味のある方は見てみてください。

最大値を持つレコードがある複数ある時に、該当レコードを全て抽出する方法

上述の通りmax()はレコード1件分を返す仕様のため、「最大値を持つレコードがある複数ある時に該当レコードを全て抽出したい」時は使えません。この要望を実現するには代替案として2つ方法があります。

まず、下準備として下記を実装します。

// 使い回しやすいよう、Queryオブジェクトを事前に生成
$query = $this->Articles
    ->find();

// max_view_countの値を先に取得しておく
$maxViewCount = $query
    ->max('page_view_count')
    ->page_view_count;

以降は2つ実装方法があります。

【方法1】SQLに組み込んで抽出する

$maxPageViewArticle = $query
    ->where(['page_view_count' => $maxViewCount])
    ->toArray();

【方法2】filter()関数で抽出する

$maxPageViewArticle = $query
    ->filter(function ($article) use ($maxViewCount) {
        return $article->page_view_count === $maxViewCount;
    })
    ->toArray();

個人的にはクエリーの効率を高めた上で処理したいと考えますので、A案を採用するかと思われます。

参考