Skip to content

Make generated QueryComponents publicly available in ParserResult #11870

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 3.3.x
Choose a base branch
from

Conversation

Jalliuz
Copy link

@Jalliuz Jalliuz commented Mar 14, 2025

The Parser turns A DQL query in a ParserResult. This parser generates all the QueryComponents, which hold all the mappings calculated from the DQL query. But in the end these calculated components are never consultable. This is actually very useful information but you can't access it anywhere. So that really is a pity!

A scenario where access to this generated data becomes very useful is when you have a QueryBuilder with subQueries which are injected with DQL.
Let's say you have a dynamic query builder that goes through a lot of code and might add subqueries along the way.

// $subqueryBuilder holds potential ToMany joins

if ($forSomeReason) {
    $queryBuilder->andWhere($qb->expr()->in('n.user', $subqueryBuilder->getDQL()))
}

In the end just before executing the query I want to check my QueryBuilder if it has at least one ...toMany relation before I add

  • ->distinct() (if no aggregate selects found)
  • OR ->addGroupBy('rootalias.id') (depending if the select has aggregates or not)

I only want to add distinct or groupBy if it is really necessary because it is a performance hit. There is no need to add it if we don't have toMany relations in the final query.

Because we have DQL injected in our query we lose the original "$subqueryBuilder" join mapping information in the final $queryBuilder

So By adding the generated QueryComponents to the ParserResult all the oh-so-valuable mapping information with all the join types becomes available.

To solve this problem in my current project I made this solution with a ReflectionClass to access the private generated "queryComponents"

$parser = new Parser($qb->getQuery());
$parser->parse();
$reflectionParser = new \ReflectionClass($parser);
$queryComponentsProp = $reflectionParser->getProperty('queryComponents');
$queryComponentsProp->setAccessible(true);
$queryComponents = $queryComponentsProp->getValue($parser);

But it would be nice that we all could just do $parserResult->getQueryComponents(), and then we can check if there is at least one ...toMany relation with

$parser = new Parser($qb->getQuery());
$parserResult = $parser->parse();

$hasToManyJoins = false;
foreach ($parserResult->getQueryComponents() as $queryComponent) {
    $relationType = $queryComponent['relation']['type'] ?? null;
            
    if (in_array($relationType, [
        ClassMetadataInfo::ONE_TO_MANY,
        ClassMetadataInfo::MANY_TO_MANY,
    ])) {
        $hasToManyJoins = true;
        break;
    }
}

@beberlei
Copy link
Member

The query components are an intenal data structure and exposing it in this rough state as an array with no typing would be problematic

@tvlooy
Copy link

tvlooy commented Mar 18, 2025

@beberlei how would you suggest the given usecase should be handled? The workaround is to use reflection to get the info anyways, but that solution is even worse.

@Jalliuz
Copy link
Author

Jalliuz commented Mar 19, 2025

@beberlei

In my case I just want to know if there is at least one ...toMany Relation because I encountered this "iterateWithFetchJoinNotAllowed" exception in SQLWalker and I wanted to solve it

if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && (! $this->query->getHint(self::HINT_DISTINCT) || isset($this->selectedClasses[$joinedDqlAlias]))) {

if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && (! $this->query->getHint(self::HINT_DISTINCT) || isset($this->selectedClasses[$joinedDqlAlias]))) {
    if ($relation->isToMany()) {
        throw QueryException::iterateWithFetchJoinNotAllowed($assoc);
    }
}

So in the end we do need to set it or it fails when iterating. The information is actually present but we can't access it.

Some ideas:

  • Narrowing it totally down to a method if ($query->iterationNeedsDistinct()) {} or if ($query->hasJoinsOnCollection()) {} and do the logic internally
  • Exposing (a selection of) the internal QueryComponents objects to more solid objects instead of an array. So lost mapping information becomes available again if we turn a subQuery DQL string back into usable information.
  • Allowing to use Query or QueryBuilder objects as sub queries instead of transforming it to DQL when injecting $qb->andWhere($qb->expr()->in('user.id', $subQb)). So In the end we never lost the subquery mapping information.

And another use case is when we want to paginate

/** @param bool $fetchJoinCollection Whether the query joins a collection (true by default). */

public function __construct(
    Query|QueryBuilder $query,
    private readonly bool $fetchJoinCollection = true,
) {
    if ($query instanceof QueryBuilder) {
        $query = $query->getQuery();
    }

    $this->query = $query;
}

Here we also want to know if we need to use true or false on the "fetchJoinCollection" parameter.
I had a query which was 10 times faster when leaving distinct or groupBy out (and I did not need it anyway)

This could also solve this Pull Request #11595 "Make Paginator use simpler queries when there are no sql joins used"

@mpdude
Copy link
Contributor

mpdude commented Mar 30, 2025

I agree that this is internal stuff that we should not expose.

However, I've several times been at a point where I would have liked to obtain similar information as the OP. IIRC, one was about optimizing the ORM Paginator, which would require to know a bit more about the structure of the query and the entities being selected.

Not sure if it helps, but my impression that it would be useful if I could use a TreeWalker to traverse the parse result. The challenge with that is there is no good base/helper class for such a walker that I could extend in order to get a full, recursive tree traversal basically for free, with the opportunity to hook into very specific places where the information I need is available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants