Skip to content

Commit

Permalink
ObjectHydrator: defer initialization of potentially empty collections
Browse files Browse the repository at this point in the history
If ObjectHydrator faces an empty row to an uninitialized collection,
it initializes it, to prevent it from querying again (DDC-1526).
However, if that row is the first but not the only in the collection,
the next rows will be ignored, as the collection will be considered
"existing", and "existing" collections are only replaced if REFRESH hint
is present. To prevent it, we defer initialization to the end of the
hydration.

Fixes GH-9807
  • Loading branch information
popov-a-e committed Jul 1, 2022
1 parent 07ee555 commit 36f436c
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 2 deletions.
13 changes: 11 additions & 2 deletions lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class ObjectHydrator extends AbstractHydrator
/** @var mixed[] */
private $initializedCollections = [];

/** @var array<string, PersistentCollection> */
private $uninitializedCollections = [];

/** @var mixed[] */
private $existingCollections = [];

Expand Down Expand Up @@ -148,6 +151,12 @@ protected function hydrateAllData()
$coll->takeSnapshot();
}

foreach ($this->uninitializedCollections as $coll) {
if (! $coll->isInitialized()) {
$coll->setInitialized(true);
}
}

return $result;
}

Expand Down Expand Up @@ -411,8 +420,8 @@ protected function hydrateRowData(array $row, array &$result)
}
} elseif (! $reflFieldValue) {
$this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias);
} elseif ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === false) {
$reflFieldValue->setInitialized(true);
} elseif ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === false && ! isset($this->uninitializedCollections[$oid . $relationField])) {
$this->uninitializedCollections[$oid . $relationField] = $reflFieldValue;
}
} else {
// PATH B: Single-valued association
Expand Down
129 changes: 129 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/GH9807Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\OrmFunctionalTestCase;

final class GH9807Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$this->createSchemaForModels(GH9807Main::class, GH9807Join::class);
}

public function testHydrateJoinedCollectionWithFirstNullishRow(): void
{
$rsm = new ResultSetMapping();
$rsm->addEntityResult(GH9807Main::class, 'm');
$rsm->addJoinedEntityResult(GH9807Join::class, 'j', 'm', 'joins');

$rsm->addFieldResult('m', 'id_0', 'id');
$rsm->addFieldResult('j', 'id_1', 'id');
$rsm->addFieldResult('j', 'value_2', 'value');

$hydrator = new ObjectHydrator($this->_em);

$uow = $this->_em->getUnitOfWork();

$uow->createEntity(
GH9807Main::class,
['id' => 1]
);

$resultSet = [
[
'id_0' => 1,
'id_1' => null,
'value_2' => null,
],
[
'id_0' => 1,
'id_1' => 1,
'value_2' => '2',
],
[
'id_0' => 1,
'id_1' => 2,
'value_2' => '2',
],
];

$stmt = ArrayResultFactory::createFromArray($resultSet);

/** @var GH9807Main[] $result */
$result = $hydrator->hydrateAll($stmt, $rsm);

self::assertInstanceOf(GH9807Main::class, $result[0]);
self::assertCount(2, $result[0]->getJoins());
}
}

/**
* @Entity
*/
class GH9807Main
{
/**
* @var int
* @Column(type="integer")
* @Id
* @GeneratedValue
*/
private $id;

/**
* @ORM\ManyToMany(targetEntity="GH9807Join", inversedBy="starts")
*
* @var Collection<int, GH9807Join>
*/
private $joins;

/**
* @return Collection<int, GH9807Join>
*/
public function getJoins(): Collection
{
return $this->joins;
}
}

/**
* @Entity
*/
class GH9807Join
{
/**
* @var int
* @Column(type="integer")
* @Id
* @GeneratedValue
*/
private $id;

/**
* @ORM\ManyToMany(targetEntity="GH9807Main", mappedBy="bases")
*
* @var Collection<int, GH9807Main>
*/
private $mains;

/**
* @ORM\Column(type="string", nullable=false)
*
* @var string
*/
private $value;
}

0 comments on commit 36f436c

Please sign in to comment.