Skip to content

Commit a3d17fa

Browse files
authored
add support for ordered one to many associations (#7)
1 parent fab0f2d commit a3d17fa

File tree

4 files changed

+41
-13
lines changed

4 files changed

+41
-13
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
],
77
"require": {
88
"php": "^8.1",
9-
"doctrine/orm": "^3"
9+
"doctrine/orm": "^3.2"
1010
},
1111
"require-dev": {
1212
"doctrine/collections": "^2.2",

src/EntityPreloader.php

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
use Doctrine\ORM\EntityManagerInterface;
66
use Doctrine\ORM\Mapping\ClassMetadata;
7+
use Doctrine\ORM\Mapping\ManyToManyAssociationMapping;
8+
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
9+
use Doctrine\ORM\Mapping\ToManyAssociationMapping;
710
use Doctrine\ORM\PersistentCollection;
811
use Doctrine\ORM\QueryBuilder;
912
use LogicException;
@@ -59,10 +62,6 @@ public function preload(
5962
throw new LogicException('Preloading of indexed associations is not supported');
6063
}
6164

62-
if ($associationMapping->isOrdered()) {
63-
throw new LogicException('Preloading of ordered associations is not supported');
64-
}
65-
6665
$maxFetchJoinSameFieldCount ??= 1;
6766
$sourceEntities = $this->loadProxies($sourceClassMetadata, $sourceEntities, $batchSize ?? self::PRELOAD_ENTITY_DEFAULT_BATCH_SIZE, $maxFetchJoinSameFieldCount);
6867

@@ -200,14 +199,21 @@ private function preloadToMany(
200199
}
201200
}
202201

203-
$innerLoader = match ($sourceClassMetadata->getAssociationMapping($sourcePropertyName)->type()) {
204-
ClassMetadata::ONE_TO_MANY => $this->preloadOneToManyInner(...),
205-
ClassMetadata::MANY_TO_MANY => $this->preloadManyToManyInner(...),
202+
$associationMapping = $sourceClassMetadata->getAssociationMapping($sourcePropertyName);
203+
204+
if (!$associationMapping instanceof ToManyAssociationMapping) {
205+
throw new LogicException('Unsupported association mapping type');
206+
}
207+
208+
$innerLoader = match (true) {
209+
$associationMapping instanceof OneToManyAssociationMapping => $this->preloadOneToManyInner(...),
210+
$associationMapping instanceof ManyToManyAssociationMapping => $this->preloadManyToManyInner(...),
206211
default => throw new LogicException('Unsupported association mapping type'),
207212
};
208213

209214
foreach (array_chunk($uninitializedSourceEntityIds, $batchSize, preserve_keys: true) as $uninitializedSourceEntityIdsChunk) {
210215
$targetEntitiesChunk = $innerLoader(
216+
associationMapping: $associationMapping,
211217
sourceClassMetadata: $sourceClassMetadata,
212218
sourceIdentifierReflection: $sourceIdentifierReflection,
213219
sourcePropertyName: $sourcePropertyName,
@@ -242,6 +248,7 @@ private function preloadToMany(
242248
* @template T of E
243249
*/
244250
private function preloadOneToManyInner(
251+
ToManyAssociationMapping $associationMapping,
245252
ClassMetadata $sourceClassMetadata,
246253
ReflectionProperty $sourceIdentifierReflection,
247254
string $sourcePropertyName,
@@ -260,7 +267,15 @@ private function preloadOneToManyInner(
260267
throw new LogicException('Doctrine should use RuntimeReflectionService which never returns null.');
261268
}
262269

263-
foreach ($this->loadEntitiesBy($targetClassMetadata, $targetPropertyName, $uninitializedSourceEntityIdsChunk, $maxFetchJoinSameFieldCount) as $targetEntity) {
270+
$targetEntitiesList = $this->loadEntitiesBy(
271+
$targetClassMetadata,
272+
$targetPropertyName,
273+
$uninitializedSourceEntityIdsChunk,
274+
$maxFetchJoinSameFieldCount,
275+
$associationMapping->orderBy(),
276+
);
277+
278+
foreach ($targetEntitiesList as $targetEntity) {
264279
$sourceEntity = $targetPropertyReflection->getValue($targetEntity);
265280
$sourceEntityKey = (string) $sourceIdentifierReflection->getValue($sourceEntity);
266281
$uninitializedCollections[$sourceEntityKey]->add($targetEntity);
@@ -283,6 +298,7 @@ private function preloadOneToManyInner(
283298
* @template T of E
284299
*/
285300
private function preloadManyToManyInner(
301+
ToManyAssociationMapping $associationMapping,
286302
ClassMetadata $sourceClassMetadata,
287303
ReflectionProperty $sourceIdentifierReflection,
288304
string $sourcePropertyName,
@@ -293,6 +309,10 @@ private function preloadManyToManyInner(
293309
int $maxFetchJoinSameFieldCount,
294310
): array
295311
{
312+
if (count($associationMapping->orderBy()) > 0) {
313+
throw new LogicException('Many-to-many associations with order by are not supported');
314+
}
315+
296316
$sourceIdentifierName = $sourceClassMetadata->getSingleIdentifierFieldName();
297317
$targetIdentifierName = $targetClassMetadata->getSingleIdentifierFieldName();
298318

@@ -382,6 +402,7 @@ private function preloadToOne(
382402
* @param ClassMetadata<T> $targetClassMetadata
383403
* @param list<mixed> $fieldValues
384404
* @param non-negative-int $maxFetchJoinSameFieldCount
405+
* @param array<string, 'asc'|'desc'> $orderBy
385406
* @return list<T>
386407
* @template T of E
387408
*/
@@ -390,6 +411,7 @@ private function loadEntitiesBy(
390411
string $fieldName,
391412
array $fieldValues,
392413
int $maxFetchJoinSameFieldCount,
414+
array $orderBy = [],
393415
): array
394416
{
395417
if (count($fieldValues) === 0) {
@@ -406,6 +428,10 @@ private function loadEntitiesBy(
406428

407429
$this->addFetchJoinsToPreventFetchDuringHydration($rootLevelAlias, $queryBuilder, $targetClassMetadata, $maxFetchJoinSameFieldCount);
408430

431+
foreach ($orderBy as $field => $direction) {
432+
$queryBuilder->addOrderBy("{$rootLevelAlias}.{$field}", $direction);
433+
}
434+
409435
return $queryBuilder->getQuery()->getResult();
410436
}
411437

tests/EntityPreloadBlogOneHasManyAbstractTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function testOneHasManyAbstractUnoptimized(): void
2020

2121
self::assertAggregatedQueries([
2222
['count' => 1, 'query' => 'SELECT * FROM article t0'],
23-
['count' => 5, 'query' => 'SELECT * FROM comment t0 WHERE t0.article_id = ?'],
23+
['count' => 5, 'query' => 'SELECT * FROM comment t0 WHERE t0.article_id = ? ORDER BY t0.id DESC'],
2424
['count' => 25, 'query' => 'SELECT * FROM contributor t0 WHERE t0.id = ? AND t0.dtype IN (?)'],
2525
]);
2626
}
@@ -40,7 +40,7 @@ public function testOneHasManyAbstractWithFetchJoin(): void
4040
$this->readComments($articles);
4141

4242
self::assertAggregatedQueries([
43-
['count' => 1, 'query' => 'SELECT * FROM article a0_ LEFT JOIN comment c1_ ON a0_.id = c1_.article_id LEFT JOIN contributor c2_ ON c1_.author_id = c2_.id AND c2_.dtype IN (?)'],
43+
['count' => 1, 'query' => 'SELECT * FROM article a0_ LEFT JOIN comment c1_ ON a0_.id = c1_.article_id LEFT JOIN contributor c2_ ON c1_.author_id = c2_.id AND c2_.dtype IN (?) ORDER BY c1_.id DESC'],
4444
]);
4545
}
4646

@@ -60,7 +60,7 @@ public function testOneHasManyAbstractWithEagerFetchMode(): void
6060

6161
self::assertAggregatedQueries([
6262
['count' => 1, 'query' => 'SELECT * FROM article a0_'],
63-
['count' => 1, 'query' => 'SELECT * FROM comment t0 WHERE t0.article_id IN (?, ?, ?, ?, ?)'],
63+
['count' => 1, 'query' => 'SELECT * FROM comment t0 WHERE t0.article_id IN (?, ?, ?, ?, ?) ORDER BY t0.id DESC'],
6464
['count' => 25, 'query' => 'SELECT * FROM contributor t0 WHERE t0.id = ? AND t0.dtype IN (?)'],
6565
]);
6666
}
@@ -76,7 +76,7 @@ public function testOneHasManyAbstractWithPreload(): void
7676

7777
self::assertAggregatedQueries([
7878
['count' => 1, 'query' => 'SELECT * FROM article t0'],
79-
['count' => 1, 'query' => 'SELECT * FROM comment c0_ LEFT JOIN contributor c1_ ON c0_.author_id = c1_.id AND c1_.dtype IN (?) WHERE c0_.article_id IN (?, ?, ?, ?, ?)'],
79+
['count' => 1, 'query' => 'SELECT * FROM comment c0_ LEFT JOIN contributor c1_ ON c0_.author_id = c1_.id AND c1_.dtype IN (?) WHERE c0_.article_id IN (?, ?, ?, ?, ?) ORDER BY c0_.id DESC'],
8080
]);
8181
}
8282

tests/Fixtures/Blog/Article.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Doctrine\ORM\Mapping\ManyToMany;
1313
use Doctrine\ORM\Mapping\ManyToOne;
1414
use Doctrine\ORM\Mapping\OneToMany;
15+
use Doctrine\ORM\Mapping\OrderBy;
1516

1617
#[Entity]
1718
class Article
@@ -41,6 +42,7 @@ class Article
4142
* @var Collection<int, Comment>
4243
*/
4344
#[OneToMany(targetEntity: Comment::class, mappedBy: 'article')]
45+
#[OrderBy(['id' => 'DESC'])]
4446
private Collection $comments;
4547

4648
public function __construct(string $title, string $content, ?Category $category = null)

0 commit comments

Comments
 (0)