CakePHP 4.x: `TableClass->find()...->all()` では `ResultSet` が返り、`ResultSet` を foreach で回すと `PDOStatement::fetch` しDBから取得したデータがメモリ上に展開される

※2022/02/05: 追記 (->toArray() する際の注意点)

ドキュメント

TableClass->find()...->all() では ResultSet が返り、ResultSet を foreach で回すと PDOStatement::fetch しDBから取得したデータがメモリ上に展開される

  • TableClass->find()...->all()\Cake\ORM\ResultSet を返す

  • \Cake\ORM\ResultSet

    • 取得した時点では PDOStatement::execute を実行した段階
      • この時点では、メモリ上には、配列として展開されてない (※1)
  • (※1) ResultSet に対して下記のような事を実行した際に、(fetch|fetchAll) し array|object(=Entityオブジェクト) としてメモリ上に展開される

    • foreach で回す
      • \Cake\ORM\ResultSetIterator interface を実装したクラスなので、foreach で回す事ができる (※2)
    • ->first()
    • ->toArray() (※3)
  • (※2) Iterator

    • Iterator::current: 現在の要素を取り出す
    • \Cake\ORM\ResultSet\Cake\ORM\ResultSet::currentIterator::current を実装している
      • Cake\ORM\ResultSet::current\Cake\ORM\ResultSet::$_current を返す
        • ここで PDOStatement::fetch して、メモリ上に Entity オブジェクトとして展開している

※3 ->toArray() する際の注意点

  • \Cake\Datasource\QueryTrait::toArray

    • PDOStatement::fetchAll して、全データをメモリ上にEntityオブジェクトの配列として 展開される
  • 問題点

    • データの件数が多い場合に、メモリオーバーになる事が可能性がある
    • ※paginator 等で件数が指定されてるケースで ->toArray() するのは問題ない
  • 対策

    • データ量が増えていくようなテーブルから、全件とってくるようなケース
      • ->toArray() せず、ResultSet のまま処理する(ResultRest を foreach で回して処理する)

PDO

https://dev-dub.hatenablog.com/entry/2022/02/05/120309 を参照

以下、コードを追った際のメモ

// in Controller

/** @var ResultSetInterface $resultSet */
$resultSet = $this->Hoges //Class Hoges extends \Cake\ORM\Table
    // \Cake\ORM\Table::find
    //   return \Cake\ORM\Query
    //     - Table オブジェクトの find() を使う事で Query オブジェクトを生成
    ->find()
    // \Cake\Database\Query::where
    //   return \Cake\Database\Query
    ->where(...)
    // \Cake\ORM\Query::contain
    //   return \Cake\ORM\Query
    ->contain(...)
    // \Cake\Database\Query::orderAsc
    //   return \Cake\ORM\Query
    ->orderAsc(...)
    // \Cake\ORM\Query::all
    //   return \Cake\ORM\ResultSet
    //     - \Cake\ORM\ResultSet は \Cake\Datasource\ResultSetInterface を実装したクラスで、\Iterator を実装している
    //     - \Iterator = PHP の Iterator インターフェース(https://www.php.net/manual/ja/class.iterator.php)
    //       - Iterator インターフェイスを実装したオブジェクトは foreach 文でループ処理を行う事ができる
    ->all();
// in Controller

$hoge = $this->Hoges //Class Hoges extends \Cake\ORM\Table
    ->find()
    ->all()
    // \Cake\ORM\ResultSet::first
    //   return array|object|null
    ->first();
// in app/vendor/cakephp/cakephp/src/ORM/ResultSet.php

/**
 * Get the first record from a result set.
 *
 * This method will also close the underlying statement cursor.
 *
 * @return array|object|null
 */
public function first()
{
    // \Cake\ORM\ResultSet をループで回す事で、DBから最初のレコードを取得し、array|object として返す
    foreach ($this as $result) {
        if ($this->_statement !== null && !$this->_useBuffering) {
            // \Cake\Database\StatementInterface::closeCursor
            $this->_statement->closeCursor();
        }

        // DBからfetchして、array|object で返す
        // array|object => メモリ上に展開
        return $result;
    }

    return null;
}
$resultArray = $this->Hoges
    ->find()
    // \Cake\Collection\CollectionTrait::toList
    //   return $this->toArray(false);
    ->toList();
$resultArray = $this->Hoges
    ->find()
    // Returns a key-value array with the results of this query.
    //
    // \Cake\Datasource\QueryTrait::toArray
    //   return $this->all()->toArray(); ※ `->all` してるので、\Cake\ORM\ResultSet の toArray()(=\Cake\Collection\CollectionTrait::toArray) を実行している
    //
    // \Cake\Collection\CollectionTrait::toArray
    //   return iterator_to_array($this, $preserveKeys);
    //     $this は \Cake\ORM\ResultSet なので、DBからレコードを取得して Array で返している
    ->toArray(); //\Cake\Datasource\QueryTrait::toArray