Skip to content

Commit

Permalink
add support for ordered one to many associations (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
JanTvrdik authored Oct 9, 2024
1 parent fab0f2d commit a3d17fa
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 13 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
],
"require": {
"php": "^8.1",
"doctrine/orm": "^3"
"doctrine/orm": "^3.2"
},
"require-dev": {
"doctrine/collections": "^2.2",
Expand Down
42 changes: 34 additions & 8 deletions src/EntityPreloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ManyToManyAssociationMapping;
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
use Doctrine\ORM\Mapping\ToManyAssociationMapping;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\QueryBuilder;
use LogicException;
Expand Down Expand Up @@ -59,10 +62,6 @@ public function preload(
throw new LogicException('Preloading of indexed associations is not supported');
}

if ($associationMapping->isOrdered()) {
throw new LogicException('Preloading of ordered associations is not supported');
}

$maxFetchJoinSameFieldCount ??= 1;
$sourceEntities = $this->loadProxies($sourceClassMetadata, $sourceEntities, $batchSize ?? self::PRELOAD_ENTITY_DEFAULT_BATCH_SIZE, $maxFetchJoinSameFieldCount);

Expand Down Expand Up @@ -200,14 +199,21 @@ private function preloadToMany(
}
}

$innerLoader = match ($sourceClassMetadata->getAssociationMapping($sourcePropertyName)->type()) {
ClassMetadata::ONE_TO_MANY => $this->preloadOneToManyInner(...),
ClassMetadata::MANY_TO_MANY => $this->preloadManyToManyInner(...),
$associationMapping = $sourceClassMetadata->getAssociationMapping($sourcePropertyName);

if (!$associationMapping instanceof ToManyAssociationMapping) {
throw new LogicException('Unsupported association mapping type');
}

$innerLoader = match (true) {
$associationMapping instanceof OneToManyAssociationMapping => $this->preloadOneToManyInner(...),
$associationMapping instanceof ManyToManyAssociationMapping => $this->preloadManyToManyInner(...),
default => throw new LogicException('Unsupported association mapping type'),
};

foreach (array_chunk($uninitializedSourceEntityIds, $batchSize, preserve_keys: true) as $uninitializedSourceEntityIdsChunk) {
$targetEntitiesChunk = $innerLoader(
associationMapping: $associationMapping,
sourceClassMetadata: $sourceClassMetadata,
sourceIdentifierReflection: $sourceIdentifierReflection,
sourcePropertyName: $sourcePropertyName,
Expand Down Expand Up @@ -242,6 +248,7 @@ private function preloadToMany(
* @template T of E
*/
private function preloadOneToManyInner(
ToManyAssociationMapping $associationMapping,
ClassMetadata $sourceClassMetadata,
ReflectionProperty $sourceIdentifierReflection,
string $sourcePropertyName,
Expand All @@ -260,7 +267,15 @@ private function preloadOneToManyInner(
throw new LogicException('Doctrine should use RuntimeReflectionService which never returns null.');
}

foreach ($this->loadEntitiesBy($targetClassMetadata, $targetPropertyName, $uninitializedSourceEntityIdsChunk, $maxFetchJoinSameFieldCount) as $targetEntity) {
$targetEntitiesList = $this->loadEntitiesBy(
$targetClassMetadata,
$targetPropertyName,
$uninitializedSourceEntityIdsChunk,
$maxFetchJoinSameFieldCount,
$associationMapping->orderBy(),
);

foreach ($targetEntitiesList as $targetEntity) {
$sourceEntity = $targetPropertyReflection->getValue($targetEntity);
$sourceEntityKey = (string) $sourceIdentifierReflection->getValue($sourceEntity);
$uninitializedCollections[$sourceEntityKey]->add($targetEntity);
Expand All @@ -283,6 +298,7 @@ private function preloadOneToManyInner(
* @template T of E
*/
private function preloadManyToManyInner(
ToManyAssociationMapping $associationMapping,
ClassMetadata $sourceClassMetadata,
ReflectionProperty $sourceIdentifierReflection,
string $sourcePropertyName,
Expand All @@ -293,6 +309,10 @@ private function preloadManyToManyInner(
int $maxFetchJoinSameFieldCount,
): array
{
if (count($associationMapping->orderBy()) > 0) {
throw new LogicException('Many-to-many associations with order by are not supported');
}

$sourceIdentifierName = $sourceClassMetadata->getSingleIdentifierFieldName();
$targetIdentifierName = $targetClassMetadata->getSingleIdentifierFieldName();

Expand Down Expand Up @@ -382,6 +402,7 @@ private function preloadToOne(
* @param ClassMetadata<T> $targetClassMetadata
* @param list<mixed> $fieldValues
* @param non-negative-int $maxFetchJoinSameFieldCount
* @param array<string, 'asc'|'desc'> $orderBy
* @return list<T>
* @template T of E
*/
Expand All @@ -390,6 +411,7 @@ private function loadEntitiesBy(
string $fieldName,
array $fieldValues,
int $maxFetchJoinSameFieldCount,
array $orderBy = [],
): array
{
if (count($fieldValues) === 0) {
Expand All @@ -406,6 +428,10 @@ private function loadEntitiesBy(

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

foreach ($orderBy as $field => $direction) {
$queryBuilder->addOrderBy("{$rootLevelAlias}.{$field}", $direction);
}

return $queryBuilder->getQuery()->getResult();
}

Expand Down
8 changes: 4 additions & 4 deletions tests/EntityPreloadBlogOneHasManyAbstractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function testOneHasManyAbstractUnoptimized(): void

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

self::assertAggregatedQueries([
['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 (?)'],
['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'],
]);
}

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

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

self::assertAggregatedQueries([
['count' => 1, 'query' => 'SELECT * FROM article t0'],
['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 (?, ?, ?, ?, ?)'],
['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'],
]);
}

Expand Down
2 changes: 2 additions & 0 deletions tests/Fixtures/Blog/Article.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\OrderBy;

#[Entity]
class Article
Expand Down Expand Up @@ -41,6 +42,7 @@ class Article
* @var Collection<int, Comment>
*/
#[OneToMany(targetEntity: Comment::class, mappedBy: 'article')]
#[OrderBy(['id' => 'DESC'])]
private Collection $comments;

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

0 comments on commit a3d17fa

Please sign in to comment.