From 557686aa0acd0666fd2b0f23b3ceb56ecce01b66 Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Thu, 6 Feb 2014 12:27:12 +0100 Subject: [PATCH] Entra-lazy for containsKey on collections --- lib/Doctrine/ORM/PersistentCollection.php | 9 +- .../ORM/Persisters/ManyToManyPersister.php | 80 ++++++++++++- .../ORM/Persisters/OneToManyPersister.php | 69 ++++++++---- .../Functional/ExtraLazyCollectionTest.php | 106 +++++++++++++++++- 4 files changed, 240 insertions(+), 24 deletions(-) diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 8d0fef757..e2d46408d 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -471,6 +471,13 @@ final class PersistentCollection implements Collection, Selectable */ public function containsKey($key) { + + if (! $this->initialized && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY + && isset($this->association['indexBy'])) { + $persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association); + + return $this->coll->containsKey($key) || $persister->containsKey($this, $key); + } $this->initialize(); return $this->coll->containsKey($key); @@ -778,7 +785,7 @@ final class PersistentCollection implements Collection, Selectable public function next() { $this->initialize(); - + return $this->coll->next(); } diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index 14664d3ce..2ecd90cf1 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -265,7 +265,16 @@ class ManyToManyPersister extends AbstractCollectionPersister return $this->em->getUnitOfWork()->getEntityPersister($mapping['targetEntity'])->getManyToManyCollection($mapping, $coll->getOwner(), $offset, $length); } - + /** + * {@inheritdoc} + */ + public function containsKey(PersistentCollection $coll, $key) + { + list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictionsWithKey($coll, $key, true); + $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); + return (bool) $this->conn->fetchColumn($sql, $params); + } + /** * {@inheritdoc} */ @@ -319,6 +328,75 @@ class ManyToManyPersister extends AbstractCollectionPersister return (bool) $this->conn->executeUpdate($sql, $params); } + /** + * @param \Doctrine\ORM\PersistentCollection $coll + * @param string $key + * @param boolean $addFilters Whether the filter SQL should be included or not. + * + * @return array + */ + private function getJoinTableRestrictionsWithKey(PersistentCollection $coll, $key, $addFilters) + { + $uow = $this->em->getUnitOfWork(); + $filterMapping = $coll->getMapping(); + $mapping = $filterMapping; + $indexBy = $mapping['indexBy']; + $wasOwning = $mapping['isOwningSide']; + $id = $uow->getEntityIdentifier($coll->getOwner()); + + $targetEntity = $this->em->getClassMetadata($mapping['targetEntity']); + + if (! $mapping['isOwningSide']) { + $associationSourceClass = $this->em->getClassMetadata($mapping['targetEntity']); + $mapping = $associationSourceClass->associationMappings[$mapping['mappedBy']]; + } else { + $associationSourceClass = $this->em->getClassMetadata($mapping['sourceEntity']); + } + + $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform). ' t'; + $whereClauses = array(); + $params = array(); + + $joinNeeded = !in_array($indexBy, $targetEntity->identifier); + + if ($joinNeeded) { // extra join needed if indexBy is not a @id + $joinConditions = array(); + foreach ($wasOwning?$mapping['joinTable']['inverseJoinColumns']:$mapping['joinTable']['joinColumns'] as $joinTableColumn) { + $joinConditions[] = 't.'.$joinTableColumn['name'].' = tr.'.$joinTableColumn['referencedColumnName']; + } + $tableName = $this->quoteStrategy->getTableName($targetEntity, $this->platform); + $quotedJoinTable .= ' JOIN '. $tableName. ' tr ON '.implode(' AND ', $joinConditions); + + $whereClauses[] = 'tr.'.$targetEntity->getColumnName($indexBy).' = ?'; + $params[] = $key; + + } + + foreach ($mapping['joinTableColumns'] as $joinTableColumn) { + + + if (isset($mapping['relationToTargetKeyColumns'][$joinTableColumn])) { + $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; + $params[] = $targetEntity->containsForeignIdentifier + ? $id[$targetEntity->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])] + : $id[$targetEntity->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]]; + } elseif (!$joinNeeded) { + $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; + $params[] = $key; + } + } + + if ($addFilters) { + list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping); + + if ($filterSql) { + $quotedJoinTable .= ' ' . $joinTargetEntitySQL; + $whereClauses[] = $filterSql; + } + } + + return array($quotedJoinTable, $whereClauses, $params); + } /** * @param \Doctrine\ORM\PersistentCollection $coll * @param object $element diff --git a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php index 6c0c1e78e..33aed7c8e 100644 --- a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php @@ -131,6 +131,45 @@ class OneToManyPersister extends AbstractCollectionPersister * {@inheritdoc} */ public function count(PersistentCollection $coll) + { + list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, true); + + $sql = 'SELECT count(*) FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); + + return $this->conn->fetchColumn($sql, $params); + } + + /** + * {@inheritdoc} + */ + public function slice(PersistentCollection $coll, $offset, $length = null) + { + $mapping = $coll->getMapping(); + $uow = $this->em->getUnitOfWork(); + $persister = $uow->getEntityPersister($mapping['targetEntity']); + + return $persister->getOneToManyCollection($mapping, $coll->getOwner(), $offset, $length); + } + + /** + * {@inheritdoc} + */ + public function containsKey(PersistentCollection $coll, $key) + { + list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, true); + + $mapping = $coll->getMapping(); + $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']); + + $whereClauses[] = $sourceClass->getColumnName($mapping['indexBy']) . ' = ?'; + $params[] = $key; + + $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); + + return (bool) $this->conn->fetchColumn($sql, $params); + } + + private function getJoinTableRestrictions(PersistentCollection $coll, $addFilters) { $mapping = $coll->getMapping(); $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); @@ -149,30 +188,18 @@ class OneToManyPersister extends AbstractCollectionPersister : $id[$sourceClass->fieldNames[$joinColumn['referencedColumnName']]]; } - $filterTargetClass = $this->em->getClassMetadata($targetClass->rootEntityName); - foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { - if ($filterExpr = $filter->addFilterConstraint($filterTargetClass, 't')) { - $whereClauses[] = '(' . $filterExpr . ')'; + if ($addFilters) { + $filterTargetClass = $this->em->getClassMetadata($targetClass->rootEntityName); + foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { + if ($filterExpr = $filter->addFilterConstraint($filterTargetClass, 't')) { + $whereClauses[] = '(' . $filterExpr . ')'; + } } } - $sql = 'SELECT count(*)' - . ' FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' t' - . ' WHERE ' . implode(' AND ', $whereClauses); + $quotedJoinTable = $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' t'; - return $this->conn->fetchColumn($sql, $params); - } - - /** - * {@inheritdoc} - */ - public function slice(PersistentCollection $coll, $offset, $length = null) - { - $mapping = $coll->getMapping(); - $uow = $this->em->getUnitOfWork(); - $persister = $uow->getEntityPersister($mapping['targetEntity']); - - return $persister->getOneToManyCollection($mapping, $coll->getOwner(), $offset, $length); + return array($quotedJoinTable, $whereClauses, $params); } /** @@ -200,7 +227,7 @@ class OneToManyPersister extends AbstractCollectionPersister // only works with single id identifier entities. Will throw an // exception in Entity Persisters if that is not the case for the // 'mappedBy' field. - $id = current( $uow->getEntityIdentifier($coll->getOwner())); + $id = current($uow->getEntityIdentifier($coll->getOwner())); return $persister->exists($element, array($mapping['mappedBy'] => $id)); } diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index f6c816037..6e882c405 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -35,6 +35,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsGroup'); $class->associationMappings['users']['fetch'] = ClassMetadataInfo::FETCH_EXTRA_LAZY; + $class->associationMappings['users']['indexBy'] = 'username'; $this->loadFixture(); } @@ -539,7 +540,6 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /* @var $user CmsUser */ $queryCount = $this->getCurrentQueryCount(); - $phonenumber = $user->phonenumbers->get($this->phonenumber); $this->assertFalse($user->phonenumbers->isInitialized()); @@ -576,6 +576,106 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertNull($user->articles->get(-1)); } + public function testContainsKeyIndexByOneToMany() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + /* @var $user CmsUser */ + + $queryCount = $this->getCurrentQueryCount(); + + $contains = $user->articles->containsKey($this->topic); + + $this->assertTrue($contains); + $this->assertFalse($user->articles->isInitialized()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + + public function testContainsKeyIndexByManyToMany() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + + $group = $this->_em->find('Doctrine\Tests\Models\CMS\CmsGroup', $this->groupId); + + $queryCount = $this->getCurrentQueryCount(); + + $contains = $user->groups->containsKey($group->name); + + $this->assertTrue($contains, "The item is not into collection"); + $this->assertFalse($user->groups->isInitialized(), "The collection must not be initialized"); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + public function testContainsKeyIndexByManyToManyNonOwning() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $group = $this->_em->find('Doctrine\Tests\Models\CMS\CmsGroup', $this->groupId); + + $queryCount = $this->getCurrentQueryCount(); + + $contains = $group->users->containsKey($user->username); + + $this->assertTrue($contains, "The item is not into collection"); + $this->assertFalse($group->users->isInitialized(), "The collection must not be initialized"); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + + public function testContainsKeyIndexByWithPkManyToMany() + { + $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $class->associationMappings['groups']['indexBy'] = 'id'; + + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + + $queryCount = $this->getCurrentQueryCount(); + + $contains = $user->groups->containsKey($this->groupId); + + $this->assertTrue($contains, "The item is not into collection"); + $this->assertFalse($user->groups->isInitialized(), "The collection must not be initialized"); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + public function testContainsKeyIndexByWithPkManyToManyNonOwning()//// + { + $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsGroup'); + $class->associationMappings['users']['indexBy'] = 'id'; + + $group = $this->_em->find('Doctrine\Tests\Models\CMS\CmsGroup', $this->groupId); + + $queryCount = $this->getCurrentQueryCount(); + + $contains = $group->users->containsKey($this->userId); + + $this->assertTrue($contains, "The item is not into collection"); + $this->assertFalse($group->users->isInitialized(), "The collection must not be initialized"); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + + public function testContainsKeyNonExistentIndexByOneToMany() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + + $queryCount = $this->getCurrentQueryCount(); + + $contains = $user->articles->containsKey("NonExistentTopic"); + + $this->assertFalse($contains); + $this->assertFalse($user->articles->isInitialized()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + + public function testContainsKeyNonExistentIndexByManyToMany() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + + + $queryCount = $this->getCurrentQueryCount(); + + $contains = $user->groups->containsKey("NonExistentTopic"); + + $this->assertFalse($contains); + $this->assertFalse($user->groups->isInitialized()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + private function loadFixture() { $user1 = new \Doctrine\Tests\Models\CMS\CmsUser(); @@ -646,6 +746,8 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->persist($phonenumber1); $this->_em->persist($phonenumber2); + $user1->addPhonenumber($phonenumber1); + $this->_em->flush(); $this->_em->clear(); @@ -655,5 +757,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->topic = $article1->topic; $this->phonenumber = $phonenumber1->phonenumber; + + } }