phpcakephppaginationcontains

Can an aliased query use a contain clause?


I use a union to join two datasets and then the following query to setup for pagination correctly

$paginationQuery = $this->find('all')
    ->contain(['EmailAddresses' => [
        'foreignKey' => false,
        'queryBuilder' => function($q) {
                  return $q->where(['Members__id' => 'EmailAddresses.member_id']);
                 }
        ]])
        ->select( $selectMainUnion )
        ->from([$this->getAlias() => $query])
        ->order(['Members__last_name' => 'ASC', 'Members__first_name' => 'ASC']);

I have also tried

$paginationQuery = $this->find('all')
        ->contain(['EmailAddresses'])
        ->select( $selectMainUnion )
        ->from([$this->getAlias() => $query])
        ->order(['Members__last_name' => 'ASC', 'Members__first_name' => 'ASC']);

and tried $query->loadInto($query, ['EmailAddresses']); where $query is the result of the union.

Neither of these result in email addresses added to $paginationQuery.

Is there a way to do this?

Adding to clarify the code

$selectMain =['Members.id',
    'Members.member_type',
    'Members.first_name',
    'Members.middle_name',
    'Members.last_name',
    'Members.suffix',
    'Members.date_joined'];

foreach($selectMain as $select) {
    $selectMainUnion[] = str_replace('.', '__', $select);
}

$this->hasMany('EmailAddresses', [
   'foreignKey' => 'member_id',
   'dependent' => true,
]);

Looking at the SQL in DebugKit SQL Log, there is no reference to the EmailAddresses table.


Solution

  • Generally containments do work fine irrespective of the queries FROM clause, whether that's a table or a subquery should be irrelevant. The requirement for this to work however is that the required primary and/or foreign key fields are being selected, and that they are in the correct format.

    By default CakePHP's ORM queries automatically alias selected fields, ie they are being selected like Alias.field AS Alias__field. So when Alias is a subquery, then Alias.field doesn't exist, you'd have to select Alias.Alias__field instead. So with the automatic aliases, your select of Members__id would be transformed to Members.Members__id AS Members__Members__id, and Members__Members__id is not something the ORM understands, it would end up as Members__id in your entities, where the eager loader would expect id instead, ie the name of the primary key which is used to inject the results of the queried hasMany associated records (this happens in a separate query), your custom queryBuilder won't help with that, as the injecting happens afterwards on PHP level.

    Long story short, to fix the problem, you can either change how the fields of the union queries are selected, ie ensure that they are not selected with aliases, that way the pagination query fields do not need to be changed at all:

    $fields = $table->getSchema()->columns();
    $fields = array_combine($fields, $fields);
    
    $query->select($fields);
    

    This will create a list of fields in the format of ['id' => 'id', ...], looks a bit whacky, but it works (as long as there's no ambiguity because of joined tables for example), the SQL would be like id AS id, so your pagination query can then simply reference the fields like Members.id.

    Another way would be to select the aliases of the subquery, ie not just select Member__id, which the ORM turns into Member__Member__id when it applies automatic aliasing, but use Members.Member__id, like:

    [
        'Member__id' => 'Members.Member__id',
        // ...
    ]
    

    That way no automatic aliasing takes place, on SQL level it would select the field like Members.Member__id AS Member__id, and the field would end up as id in your entities, which the eager loader would find and could use for injecting the associated records.