CakePHP4.x: hasMany の Association を contain すると 別SQL になる仕組み

version

仕組み

// 流れ
\Cake\Database\Driver::prepare
  \Cake\ORM\Query::sql
    \Cake\ORM\Query::_transformQuery
      \Cake\ORM\EagerLoader::attachAssociations
        \Cake\ORM\EagerLoader::attachableAssociations
          \Cake\ORM\EagerLoader::_resolveJoins

理解できていない事

  • hasMany の Association を contain した場合は、別SQLにする、という設計になっている理由 (設計思想)

作業ログ

以下のような Learning Test を書いて、デバッグした

    /**
     * @group leaning
     */
    public function testHasMany(): void
    {
      //..

      $query = $ArticlesTable->find();

      // Articles belongsTo Authors
      // Articles hasMany Comments
      $query->contain(['Authors', 'Comments']);
      $result_set = $query->all();

      // ...
    }

デバッグログ

// Leaning Test 内で \Cake\ORM\Query::all を実行
string(17) "get_class($query)"
string(14) "Cake\ORM\Query"
string(54) "↓ \Cake\ORM\Query::all $this->getRepository()::class"
  string(28) "App\Model\Table\ArticlesTable"
  string(69) "↓↓ \Cake\Datasource\QueryTrait::all $this->getRepository()::class"
    string(28) "App\Model\Table\ArticlesTable"
      string(65) "↓↓↓ \Cake\ORM\Query::_execute $this->getRepository()::class"
      string(28) "App\Model\Table\ArticlesTable"
      string(121) "!!!!!!!! \Cake\ORM\Query::_execute | $statement = $this->getEagerLoader()->loadExternal($this, $this->execute()) !!!!!!!!"
        string(72) "↓↓↓↓ \Cake\Database\Query::execute $this->getRepository()::class"
          string(28) "App\Model\Table\ArticlesTable"
          string(30) "\Cake\Database\Connection::run"
          string(34) "\Cake\Core\Retry\CommandRetry::run"
          string(43) "\Cake\Database\Connection::run の callback"
          string(34) "\Cake\Database\Connection::prepare"
          string(34) "\Cake\Core\Retry\CommandRetry::run"
          string(47) "\Cake\Database\Connection::prepare の callback"
          string(34) "\Cake\Database\Driver::prepare (1)"
          string(38) "\Cake\Database\Driver::prepare (2) sql"
          string(1134) "SELECT ... FROM Articles Articles INNER JOIN Authors Authors ON (Authors.id = (Articles.author_id))"
          string(60) "\Cake\Database\Driver::prepare (end) return new PDOStatement"
        string(121) "↑↑↑↑ \Cake\Database\Query::execute (end) return \Cake\Database\StatementInterface | $this->getRepository()::class"
        string(28) "App\Model\Table\ArticlesTable"
        string(83) "↓↓↓↓↓ \Cake\ORM\EagerLoader::loadExternal  $query->getRepository()::class"
          string(28) "App\Model\Table\ArticlesTable"
          string(81) "\Cake\ORM\EagerLoader::loadExternal (2) $external is not empty | count($external)"
          int(1)
          string(68) "\Cake\ORM\EagerLoader::loadExternal (3) foreach ($external as $meta)"
          string(62) "\Cake\ORM\EagerLoader::loadExternal (3-1) ※foreach内 $alias"
          string(7) "Articles"
          string(54) "↓ \Cake\ORM\Query::all $this->getRepository()::class"
            string(26) "App\Model\Table\CommentsTable"
            string(69) "↓↓ \Cake\Datasource\QueryTrait::all $this->getRepository()::class"
              string(26) "App\Model\Table\CommentsTable"
              string(65) "↓↓↓ \Cake\ORM\Query::_execute $this->getRepository()::class"
                string(26) "App\Model\Table\CommentsTable"
                string(121) "!!!!!!!! \Cake\ORM\Query::_execute | $statement = $this->getEagerLoader()->loadExternal($this, $this->execute()) !!!!!!!!"
                  string(72) "↓↓↓↓ \Cake\Database\Query::execute $this->getRepository()::class"
                    string(26) "App\Model\Table\CommentsTable"
                    string(30) "\Cake\Database\Connection::run"
                    string(34) "\Cake\Core\Retry\CommandRetry::run"
                    string(43) "\Cake\Database\Connection::run の callback"
                    string(34) "\Cake\Database\Connection::prepare"
                    string(34) "\Cake\Core\Retry\CommandRetry::run"
                    string(47) "\Cake\Database\Connection::prepare の callback"
                    string(34) "\Cake\Database\Driver::prepare (1)"
                    string(38) "\Cake\Database\Driver::prepare (2) sql"
                    string(399) "SELECT ... FROM Comments Comments WHERE Comments.article_id in (:c0)"
                    string(60) "\Cake\Database\Driver::prepare (end) return new PDOStatement"
                  string(121) "↑↑↑↑ \Cake\Database\Query::execute (end) return \Cake\Database\StatementInterface | $this->getRepository()::class"
                  string(26) "App\Model\Table\CommentsTable"
                  string(83) "↓↓↓↓↓ \Cake\ORM\EagerLoader::loadExternal  $query->getRepository()::class"
                    string(26) "App\Model\Table\CommentsTable"
                  string(78) "↑↑↑↑↑ \Cake\ORM\EagerLoader::loadExternal (1-end) $external is empty"
              string(113) "↑↑↑ \Cake\ORM\Query::_execute (end) return new ResultSet($this, $statement) | $this->getRepository()::class"
              string(26) "App\Model\Table\CommentsTable"
            string(120) "↑↑ \Cake\Datasource\QueryTrait::all (end) return \Cake\Datasource\ResultSetInterface | $this->getRepository()::class"
            string(26) "App\Model\Table\CommentsTable"
            string(106) "↑ \Cake\ORM\Query::all (end) return \Cake\Datasource\ResultSetInterface | $this->getRepository()::class "
          string(26) "App\Model\Table\CommentsTable"
          string(126) "\Cake\ORM\EagerLoader::loadExternal (3-end) ※foreach内 $statement = new CallbackStatement($statement, $driver, $f) | $alias"
          string(7) "Articles"
        string(98) "↑↑↑↑↑ \Cake\ORM\EagerLoader::loadExternal (end) return \Cake\Database\StatementInterface"
      string(113) "↑↑↑ \Cake\ORM\Query::_execute (end) return new ResultSet($this, $statement) | $this->getRepository()::class"
    string(28) "App\Model\Table\ArticlesTable"
  string(120) "↑↑ \Cake\Datasource\QueryTrait::all (end) return \Cake\Datasource\ResultSetInterface | $this->getRepository()::class"
  string(28) "App\Model\Table\ArticlesTable"
string(106) "↑ \Cake\ORM\Query::all (end) return \Cake\Datasource\ResultSetInterface | $this->getRepository()::class "
string(28) "App\Model\Table\ArticlesTable"

今後の学習テーマ

  • hasManyのTableをSearchプラグインで検索する際の問題点と対応

  • note

    • contain すると 別SQL になる為、beforeProcessでJOINする必要がある
    • 意図した検索結果を得るには ID で DISTINCTする必要がある
    • ID 以外で ORDER BY したい
      • DISTINCT で指定したカラムでしか ORDER BY できない