Skip to content

Commit 706a848

Browse files
committed
fix(doctrine): fix partial fetch with same entity included multiple time with different fields
1 parent 0e899fa commit 706a848

File tree

3 files changed

+88
-22
lines changed

3 files changed

+88
-22
lines changed

features/doctrine/eager_loading.feature

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Feature: Eager Loading
1111
Then the response status code should be 200
1212
And the DQL should be equal to:
1313
"""
14-
SELECT o, thirdLevel_a1, fourthLevel_a2, relatedToDummyFriend_a3, dummyFriend_a4
14+
SELECT o, thirdLevel_a1, relatedToDummyFriend_a3, fourthLevel_a2, dummyFriend_a4
1515
FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
1616
LEFT JOIN o.thirdLevel thirdLevel_a1
1717
LEFT JOIN thirdLevel_a1.fourthLevel fourthLevel_a2
@@ -46,7 +46,7 @@ Feature: Eager Loading
4646
Then the response status code should be 200
4747
And the DQL should be equal to:
4848
"""
49-
SELECT o, thirdLevel_a4, fourthLevel_a5, relatedToDummyFriend_a1, dummyFriend_a6
49+
SELECT o, thirdLevel_a4, relatedToDummyFriend_a1, fourthLevel_a5, dummyFriend_a6
5050
FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
5151
INNER JOIN o.relatedToDummyFriend relatedToDummyFriend_a1
5252
LEFT JOIN o.thirdLevel thirdLevel_a4
@@ -83,7 +83,7 @@ Feature: Eager Loading
8383
Then the response status code should be 200
8484
And the DQL should be equal to:
8585
"""
86-
SELECT o, thirdLevel_a3, fourthLevel_a4, relatedToDummyFriend_a5, dummyFriend_a6
86+
SELECT o, thirdLevel_a3, relatedToDummyFriend_a5, fourthLevel_a4, dummyFriend_a6
8787
FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
8888
LEFT JOIN o.thirdLevel thirdLevel_a3
8989
LEFT JOIN thirdLevel_a3.fourthLevel fourthLevel_a4

src/Doctrine/Orm/Extension/EagerLoadingExtension.php

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,61 @@ private function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $
9494
$options['denormalization_groups'] = $denormalizationGroups;
9595
}
9696

97-
$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $fetchPartial, $queryBuilder->getRootAliases()[0], $options, $context);
97+
$selects = $this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $fetchPartial, $queryBuilder->getRootAliases()[0], $options, $context);
98+
$selectsByClass = [];
99+
foreach ($selects as [$entity, $alias, $fields]) {
100+
if ($entity === $resourceClass) {
101+
// We don't perform partial select the root entity
102+
$fields = null;
103+
}
104+
105+
if (!isset($selectsByClass[$entity])) {
106+
$selectsByClass[$entity] = [
107+
'aliases' => [$alias => true],
108+
'fields' => null === $fields ? null : array_flip($fields),
109+
];
110+
} else {
111+
$selectsByClass[$entity]['aliases'][$alias] = true;
112+
if (null === $selectsByClass[$entity]['fields']) {
113+
continue;
114+
}
115+
116+
if (null === $fields) {
117+
$selectsByClass[$entity]['fields'] = null;
118+
continue;
119+
}
120+
121+
// Merge fields
122+
foreach ($fields as $field) {
123+
$selectsByClass[$entity]['fields'][$field] = true;
124+
}
125+
}
126+
}
127+
128+
$existingSelects = [];
129+
foreach ($queryBuilder->getDQLPart('select') ?? [] as $dqlSelect) {
130+
if (!$dqlSelect instanceof Select) {
131+
continue;
132+
}
133+
foreach ($dqlSelect->getParts() as $part) {
134+
$existingSelects[(string) $part] = true;
135+
}
136+
}
137+
138+
foreach ($selectsByClass as $data) {
139+
$fields = $data['fields'] === null ? null : array_keys($data['fields']);
140+
foreach (array_keys($data['aliases']) as $alias) {
141+
if (isset($existingSelects[$alias])) {
142+
continue;
143+
}
144+
145+
if (null === $fields) {
146+
$queryBuilder->addSelect($alias);
147+
} else {
148+
$queryBuilder->addSelect(\sprintf('partial %s.{%s}', $alias, implode(',', $fields)));
149+
}
150+
}
151+
}
98152
}
99153

100154
/**
@@ -106,7 +160,7 @@ private function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $
106160
*
107161
* @throws RuntimeException when the max number of joins has been reached
108162
*/
109-
private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, bool $fetchPartial, string $parentAlias, array $options = [], array $normalizationContext = [], bool $wasLeftJoin = false, int &$joinCount = 0, ?int $currentDepth = null, ?string $parentAssociation = null): void
163+
private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, bool $fetchPartial, string $parentAlias, array $options = [], array $normalizationContext = [], bool $wasLeftJoin = false, int &$joinCount = 0, ?int $currentDepth = null, ?string $parentAssociation = null): iterable
110164
{
111165
if ($joinCount > $this->maxJoins) {
112166
throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary with the "api_platform.eager_loading.max_joins" configuration key (https://api-platform.com/docs/core/performance/#eager-loading), or limit the maximum serialization depth using the "enable_max_depth" option of the Symfony serializer (https://symfony.com/doc/current/components/serializer.html#handling-serialization-depth).');
@@ -198,12 +252,12 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
198252

199253
if (true === $fetchPartial) {
200254
try {
201-
$this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $options);
255+
yield from $this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $options);
202256
} catch (ResourceClassNotFoundException) {
203257
continue;
204258
}
205259
} else {
206-
$this->addSelectOnce($queryBuilder, $associationAlias);
260+
yield [$resourceClass, $associationAlias, null];
207261
}
208262

209263
// Avoid recursive joins for self-referencing relations
@@ -225,17 +279,17 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
225279
}
226280
}
227281

228-
$this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $options, $childNormalizationContext, $isLeftJoin, $joinCount, $currentDepth, $association);
282+
yield from $this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $options, $childNormalizationContext, $isLeftJoin, $joinCount, $currentDepth, $association);
229283
}
230284
}
231285

232-
private function addSelect(QueryBuilder $queryBuilder, string $entity, string $associationAlias, array $propertyMetadataOptions): void
286+
private function addSelect(QueryBuilder $queryBuilder, string $entity, string $associationAlias, array $propertyMetadataOptions): iterable
233287
{
234288
$select = [];
235289
$entityManager = $queryBuilder->getEntityManager();
236290
$targetClassMetadata = $entityManager->getClassMetadata($entity);
237291
if (!empty($targetClassMetadata->subClasses)) {
238-
$this->addSelectOnce($queryBuilder, $associationAlias);
292+
yield [$entity, $associationAlias, null];
239293

240294
return;
241295
}
@@ -270,15 +324,6 @@ private function addSelect(QueryBuilder $queryBuilder, string $entity, string $a
270324
}
271325
}
272326

273-
$queryBuilder->addSelect(\sprintf('partial %s.{%s}', $associationAlias, implode(',', $select)));
274-
}
275-
276-
private function addSelectOnce(QueryBuilder $queryBuilder, string $alias): void
277-
{
278-
$existingSelects = array_reduce($queryBuilder->getDQLPart('select') ?? [], fn ($existing, $dqlSelect) => ($dqlSelect instanceof Select) ? array_merge($existing, $dqlSelect->getParts()) : $existing, []);
279-
280-
if (!\in_array($alias, $existingSelects, true)) {
281-
$queryBuilder->addSelect($alias);
282-
}
327+
yield [$entity, $associationAlias, $select];
283328
}
284329
}

0 commit comments

Comments
 (0)