2

I have a vanilla MySQL query as follows which works fine:

SELECT d.*, IFNULL(
     (SELECT GROUP_CONCAT(value) FROM display_substances `ds` 
         WHERE `ds`.`display_id` = `d`.`id`
         AND ds.substance_id = 2 
         GROUP BY `ds`.`display_id`
     ), 'Not Listed'
) `substances` FROM displays `d`;

The background to this is that I have 2 tables, displays and display_substances. I want to render a table of every row in displays and the corresponding values in display_substances for a given substance ID (represented by ds.substance_id = 2 in the query above).

Not every row in displays has a corresponding display_substances value. If this is the case it should output the words "Not Listed". This effectively means that if there is no corresponding row in display_substances - then there is no data for that display.id - therefore the data is "Not Listed" in the database for that particular row. The query above does exactly this.

I want to be able to write my query using CakePHP's ORM syntax.

The structures for the tables are as follows.

mysql> DESCRIBE displays;
+----------+----------------------+------+-----+---------+----------------+
| Field    | Type                 | Null | Key | Default | Extra          |
+----------+----------------------+------+-----+---------+----------------+
| id       | smallint(5) unsigned | NO   | PRI | NULL    | auto_increment |
| label    | varchar(255)         | NO   |     | NULL    |                |
+----------+----------------------+------+-----+---------+----------------+

mysql> DESCRIBE display_substances;
+--------------+-----------------------+------+-----+---------+----------------+
| Field        | Type                  | Null | Key | Default | Extra          |
+--------------+-----------------------+------+-----+---------+----------------+
| id           | mediumint(8) unsigned | NO   | PRI | NULL    | auto_increment |
| display_id   | smallint(5) unsigned  | NO   | MUL | NULL    |                |
| substance_id | mediumint(8) unsigned | NO   | MUL | NULL    |                |
| value        | text                  | NO   |     | NULL    |                |
+--------------+-----------------------+------+-----+---------+----------------+

The Table classes exist with the appropriate relationships defined.

// src/Model/Table/DisplaysTable.php
$this->hasMany('DisplaySubstances', [
    'foreignKey' => 'display_id'
]);

// src/Model/Table/DisplaysSubstancesTable.php
$this->belongsTo('Displays', [
   'foreignKey' => 'display_id',
   'joinType' => 'INNER'
]);

So far I have this:

    $substance_id = 2;

    $data = 
        $DisplaySubstances->find()
        ->contain(['Displays'])
        ->where(['DisplaySubstances.substance_id' => $substance_id)
        ->select(['Displays.id', 'Displays.label', 'Displays.anchor', 'DisplaySubstances.value'])
        ->enableHydration(false)->toArray();

This will get me the rows in displays which have corresponding values in display_substances for substance ID 2. This effectively is everything where we have data, but doesn't include any rows which are "Not Listed".

I don't know how to write the IFNULL...GROUP_CONCAT part of my vanilla SQL query using Cake's ORM syntax.

I've read How to use group_contact in cakephp query? so can see that it's possible to use GROUP_CONCAT in the ->select() condition. But the linked example is a lot more simple because it doesn't use an IFNULL condition and the corresponding action (returning "Not Listed").

Please can someone advise how to write this in Cake's ORM syntax, if this is possible?

CakePHP version is 3.5.18

Andy
  • 5,142
  • 11
  • 58
  • 131

1 Answers1

2

IFNULL and GROUP_CONCAT are both SQL functions, so you'd use the functions builder, which can be used to create any function call you want, its magic call handler will create a generic function call if there's no concrete implementation for the invoked method, ie $functionsBuilder->IFNULL() and $functionsBuilder->GROUP_CONCAT() will just work.

The functions builder also accepts expressions, so you can pass another query object to your IFNULL() call, ie the subquery in your example SQL.

$subquery = $DisplaySubstances->find()
    ->select(function (\Cake\ORM\Query $query) {
        return [
            $query->func()->GROUP_CONCAT(['value' => 'identifier'])
        ];
    })
    ->where(function (\Cake\Database\Expression\QueryExpression $exp) use ($substance_id) {
        return [
            $exp->equalFields('DisplaySubstances.display_id', 'Displays.id'),
            'DisplaySubstances.substance_id' => $substance_id
        ];
    })
    ->group('DisplaySubstances.display_id');

$query = $Displays->find()
    ->select(function (\Cake\ORM\Query $query) use ($subquery) {
        // Before CakePHP 3.8.3 queries need to be wrapped in an additional expression
        // in order for the query builder to generate them wrapped in parentheses in SQL
        $subquery = $query->newExpr($subquery);

        return [
            'substances' => $query->func()->IFNULL([$subquery, 'Not Listed'])
        ];
    })
    ->select($Displays);

See also

ndm
  • 59,784
  • 9
  • 71
  • 110
  • I've only just had chance to try this. It gives an SQL error `Error: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'SELECT (GROUP_CONCAT(value)) FROM display_substances DisplaySubstances WHERE (Di' at line 1`. I can't spot where this error is as it looks like valid SQL. – Andy Apr 24 '20 at 11:14
  • Whole query is `SELECT (IFNULL(SELECT (GROUP_CONCAT(value)) FROM display_substances DisplaySubstances WHERE (DisplaySubstances.display_id = (Displays.id) AND DisplaySubstances.substance_id = :c0) GROUP BY DisplaySubstances.display_id , :param1)) AS `substances`, Displays.id AS `Displays__id`, Displays.name AS `Displays__name`, Displays.label AS `Displays__label`, Displays.link AS `Displays__link`, Displays.anchor AS `Displays__anchor`, Displays.group_id AS `Displays__group_id`, Displays.comment AS `Displays__comment` FROM displays Displays` – Andy Apr 24 '20 at 11:14
  • 1
    @Andy Please try to host such code snippets externally (gist for example), they're very unreadable in a comment. That being said, the subquery should be wrapped in parentheses ie `IFNULL((SELECT ...), ...)` (note the double parentheses, one for the function call, one as a wrapper). CakePHP should do that by default, maybe you're using a too old version, or you've modified the example code in a way that it would change the output (though I'm not sure how that would be possible). – ndm Apr 24 '20 at 11:39
  • 1
    @Andy Ah look, it was me who fixed it https://github.com/cakephp/cakephp/pull/13537 :P As a workaround wrapping the query in another expression should work in older versions: `$query->newExpr($subquery)` – ndm Apr 24 '20 at 12:02
  • Thanks very much @ndm. I can confirm this works on CakePHP 3.5.13 – Andy Apr 24 '20 at 12:53