-
-
Notifications
You must be signed in to change notification settings - Fork 895
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
Fix Doctrine query for nested subresources #1608
Changes from 5 commits
620637b
8c705b7
c2cbed3
3ef7c9e
5b23e18
f71432d
1a6aa73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ | |
use Doctrine\Common\Persistence\ManagerRegistry; | ||
use Doctrine\ORM\EntityManagerInterface; | ||
use Doctrine\ORM\Mapping\ClassMetadataInfo; | ||
use Doctrine\ORM\QueryBuilder; | ||
|
||
/** | ||
* Subresource data provider for the Doctrine ORM. | ||
|
@@ -79,82 +80,10 @@ public function getSubresource(string $resourceClass, array $identifiers, array | |
throw new ResourceClassNotSupportedException('The given resource class is not a subresource.'); | ||
} | ||
|
||
$originAlias = 'o'; | ||
$queryBuilder = $repository->createQueryBuilder($originAlias); | ||
$queryNameGenerator = new QueryNameGenerator(); | ||
$previousQueryBuilder = null; | ||
$previousAlias = null; | ||
|
||
$num = \count($context['identifiers']); | ||
|
||
while ($num--) { | ||
list($identifier, $identifierResourceClass) = $context['identifiers'][$num]; | ||
$previousAssociationProperty = $context['identifiers'][$num + 1][0] ?? $context['property']; | ||
|
||
$manager = $this->managerRegistry->getManagerForClass($identifierResourceClass); | ||
|
||
if (!$manager instanceof EntityManagerInterface) { | ||
throw new RuntimeException("The manager for $identifierResourceClass must be an EntityManager."); | ||
} | ||
|
||
$classMetadata = $manager->getClassMetadata($identifierResourceClass); | ||
|
||
if (!$classMetadata instanceof ClassMetadataInfo) { | ||
throw new RuntimeException("The class metadata for $identifierResourceClass must be an instance of ClassMetadataInfo."); | ||
} | ||
|
||
$qb = $manager->createQueryBuilder(); | ||
$alias = $queryNameGenerator->generateJoinAlias($identifier); | ||
$relationType = $classMetadata->getAssociationMapping($previousAssociationProperty)['type']; | ||
$normalizedIdentifiers = isset($identifiers[$identifier]) ? $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass) : []; | ||
|
||
switch ($relationType) { | ||
//MANY_TO_MANY relations need an explicit join so that the identifier part can be retrieved | ||
case ClassMetadataInfo::MANY_TO_MANY: | ||
$joinAlias = $queryNameGenerator->generateJoinAlias($previousAssociationProperty); | ||
|
||
$qb->select($joinAlias) | ||
->from($identifierResourceClass, $alias) | ||
->innerJoin("$alias.$previousAssociationProperty", $joinAlias); | ||
|
||
break; | ||
case ClassMetadataInfo::ONE_TO_MANY: | ||
$mappedBy = $classMetadata->getAssociationMapping($previousAssociationProperty)['mappedBy']; | ||
|
||
// first pass, o.property instead of alias.property | ||
if (null === $previousQueryBuilder) { | ||
$originAlias = "$originAlias.$mappedBy"; | ||
} else { | ||
$previousAlias = "$previousAlias.$mappedBy"; | ||
} | ||
|
||
$qb->select($alias) | ||
->from($identifierResourceClass, $alias); | ||
break; | ||
default: | ||
$qb->select("IDENTITY($alias.$previousAssociationProperty)") | ||
->from($identifierResourceClass, $alias); | ||
} | ||
|
||
// Add where clause for identifiers | ||
foreach ($normalizedIdentifiers as $key => $value) { | ||
$placeholder = $queryNameGenerator->generateParameterName($key); | ||
$qb->andWhere("$alias.$key = :$placeholder"); | ||
$queryBuilder->setParameter($placeholder, $value); | ||
} | ||
|
||
// recurse queries | ||
if (null === $previousQueryBuilder) { | ||
$previousQueryBuilder = $qb; | ||
} else { | ||
$previousQueryBuilder->andWhere($qb->expr()->in($previousAlias, $qb->getDQL())); | ||
} | ||
|
||
$previousAlias = $alias; | ||
} | ||
|
||
/* | ||
* The following translate to this pseudo-dql: | ||
* The following recursively translates to this pseudo-dql: | ||
* | ||
* SELECT thirdLevel WHERE thirdLevel IN ( | ||
* SELECT thirdLevel FROM relatedDummies WHERE relatedDummies = ? AND relatedDummies IN ( | ||
|
@@ -164,9 +93,7 @@ public function getSubresource(string $resourceClass, array $identifiers, array | |
* | ||
* By using subqueries, we're forcing the SQL execution plan to go through indexes on doctrine identifiers. | ||
*/ | ||
$queryBuilder->where( | ||
$queryBuilder->expr()->in($originAlias, $previousQueryBuilder->getDQL()) | ||
); | ||
$queryBuilder = $this->buildQuery($identifiers, $context, $queryNameGenerator, $repository->createQueryBuilder($alias = 'o'), $alias, \count($context['identifiers'])); | ||
|
||
if (true === $context['collection']) { | ||
foreach ($this->collectionExtensions as $extension) { | ||
|
@@ -195,4 +122,77 @@ public function getSubresource(string $resourceClass, array $identifiers, array | |
|
||
return $context['collection'] ? $query->getResult() : $query->getOneOrNullResult(); | ||
} | ||
|
||
/** | ||
* @throws RuntimeException | ||
*/ | ||
private function buildQuery(array $identifiers, array $context, QueryNameGenerator $queryNameGenerator, QueryBuilder $previousQueryBuilder, string $previousAlias, int $remainingIdentifiers, QueryBuilder $topQueryBuilder = null): QueryBuilder | ||
{ | ||
if (!$remainingIdentifiers) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's clearer this way. Because if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok then maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you prefer. Then not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or even There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dunno it's fine like this as well. |
||
return $previousQueryBuilder; | ||
} | ||
|
||
$topQueryBuilder = $topQueryBuilder ?? $previousQueryBuilder; | ||
|
||
list($identifier, $identifierResourceClass) = $context['identifiers'][$remainingIdentifiers - 1]; | ||
$previousAssociationProperty = $context['identifiers'][$remainingIdentifiers][0] ?? $context['property']; | ||
|
||
$manager = $this->managerRegistry->getManagerForClass($identifierResourceClass); | ||
|
||
if (!$manager instanceof EntityManagerInterface) { | ||
throw new RuntimeException("The manager for $identifierResourceClass must be an EntityManager."); | ||
} | ||
|
||
$classMetadata = $manager->getClassMetadata($identifierResourceClass); | ||
|
||
if (!$classMetadata instanceof ClassMetadataInfo) { | ||
throw new RuntimeException( | ||
"The class metadata for $identifierResourceClass must be an instance of ClassMetadataInfo." | ||
); | ||
} | ||
|
||
$qb = $manager->createQueryBuilder(); | ||
$alias = $queryNameGenerator->generateJoinAlias($identifier); | ||
$relationType = $classMetadata->getAssociationMapping($previousAssociationProperty)['type']; | ||
$normalizedIdentifiers = isset($identifiers[$identifier]) ? $this->normalizeIdentifiers( | ||
$identifiers[$identifier], | ||
$manager, | ||
$identifierResourceClass | ||
) : []; | ||
|
||
switch ($relationType) { | ||
// MANY_TO_MANY relations need an explicit join so that the identifier part can be retrieved | ||
case ClassMetadataInfo::MANY_TO_MANY: | ||
$joinAlias = $queryNameGenerator->generateJoinAlias($previousAssociationProperty); | ||
|
||
$qb->select($joinAlias) | ||
->from($identifierResourceClass, $alias) | ||
->innerJoin("$alias.$previousAssociationProperty", $joinAlias); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for cosmetics, new line is useless here imo \o/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure :) |
||
break; | ||
case ClassMetadataInfo::ONE_TO_MANY: | ||
$mappedBy = $classMetadata->getAssociationMapping($previousAssociationProperty)['mappedBy']; | ||
|
||
$previousAlias = "$previousAlias.$mappedBy"; | ||
|
||
$qb->select($alias) | ||
->from($identifierResourceClass, $alias); | ||
break; | ||
default: | ||
$qb->select("IDENTITY($alias.$previousAssociationProperty)") | ||
->from($identifierResourceClass, $alias); | ||
} | ||
|
||
// Add where clause for identifiers | ||
foreach ($normalizedIdentifiers as $key => $value) { | ||
$placeholder = $queryNameGenerator->generateParameterName($key); | ||
$qb->andWhere("$alias.$key = :$placeholder"); | ||
$topQueryBuilder->setParameter($placeholder, $value); | ||
} | ||
|
||
// Recurse queries | ||
$qb = $this->buildQuery($identifiers, $context, $queryNameGenerator, $qb, $alias, --$remainingIdentifiers, $topQueryBuilder); | ||
|
||
return $previousQueryBuilder->andWhere($qb->expr()->in($previousAlias, $qb->getDQL())); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is recursive already no? Not sure what was going on wrong here 😸
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this was not recursive (despite the comment!).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should've been ? 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup! :D