Feature: #104631 - Add UNION Clause
support to the QueryBuilder
See forge#104631
Description
The UNION
clause is used to combine the result sets of two or more
SELECT
statements, which all database vendors support, each with their
own specific variations.
However, there is a commonly shared subset that works across all of them:
SELECT column_name(s) FROM table1
WHERE ...
UNION <ALL | DISTINCT>
SELECT column_name(s) FROM table2
WHERE ...
ORDER BY ...
LIMIT x OFFSET y
with shared requirements:
- Each SELECT must return the same fields in number, naming and order.
- Each SELECT must not have ORDER BY, expect MySQL allowing it to be used as sub query expression encapsulated in parentheses.
Generic UNION
clause support has been contributed to Doctrine DBAL
and
is included since Release 4.1.0
which introduces two new API method on the
\Query
:
union
to create first UNION query part(string |Query Builder $part) add
to add additionalUnion (string |Query Builder $part, Union Type $type = Union Type:: DISTINCT) UNION
query parts with the selected union query type.(ALL |DISTINCT)
TYPO3 decorates the Doctrine DBAL \Query
to provide for most API methods automatic
quoting of identifiers and values and to apply database restrictions automatically
for SELECT
queries.
The Doctrine DBAL API has been adopted now to provide the same surface for the
TYPO3 \TYPO3\
and the intermediate
\TYPO3\
to make it easier to
create UNION
clause queries. The API on both methods allows to provide
dedicated Query
instances
or direct queries as strings in case it is needed.
Note
Providing UNION
parts as plain string requires the developer to take
care of proper quoting and escaping within the query part.
In queries containing subqueries, only named placeholders (such as :username
)
can be used and must be registered on the outermost
Query
object,
similar to advanced query creation with SUB QUERIES
.
Warning
Query
can be used create
UNION
clause queries not compatible with all database providers,
for example using LIMIT/
in each part query or other stuff.
UnionType::DISTINCT and UnionType::ALL
Each subsequent part needs to be defined either as UNION DISTINCT
or
UNION ALL
which could have not so obvious effects.
For example, using UNION ALL
for all parts in between except for the last
one would generate larger result sets first, but discards duplicates when adding
the last result set. On the other side, using UNION ALL
tells the query
optimizer not to scan for duplicates and remove them at all which can be a
performance improvement - if you can deal with duplicates it can be ensured that
each part does not produce same outputs.
Example: Compose a UNION
clause query
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
final readonly MyService {
public function __construct(
private ConnectionPool $connectionPool,
) {}
public function executeUnionQuery(
int $pageIdOne,
int $pageIdTwo,
): ?array {
$connection = $this->connectionPool->getConnectionForTable('pages');
$unionQueryBuilder = $connection->createQueryBuilder();
$firstPartQueryBuilder = $connection->createQueryBuilder();
$firstPartQueryBuilder->getRestrictions()->removeAll();
$secondPartQueryBuilder = $connection->createQueryBuilder();
$secondPartQueryBuilder->getRestrictions()->removeAll();
$expr = $unionQueryBuilder->expr();
$firstPartQueryBuilder
->select('uid', 'pid', 'title')
->from('pages')
->where(
$expr->eq(
'pages.uid',
$unionQueryBuilder->createNamedParameter($pageIdOne),
);
$secondPartQueryBuilder
->select('uid', 'pid', 'title')
->from('pages')
->where(
$expr->eq(
'pages.uid',
$unionQueryBuilder->createNamedParameter($pageIdOne),
);
return $unionQueryBuilder
->union($firstPartQueryBuilder)
->addUnion($secondPartQueryBuilder, UnionType::DISTINCT)
->orderBy('uid', 'ASC')
->executeQuery()
->fetchAllAssociative();
}
}
This would create the following query for MySQL with $page
and
$page
:
(SELECT `uid`, `pid`, `title` FROM pages WHERE `pages`.`uid` = 100)
UNION
(SELECT `uid`, `pid`, `title` FROM pages WHERE `pages`.`uid` = 10)
ORDER BY `uid` ASC
Impact
Extension authors can use the new
Query
methods to build more
advanced queries.