From c70f32f4c92310ca6e28ed989c668e7b34b9ded8 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 21 Sep 2010 00:32:07 +0200 Subject: [PATCH] DDC-809 - Fix nasty issue in ObjectHydrator yielding Many-To-Many hydration problems with multi-valued collections that are join-fetched. --- .../ORM/Internal/Hydration/ObjectHydrator.php | 11 +- .../ORM/Functional/Ticket/DDC809Test.php | 111 ++++++++++++++++ .../ORM/Hydration/ObjectHydratorTest.php | 120 ++++++++++++++++++ 3 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC809Test.php diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index a13119aea..ca937059b 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -269,6 +269,9 @@ class ObjectHydrator extends AbstractHydrator // It's a joined result $parentAlias = $this->_rsm->parentAliasMap[$dqlAlias]; + // we need the $path to save into the identifier map which entities were already + // seen for this parent-child relationship + $path = $parentAlias . '.' . $dqlAlias; // Get a reference to the parent object to which the joined element belongs. if ($this->_rsm->isMixed && isset($this->_rootAliases[$parentAlias])) { @@ -298,8 +301,8 @@ class ObjectHydrator extends AbstractHydrator $reflFieldValue = $this->_initRelatedCollection($parentObject, $parentClass, $relationField); } - $indexExists = isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]]); - $index = $indexExists ? $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] : false; + $indexExists = isset($this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]); + $index = $indexExists ? $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false; $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false; if ( ! $indexExists || ! $indexIsValid) { @@ -317,11 +320,11 @@ class ObjectHydrator extends AbstractHydrator $field = $this->_rsm->indexByMap[$dqlAlias]; $indexValue = $this->_ce[$entityName]->reflFields[$field]->getValue($element); $reflFieldValue->hydrateSet($indexValue, $element); - $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $indexValue; + $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue; } else { $reflFieldValue->hydrateAdd($element); $reflFieldValue->last(); - $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $reflFieldValue->key(); + $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key(); } // Update result pointer $this->_resultPointers[$dqlAlias] = $element; diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC809Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC809Test.php new file mode 100644 index 000000000..c0dcba90e --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC809Test.php @@ -0,0 +1,111 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC809Variant'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC809SpecificationValue') + )); + + $conn = $this->_em->getConnection(); + $conn->insert('specification_value_test', array('specification_value_id' => 94589)); + $conn->insert('specification_value_test', array('specification_value_id' => 94593)); + $conn->insert('specification_value_test', array('specification_value_id' => 94606)); + $conn->insert('specification_value_test', array('specification_value_id' => 94607)); + $conn->insert('specification_value_test', array('specification_value_id' => 94609)); + $conn->insert('specification_value_test', array('specification_value_id' => 94711)); + + $conn->insert('variant_test', array('variant_id' => 545208)); + $conn->insert('variant_test', array('variant_id' => 545209)); + + $conn->insert('variant_specification_value_test', array('variant_id' => 545208, 'specification_value_id' => 94606)); + $conn->insert('variant_specification_value_test', array('variant_id' => 545208, 'specification_value_id' => 94607)); + $conn->insert('variant_specification_value_test', array('variant_id' => 545208, 'specification_value_id' => 94609)); + $conn->insert('variant_specification_value_test', array('variant_id' => 545208, 'specification_value_id' => 94711)); + + $conn->insert('variant_specification_value_test', array('variant_id' => 545209, 'specification_value_id' => 94589)); + $conn->insert('variant_specification_value_test', array('variant_id' => 545209, 'specification_value_id' => 94593)); + $conn->insert('variant_specification_value_test', array('variant_id' => 545209, 'specification_value_id' => 94606)); + $conn->insert('variant_specification_value_test', array('variant_id' => 545209, 'specification_value_id' => 94607)); + } + + /** + * @group DDC-809 + */ + public function testIssue() + { + $result = $this->_em->createQueryBuilder() + ->select('Variant, SpecificationValue') + ->from('Doctrine\Tests\ORM\Functional\Ticket\DDC809Variant', 'Variant') + ->leftJoin('Variant.SpecificationValues', 'SpecificationValue') + ->getQuery() + ->getResult(); + + $this->assertEquals(4, count($result[0]->getSpecificationValues()), "Works in test-setup."); + $this->assertEquals(4, count($result[1]->getSpecificationValues()), "Only returns 2 in the case of the hydration bug."); + } +} + +/** + * @Table(name="variant_test") + * @Entity + */ +class DDC809Variant +{ + /** + * @Column(name="variant_id", type="integer") + * @Id + * @GeneratedValue(strategy="AUTO") + */ + protected $variantId; + + /** + * @ManyToMany(targetEntity="DDC809SpecificationValue", inversedBy="Variants") + * @JoinTable(name="variant_specification_value_test", + * joinColumns={ + * @JoinColumn(name="variant_id", referencedColumnName="variant_id") + * }, + * inverseJoinColumns={ + * @JoinColumn(name="specification_value_id", referencedColumnName="specification_value_id") + * } + * ) + */ + protected $SpecificationValues; + + public function getSpecificationValues() + { + return $this->SpecificationValues; + } +} + +/** + * @Table(name="specification_value_test") + * @Entity + */ +class DDC809SpecificationValue +{ + /** + * @Column(name="specification_value_id", type="integer") + * @Id + * @GeneratedValue(strategy="AUTO") + */ + protected $specificationValueId; + + /** + * @var Variant + * + * @ManyToMany(targetEntity="DDC809Variant", mappedBy="SpecificationValues") + */ + protected $Variants; +} diff --git a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php index 442961266..ecd50f237 100644 --- a/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Hydration/ObjectHydratorTest.php @@ -888,4 +888,124 @@ class ObjectHydratorTest extends HydrationTestCase ++$rowNum; } } + + /** + * This issue tests if, with multiple joined multiple-valued collections the hydration is done correctly. + * + * User x Phonenumbers x Groups blow up the resultset quite a bit, however the hydration should correctly assemble those. + * + * @group DDC-809 + */ + public function testManyToManyHydration() + { + $rsm = new ResultSetMapping; + $rsm->addEntityResult('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addFieldResult('u', 'u__id', 'id'); + $rsm->addFieldResult('u', 'u__name', 'name'); + $rsm->addJoinedEntityResult('Doctrine\Tests\Models\CMS\CmsGroup', 'g', 'u', 'groups'); + $rsm->addFieldResult('g', 'g__id', 'id'); + $rsm->addFieldResult('g', 'g__name', 'name'); + $rsm->addJoinedEntityResult('Doctrine\Tests\Models\CMS\CmsPhonenumber', 'p', 'u', 'phonenumbers'); + $rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber'); + + // Faked result set + $resultSet = array( + array( + 'u__id' => '1', + 'u__name' => 'romanb', + 'g__id' => '3', + 'g__name' => 'TestGroupB', + 'p__phonenumber' => 1111, + ), + array( + 'u__id' => '1', + 'u__name' => 'romanb', + 'g__id' => '5', + 'g__name' => 'TestGroupD', + 'p__phonenumber' => 1111, + ), + array( + 'u__id' => '1', + 'u__name' => 'romanb', + 'g__id' => '3', + 'g__name' => 'TestGroupB', + 'p__phonenumber' => 2222, + ), + array( + 'u__id' => '1', + 'u__name' => 'romanb', + 'g__id' => '5', + 'g__name' => 'TestGroupD', + 'p__phonenumber' => 2222, + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage', + 'g__id' => '2', + 'g__name' => 'TestGroupA', + 'p__phonenumber' => 3333, + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage', + 'g__id' => '3', + 'g__name' => 'TestGroupB', + 'p__phonenumber' => 3333, + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage', + 'g__id' => '4', + 'g__name' => 'TestGroupC', + 'p__phonenumber' => 3333, + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage', + 'g__id' => '5', + 'g__name' => 'TestGroupD', + 'p__phonenumber' => 3333, + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage', + 'g__id' => '2', + 'g__name' => 'TestGroupA', + 'p__phonenumber' => 4444, + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage', + 'g__id' => '3', + 'g__name' => 'TestGroupB', + 'p__phonenumber' => 4444, + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage', + 'g__id' => '4', + 'g__name' => 'TestGroupC', + 'p__phonenumber' => 4444, + ), + array( + 'u__id' => '2', + 'u__name' => 'jwage', + 'g__id' => '5', + 'g__name' => 'TestGroupD', + 'p__phonenumber' => 4444, + ), + ); + + $stmt = new HydratorMockStatement($resultSet); + $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); + + $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); + + $this->assertEquals(2, count($result)); + $this->assertContainsOnly('Doctrine\Tests\Models\CMS\CmsUser', $result); + $this->assertEquals(2, count($result[0]->groups)); + $this->assertEquals(2, count($result[0]->phonenumbers)); + $this->assertEquals(4, count($result[1]->groups)); + $this->assertEquals(2, count($result[1]->phonenumbers)); + } }