|
| 1 | +.. include:: /Includes.rst.txt |
| 2 | + |
| 3 | +.. _feature-104631-1723714985: |
| 4 | + |
| 5 | +================================================================= |
| 6 | +Feature: #104631 - Add `UNION Clause` support to the QueryBuilder |
| 7 | +================================================================= |
| 8 | + |
| 9 | +See :issue:`104631` |
| 10 | + |
| 11 | +Description |
| 12 | +=========== |
| 13 | + |
| 14 | +The :sql:`UNION` clause is used to combine the result-set of two or more |
| 15 | +:sql:`SELECT` statements, which all database vendors supports with usual |
| 16 | +specialities for each. |
| 17 | + |
| 18 | +Still, there is a common shared subset which works for all of them: |
| 19 | + |
| 20 | +.. code-block:: sql |
| 21 | +
|
| 22 | + SELECT column_name(s) FROM table1 |
| 23 | + WHERE ... |
| 24 | +
|
| 25 | + UNION <ALL | DISTINCT> |
| 26 | +
|
| 27 | + SELECT column_name(s) FROM table2 |
| 28 | + WHERE ... |
| 29 | +
|
| 30 | + ORDER BY ... |
| 31 | + LIMIT x OFFSET y |
| 32 | +
|
| 33 | +with shared requirements: |
| 34 | + |
| 35 | +* Each SELECT must return the same fields in number, naming and order. |
| 36 | +* Each SELECT must not have ORDER BY, expect MySQL allowing it to be used as sub |
| 37 | + query expression encapsulated in parenthesis. |
| 38 | + |
| 39 | +Generic :sql:`UNION` clause support has been contributed to `Doctrine DBAL` and |
| 40 | +is included since `Release 4.1.0 <https://github.com/doctrine/dbal/releases/tag/4.1.0>`__ |
| 41 | +which introduces two new API method on the QueryBuilder: |
| 42 | + |
| 43 | +* :php:`union(string|QueryBuilder $part)` to create first UNION query part |
| 44 | +* :php:`addUnion(string|QueryBuilder $part, UnionType $type = UnionType::DISTINCT)` |
| 45 | + to add addtional :sql:`UNION (ALL|DISTINCT)` query parts with the selected union |
| 46 | + query type. |
| 47 | + |
| 48 | +TYPO3 decorates the Doctrine DBAL QueryBuilder to provide for most API methods automatic |
| 49 | +quoting of identifiers and values **and** to appliy database restrictions automatically |
| 50 | +for :sql:`SELECT` queries. |
| 51 | + |
| 52 | +The Doctrine DBAL API has been adopted now to provide the same surface for the |
| 53 | +TYPO3 :php:`\TYPO3\CMS\Core\Database\Query\QueryBuilder` and the intermediate |
| 54 | +:php:`\TYPO3\CMS\Core\Database\Query\ConcreteQueryBuilder` to make it easier to |
| 55 | +create :sql:`UNION` clause queries. The API on both methods allows to provide |
| 56 | +dedicated QueryBuilder instances or direct queries as strings in case it is needed. |
| 57 | + |
| 58 | +.. note:: |
| 59 | + |
| 60 | + Providing :sql:`UNION` parts as plain string requires the developer to take |
| 61 | + care of proper quoting and escaping within the query part. |
| 62 | + |
| 63 | +Another point worth to mention is, that only `named placeholder` can be used |
| 64 | +and registered on the most outer :php:`QueryBuilder` object instance, similar |
| 65 | +to advanced query creation using for example :sql:`SUB QUERIES`. |
| 66 | + |
| 67 | +.. warning:: |
| 68 | + |
| 69 | + :php:`QueryBuilder` can be used create :sql:`UNION` clause queries not |
| 70 | + compatible with all database, for example using LIMIT/OFFSET in each |
| 71 | + part query or other stuff. |
| 72 | + |
| 73 | +UnionType::DISTINCT and UnionType::ALL |
| 74 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 75 | + |
| 76 | +Each subsequent part needs to be defined either as :sql:`UNION DISTINCT` or |
| 77 | +:sql:`UNION ALL` which could have not so obvious effects. |
| 78 | + |
| 79 | +For example, using :sql:`UNION ALL` for all parts in between except for the last |
| 80 | +one would generate larger result sets first, but discards duplicates when adding |
| 81 | +the last result set. On the other side, using :sql:`UNION ALL` tells the query |
| 82 | +optimizer **not** to scan for duplicats and remove them at all which can be a |
| 83 | +performance improvement - if you can deal with duplicates it can be ensured that |
| 84 | +each part does not produce same outputs. |
| 85 | + |
| 86 | +Example: Compose a :sql:`UNION` clause query |
| 87 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 88 | + |
| 89 | +.. code-block:: php |
| 90 | + :caption: Custom service class using an UNION query to retrieve data. |
| 91 | +
|
| 92 | + use TYPO3\CMS\Core\Database\Connection; |
| 93 | + use TYPO3\CMS\Core\Database\ConnectionPool; |
| 94 | + use TYPO3\CMS\Core\Database\Query\QueryBuilder; |
| 95 | +
|
| 96 | + final readonly MyService { |
| 97 | + public function __construct( |
| 98 | + private ConnectionPool $connectionPool, |
| 99 | + ) {} |
| 100 | +
|
| 101 | + public function executeUnionQuery( |
| 102 | + int $pageIdOne, |
| 103 | + int $pageIdTwo, |
| 104 | + ): ?array { |
| 105 | + $connection = $this->connectionPool->getConnectionForTable('pages'); |
| 106 | + $unionQueryBuilder = $connection->createQueryBuilder(); |
| 107 | + $firstPartQueryBuilder = $connection->createQueryBuilder(); |
| 108 | + $firstPartQueryBuilder->getRestrictions()->removeAll(); |
| 109 | + $secondPartQueryBuilder = $connection->createQueryBuilder(); |
| 110 | + $secondPartQueryBuilder->getRestrictions()->removeAll(); |
| 111 | + $expr = $unionQueryBuilder->expr(); |
| 112 | +
|
| 113 | + $firstPartQueryBuilder |
| 114 | + ->select('uid', 'pid', 'title') |
| 115 | + ->from('pages') |
| 116 | + ->where( |
| 117 | + $expr->eq( |
| 118 | + 'pages.uid', |
| 119 | + $unionQueryBuilder->createNamedParameter($pageIdOne), |
| 120 | + ); |
| 121 | + $secondPartQueryBuilder |
| 122 | + ->select('uid', 'pid', 'title') |
| 123 | + ->from('pages') |
| 124 | + ->where( |
| 125 | + $expr->eq( |
| 126 | + 'pages.uid', |
| 127 | + $unionQueryBuilder->createNamedParameter($pageIdOne), |
| 128 | + ); |
| 129 | +
|
| 130 | + return $unionQueryBuilder |
| 131 | + ->union($firstPartQueryBuilder) |
| 132 | + ->addUnion($secondPartQueryBuilder, UnionType::DISTINCT) |
| 133 | + ->orderBy('uid', 'ASC') |
| 134 | + ->executeQuery() |
| 135 | + ->fetchAllAssociative(); |
| 136 | + } |
| 137 | + } |
| 138 | +
|
| 139 | +which would create following query for MySQL with :php:`$pageIdOne = 100` and |
| 140 | +:php:`$pageIdTwo = 10`: |
| 141 | + |
| 142 | +.. code-block:: sql |
| 143 | +
|
| 144 | + (SELECT `uid`, `pid`, `title` FROM pages WHERE `pages`.`uid` = 100) |
| 145 | + UNION |
| 146 | + (SELECT `uid`, `pid`, `title` FROM pages WHERE `pages`.`uid` = 10) |
| 147 | + ORDER BY `uid` ASC |
| 148 | +
|
| 149 | +
|
| 150 | +Impact |
| 151 | +====== |
| 152 | + |
| 153 | +Extension authors can use the new :php:`QueryBuilder` methods to build more |
| 154 | +advanced queries. |
| 155 | + |
| 156 | +.. index:: Database, PHP-API, ext:core |
0 commit comments