From c998797c55e20603f70fe465e864cced7122bc50 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 4 Dec 2010 19:44:10 +0100 Subject: [PATCH 01/12] DDC-546 - Add Extra Lazy Collection prototype. --- .../ORM/Mapping/ClassMetadataInfo.php | 6 + lib/Doctrine/ORM/PersistentCollection.php | 25 +- .../AbstractCollectionPersister.php | 25 ++ .../ORM/Persisters/BasicEntityPersister.php | 45 ++- .../Persisters/JoinedSubclassPersister.php | 17 +- .../ORM/Persisters/ManyToManyPersister.php | 49 +++ .../ORM/Persisters/OneToManyPersister.php | 37 +++ lib/Doctrine/ORM/UnitOfWork.php | 8 +- .../Tests/ORM/Functional/AllTests.php | 1 + .../ORM/Functional/EntityRepositoryTest.php | 2 - .../Functional/ExtraLazyCollectionTest.php | 284 ++++++++++++++++++ .../Doctrine/Tests/OrmFunctionalTestCase.php | 10 + 12 files changed, 483 insertions(+), 26 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 9612bf2ac..42ebfe96b 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -121,6 +121,12 @@ class ClassMetadataInfo * association is fetched. */ const FETCH_EAGER = 3; + /** + * Specifies that an association is to be fetched lazy (on first access) and that + * commands such as Collection#count, Collection#slice are issued directly against + * the database if the collection is not yet initialized. + */ + const FETCH_EXTRALAZY = 4; /** * Identifies a one-to-one association. */ diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index bf7c6da1e..41d360f02 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -59,16 +59,16 @@ final class PersistentCollection implements Collection * The association mapping the collection belongs to. * This is currently either a OneToManyMapping or a ManyToManyMapping. * - * @var Doctrine\ORM\Mapping\AssociationMapping + * @var array */ - private $association; + protected $association; /** * The EntityManager that manages the persistence of the collection. * * @var Doctrine\ORM\EntityManager */ - private $em; + protected $em; /** * The name of the field on the target entities that points to the owner @@ -96,7 +96,7 @@ final class PersistentCollection implements Collection * * @var boolean */ - private $initialized = true; + protected $initialized = true; /** * The wrapped Collection instance. @@ -475,6 +475,17 @@ final class PersistentCollection implements Collection */ public function count() { + if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRALAZY) { + // use a dynamic public property here. That may be slower, but its not using so much + // memory as having a count variable in each collection. + if (!isset($this->doctrineCollectionCount)) { + $this->doctrineCollectionCount = $this->em->getUnitOfWork() + ->getCollectionPersister($this->association) + ->count($this) + count ($this->coll->toArray()); + } + return $this->doctrineCollectionCount; + } + $this->initialize(); return $this->coll->count(); } @@ -675,6 +686,12 @@ final class PersistentCollection implements Collection */ public function slice($offset, $length = null) { + if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRALAZY) { + return $this->em->getUnitOfWork() + ->getCollectionPersister($this->association) + ->slice($this, $offset, $length); + } + $this->initialize(); return $this->coll->slice($offset, $length); } diff --git a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php index 489bb82fc..1847c7136 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php @@ -125,6 +125,31 @@ abstract class AbstractCollectionPersister } } + public function count(PersistentCollection $coll) + { + throw new \BadMethodCallException("Counting the size of this persistent collection is not supported by this CollectionPersister."); + } + + public function slice(PersistentCollection $coll, $offset, $length = null) + { + throw new \BadMethodCallException("Slicing elements is not supported by this CollectionPersister."); + } + + public function contains(PersistentCollection $coll, $key) + { + throw new \BadMethodCallException("Checking for existance of an element is not supported by this CollectionPersister."); + } + + public function containsKey(PersistentCollection $coll, $element) + { + throw new \BadMethodCallException("Checking for existance of a key is not supported by this CollectionPersister."); + } + + public function get(PersistentCollection $coll, $index) + { + throw new \BadMethodCallException("Selecting a collection by index is not supported by this CollectionPersister."); + } + /** * Gets the SQL statement used for deleting a row from the collection. * diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 4069b657d..5f1a29859 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -723,9 +723,12 @@ class BasicEntityPersister * @param ManyToManyMapping $assoc The association mapping of the association being loaded. * @param object $sourceEntity The entity that owns the collection. * @param PersistentCollection $coll The collection to fill. + * @param int|null $offset + * @param int|null $limit + * @return array */ - public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) - { + public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll = null, $offset = null, $limit = null) + { $criteria = array(); $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); $joinTableConditions = array(); @@ -772,10 +775,17 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $coll->hydrateAdd($this->_createEntity($result)); + if ($coll) { + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $coll->hydrateAdd($this->_createEntity($result)); + } + } else { + $entities = array(); + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $entities[] = $this->_createEntity($result); + } + return $entities; } - $stmt->closeCursor(); } /** @@ -854,7 +864,7 @@ class BasicEntityPersister * @return string * @todo Refactor: _getSelectSQL(...) */ - protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0) + protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null) { $joinSql = $assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY ? $this->_getSelectManyToManyJoinSQL($assoc) : ''; @@ -872,12 +882,12 @@ class BasicEntityPersister $lockSql = ' ' . $this->_platform->getWriteLockSql(); } - return 'SELECT ' . $this->_getSelectColumnListSQL() + return $this->_platform->modifyLimitQuery('SELECT ' . $this->_getSelectColumnListSQL() . $this->_platform->appendLockHint(' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($this->_class->name), $lockMode) . $joinSql . ($conditionSql ? ' WHERE ' . $conditionSql : '') - . $orderBySql + . $orderBySql, $limit, $offset) . $lockSql; } @@ -1181,8 +1191,10 @@ class BasicEntityPersister * @param OneToManyMapping $assoc * @param array $criteria The criteria by which to select the entities. * @param PersistentCollection The collection to load/fill. + * @param int|null $offset + * @param int|null $limit */ - public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) + public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll = null, $offset = null, $limit = null) { $criteria = array(); $owningAssoc = $this->_class->associationMappings[$assoc['mappedBy']]; @@ -1204,10 +1216,17 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $coll->hydrateAdd($this->_createEntity($result)); + if ($coll) { + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $coll->hydrateAdd($this->_createEntity($result)); + } + } else { + $entities = array(); + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $entities[] = $this->_createEntity($result); + } + return $entities; } - $stmt->closeCursor(); } /** @@ -1252,4 +1271,6 @@ class BasicEntityPersister { }*/ + + #public function countCollection } diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index d75656b06..9ae242a45 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -20,7 +20,8 @@ namespace Doctrine\ORM\Persisters; use Doctrine\ORM\ORMException, - Doctrine\ORM\Mapping\ClassMetadata; + Doctrine\ORM\Mapping\ClassMetadata, + Doctrine\DBAL\LockMode; /** * The joined subclass persister maps a single entity instance to several tables in the @@ -239,7 +240,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister /** * {@inheritdoc} */ - protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0) + protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null) { $idColumns = $this->_class->getIdentifierColumnNames(); $baseTableAlias = $this->_getSQLTableAlias($this->_class->name); @@ -348,10 +349,18 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $this->_selectColumnListSql = $columnList; } - return 'SELECT ' . $this->_selectColumnListSql + $lockSql = ''; + if ($lockMode == LockMode::PESSIMISTIC_READ) { + $lockSql = ' ' . $this->_platform->getReadLockSql(); + } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) { + $lockSql = ' ' . $this->_platform->getWriteLockSql(); + } + + return $this->_platform->modifyLimitQuery('SELECT ' . $this->_selectColumnListSql . ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $baseTableAlias . $joinSql - . ($conditionSql != '' ? ' WHERE ' . $conditionSql : '') . $orderBySql; + . ($conditionSql != '' ? ' WHERE ' . $conditionSql : '') . $orderBySql, $limit, $offset) + . $lockSql; } /** diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index 5f24188ca..a5c861e30 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -181,4 +181,53 @@ class ManyToManyPersister extends AbstractCollectionPersister return $params; } + + /** + * {@inheritdoc} + */ + public function count(PersistentCollection $coll) + { + $params = array(); + $mapping = $coll->getMapping(); + $class = $this->_em->getClassMetadata($mapping['sourceEntity']); + $id = $this->_em->getUnitOfWork()->getEntityIdentifier($coll->getOwner()); + + if ($mapping['isOwningSide']) { + $joinTable = $mapping['joinTable']; + $joinColumns = $mapping['relationToSourceKeyColumns']; + } else { + $mapping = $this->_em->getClassMetadata($mapping['targetEntity'])->associationMappings[$mapping['mappedBy']]; + $joinTable = $mapping['joinTable']; + $joinColumns = $mapping['relationToTargetKeyColumns']; + } + + $whereClause = ''; + foreach ($mapping['joinTableColumns'] as $joinTableColumn) { + if (isset($joinColumns[$joinTableColumn])) { + if ($whereClause !== '') { + $whereClause .= ' AND '; + } + $whereClause .= "$joinTableColumn = ?"; + + $params[] = $id[$class->fieldNames[$joinColumns[$joinTableColumn]]]; + } + } + $sql = 'SELECT count(*) FROM ' . $joinTable['name'] . ' WHERE ' . $whereClause; + + return $this->_conn->fetchColumn($sql, $params); + } + + /** + * @param PersistentCollection $coll + * @param int $offset + * @param int $length + * @return array + */ + public function slice(PersistentCollection $coll, $offset, $length = null) + { + $mapping = $coll->getMapping(); + return $this->_em->getUnitOfWork() + ->getEntityPersister($mapping['targetEntity']) + ->loadManyToManyCollection($mapping, $coll->getOwner(), null, $offset, $length); + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php index f0d3aeafd..1656f98e7 100644 --- a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php @@ -116,4 +116,41 @@ class OneToManyPersister extends AbstractCollectionPersister */ protected function _getDeleteRowSQLParameters(PersistentCollection $coll, $element) {} + + /** + * {@inheritdoc} + */ + public function count(PersistentCollection $coll) + { + $mapping = $coll->getMapping(); + $class = $this->_em->getClassMetadata($mapping['targetEntity']); + $params = array(); + $id = $this->_em->getUnitOfWork()->getEntityIdentifier($coll->getOwner()); + + $where = ''; + foreach ($class->associationMappings[$mapping['mappedBy']]['joinColumns'] AS $joinColumn) { + if ($where != '') { + $where .= ' AND '; + } + $where .= $joinColumn['name'] . " = ?"; + $params[] = $id[$class->fieldNames[$joinColumn['referencedColumnName']]]; + } + + $sql = "SELECT count(*) FROM " . $class->getQuotedTableName($this->_conn->getDatabasePlatform()) . " WHERE " . $where; + return $this->_conn->fetchColumn($sql, $params); + } + + /** + * @param PersistentCollection $coll + * @param int $offset + * @param int $length + * @return \Doctrine\Common\Collections\ArrayCollection + */ + public function slice(PersistentCollection $coll, $offset, $length = null) + { + $mapping = $coll->getMapping(); + return $this->_em->getUnitOfWork() + ->getEntityPersister($mapping['targetEntity']) + ->loadOneToManyCollection($mapping, $coll->getOwner(), null, $offset, $length); + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index ead7e20b9..f35c20eff 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1958,11 +1958,11 @@ class UnitOfWork implements PropertyChangedListener $reflField = $class->reflFields[$field]; $reflField->setValue($entity, $pColl); - if ($assoc['fetch'] == ClassMetadata::FETCH_LAZY) { - $pColl->setInitialized(false); - } else { + if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { $this->loadCollection($pColl); $pColl->takeSnapshot(); + } else { + $pColl->setInitialized(false); } $this->originalEntityData[$oid][$field] = $pColl; } @@ -2123,7 +2123,7 @@ class UnitOfWork implements PropertyChangedListener * Gets the EntityPersister for an Entity. * * @param string $entityName The name of the Entity. - * @return Doctrine\ORM\Persister\AbstractEntityPersister + * @return Doctrine\ORM\Persisters\AbstractEntityPersister */ public function getEntityPersister($entityName) { diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index 9d25377c4..0759bcf7f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -55,6 +55,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\IdentityMapTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\DatabaseDriverTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\PostgreSQLIdentityStrategyTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ExtraLazyCollectionTest'); $suite->addTest(Locking\AllTests::suite()); $suite->addTest(Ticket\AllTests::suite()); diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index d6c29e016..ebe2c19f2 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -9,8 +9,6 @@ use Doctrine\Tests\Models\CMS\CmsAddress; require_once __DIR__ . '/../../TestInit.php'; /** - * Description of DetachedEntityTest - * * @author robo */ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php new file mode 100644 index 000000000..8be44bbea --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -0,0 +1,284 @@ +useModelSet('cms'); + parent::setUp(); + + $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $class->associationMappings['groups']['fetch'] = ClassMetadataInfo::FETCH_EXTRALAZY; + $class->associationMappings['articles']['fetch'] = ClassMetadataInfo::FETCH_EXTRALAZY; + + $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsGroup'); + $class->associationMappings['users']['fetch'] = ClassMetadataInfo::FETCH_EXTRALAZY; + + $this->loadFixture(); + } + + public function tearDown() + { + parent::tearDown(); + + $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $class->associationMappings['groups']['fetch'] = ClassMetadataInfo::FETCH_LAZY; + $class->associationMappings['articles']['fetch'] = ClassMetadataInfo::FETCH_LAZY; + + $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsGroup'); + $class->associationMappings['users']['fetch'] = ClassMetadataInfo::FETCH_LAZY; + } + + /** + * @group DDC-546 + */ + public function testCountNotInitializesCollection() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertFalse($user->groups->isInitialized()); + $this->assertEquals(3, count($user->groups)); + $this->assertFalse($user->groups->isInitialized()); + + foreach ($user->groups AS $group) { } + + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount(), "Expecting two queries to be fired for count, then iteration."); + } + + /** + * @group DDC-546 + */ + public function testCountWhenNewEntitysPresent() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + + $newGroup = new \Doctrine\Tests\Models\CMS\CmsGroup(); + $newGroup->name = "Test4"; + + $user->addGroup($newGroup); + $this->_em->persist($newGroup); + + $this->assertFalse($user->groups->isInitialized()); + $this->assertEquals(4, count($user->groups)); + $this->assertFalse($user->groups->isInitialized()); + } + + /** + * @group DDC-546 + */ + public function testCountWhenInitialized() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $queryCount = $this->getCurrentQueryCount(); + + foreach ($user->groups AS $group) { } + + $this->assertTrue($user->groups->isInitialized()); + $this->assertEquals(3, count($user->groups)); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount(), "Should only execute one query to initialize colleciton, no extra query for count() more."); + } + + /** + * @group DDC-546 + */ + public function testCountInverseCollection() + { + $group = $this->_em->find('Doctrine\Tests\Models\CMS\CmsGroup', $this->groupId); + $this->assertFalse($group->users->isInitialized(), "Pre-Condition"); + + $this->assertEquals(4, count($group->users)); + $this->assertFalse($group->users->isInitialized(), "Extra Lazy collection should not be initialized by counting the collection."); + } + + /** + * @group DDC-546 + */ + public function testCountOneToMany() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $this->assertFalse($user->groups->isInitialized(), "Pre-Condition"); + + $this->assertEquals(2, count($user->articles)); + } + + /** + * @group DDC-546 + */ + public function testFullSlice() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $this->assertFalse($user->groups->isInitialized(), "Pre-Condition: Collection is not initialized."); + + $someGroups = $user->groups->slice(null); + $this->assertEquals(3, count($someGroups)); + } + + /** + * @group DDC-546 + */ + public function testSlice() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $this->assertFalse($user->groups->isInitialized(), "Pre-Condition: Collection is not initialized."); + + $queryCount = $this->getCurrentQueryCount(); + + $someGroups = $user->groups->slice(0, 2); + + $this->assertContainsOnly('Doctrine\Tests\Models\CMS\CmsGroup', $someGroups); + $this->assertEquals(2, count($someGroups)); + $this->assertFalse($user->groups->isInitialized(), "Slice should not initialize the collection if it wasn't before!"); + + $otherGroup = $user->groups->slice(2, 1); + + $this->assertContainsOnly('Doctrine\Tests\Models\CMS\CmsGroup', $otherGroup); + $this->assertEquals(1, count($otherGroup)); + $this->assertFalse($user->groups->isInitialized()); + + foreach ($user->groups AS $group) { } + + $this->assertTrue($user->groups->isInitialized()); + $this->assertEquals(3, count($user->groups)); + + $this->assertEquals($queryCount + 3, $this->getCurrentQueryCount()); + } + + /** + * @group DDC-546 + */ + public function testSliceInitializedCollection() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $queryCount = $this->getCurrentQueryCount(); + + foreach ($user->groups AS $group) { } + + $someGroups = $user->groups->slice(0, 2); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->assertEquals(2, count($someGroups)); + $this->assertTrue($user->groups->contains($someGroups[0])); + $this->assertTrue($user->groups->contains($someGroups[1])); + } + + /** + * @group DDC-546 + */ + public function testSliceInverseCollection() + { + $group = $this->_em->find('Doctrine\Tests\Models\CMS\CmsGroup', $this->groupId); + $this->assertFalse($group->users->isInitialized(), "Pre-Condition"); + $queryCount = $this->getCurrentQueryCount(); + + $someUsers = $group->users->slice(0, 2); + $otherUsers = $group->users->slice(2, 2); + + $this->assertContainsOnly('Doctrine\Tests\Models\CMS\CmsUser', $someUsers); + $this->assertContainsOnly('Doctrine\Tests\Models\CMS\CmsUser', $otherUsers); + $this->assertEquals(2, count($someUsers)); + $this->assertEquals(2, count($otherUsers)); + + // +2 queries executed by slice, +4 are executed by EAGER fetching of User Address. + $this->assertEquals($queryCount + 2 + 4, $this->getCurrentQueryCount()); + } + + /** + * @group DDC-546 + */ + public function testSliceOneToMany() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $this->assertFalse($user->articles->isInitialized(), "Pre-Condition: Collection is not initialized."); + + $queryCount = $this->getCurrentQueryCount(); + + $someArticle = $user->articles->slice(0, 1); + $otherArticle = $user->articles->slice(1, 1); + + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + } + + private function loadFixture() + { + $user1 = new \Doctrine\Tests\Models\CMS\CmsUser(); + $user1->username = "beberlei"; + $user1->name = "Benjamin"; + $user1->status = "active"; + + $user2 = new \Doctrine\Tests\Models\CMS\CmsUser(); + $user2->username = "jwage"; + $user2->name = "Jonathan"; + $user2->status = "active"; + + $user3 = new \Doctrine\Tests\Models\CMS\CmsUser(); + $user3->username = "romanb"; + $user3->name = "Roman"; + $user3->status = "active"; + + $user4 = new \Doctrine\Tests\Models\CMS\CmsUser(); + $user4->username = "gblanco"; + $user4->name = "Guilherme"; + $user4->status = "active"; + + $this->_em->persist($user1); + $this->_em->persist($user2); + $this->_em->persist($user3); + $this->_em->persist($user4); + + $group1 = new \Doctrine\Tests\Models\CMS\CmsGroup(); + $group1->name = "Test1"; + + $group2 = new \Doctrine\Tests\Models\CMS\CmsGroup(); + $group2->name = "Test2"; + + $group3 = new \Doctrine\Tests\Models\CMS\CmsGroup(); + $group3->name = "Test3"; + + $user1->addGroup($group1); + $user1->addGroup($group2); + $user1->addGroup($group3); + + $user2->addGroup($group1); + $user3->addGroup($group1); + $user4->addGroup($group1); + + $this->_em->persist($group1); + $this->_em->persist($group2); + $this->_em->persist($group3); + + $article1 = new \Doctrine\Tests\Models\CMS\CmsArticle(); + $article1->topic = "Test"; + $article1->text = "Test"; + $article1->setAuthor($user1); + + $article2 = new \Doctrine\Tests\Models\CMS\CmsArticle(); + $article2->topic = "Test"; + $article2->text = "Test"; + $article2->setAuthor($user1); + + $this->_em->persist($article1); + $this->_em->persist($article2); + + $this->_em->flush(); + $this->_em->clear(); + + $this->userId = $user1->getId(); + $this->groupId = $group1->id; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index abf04efab..d9dd9bc77 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -314,4 +314,14 @@ abstract class OrmFunctionalTestCase extends OrmTestCase } throw $e; } + + /** + * Using the SQL Logger Stack this method retrieves the current query count executed in this test. + * + * @return int + */ + protected function getCurrentQueryCount() + { + return count($this->_sqlLoggerStack->queries); + } } From d3d9957fd4c808a4b1d7a1e44943d4a47f9d654b Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 4 Dec 2010 19:49:05 +0100 Subject: [PATCH 02/12] DDC-546 - Fix some minor glitches in patch. --- lib/Doctrine/ORM/PersistentCollection.php | 6 +++--- lib/Doctrine/ORM/Persisters/BasicEntityPersister.php | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 41d360f02..b56d0930b 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -61,14 +61,14 @@ final class PersistentCollection implements Collection * * @var array */ - protected $association; + private $association; /** * The EntityManager that manages the persistence of the collection. * * @var Doctrine\ORM\EntityManager */ - protected $em; + private $em; /** * The name of the field on the target entities that points to the owner @@ -96,7 +96,7 @@ final class PersistentCollection implements Collection * * @var boolean */ - protected $initialized = true; + private $initialized = true; /** * The wrapped Collection instance. diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 5f1a29859..caa8c22b3 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -779,11 +779,13 @@ class BasicEntityPersister while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { $coll->hydrateAdd($this->_createEntity($result)); } + $stmt->closeCursor(); } else { $entities = array(); while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { $entities[] = $this->_createEntity($result); } + $stmt->closeCursor(); return $entities; } } @@ -1220,11 +1222,13 @@ class BasicEntityPersister while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { $coll->hydrateAdd($this->_createEntity($result)); } + $stmt->closeCursor(); } else { $entities = array(); while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { $entities[] = $this->_createEntity($result); } + $stmt->closeCursor(); return $entities; } } From 7c567b305a4ec79169e54df940f6f4cc8a8c703b Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 29 Dec 2010 11:10:50 +0100 Subject: [PATCH 03/12] Refactor DDC-546 persister approach. --- .../ORM/Persisters/BasicEntityPersister.php | 104 ++++++++++++++---- .../ORM/Persisters/ManyToManyPersister.php | 2 +- .../ORM/Persisters/OneToManyPersister.php | 2 +- .../Functional/ExtraLazyCollectionTest.php | 1 + 4 files changed, 88 insertions(+), 21 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index caa8c22b3..9d4974eac 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -717,6 +717,27 @@ class BasicEntityPersister return $entities; } + /** + * Get (sliced or full) elements of the given collection. + * + * @param array $assoc + * @param object $sourceEntity + * @param int|null $offset + * @param int|null $limit + * @return array + */ + public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) + { + $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit); + + $entities = array(); + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $entities[] = $this->_createEntity($result); + } + $stmt->closeCursor(); + return $entities; + } + /** * Loads a collection of entities of a many-to-many association. * @@ -727,7 +748,17 @@ class BasicEntityPersister * @param int|null $limit * @return array */ - public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll = null, $offset = null, $limit = null) + public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) + { + $stmt = $this->getManyToManyStatement($assoc, $sourceEntity); + + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $coll->hydrateAdd($this->_createEntity($result)); + } + $stmt->closeCursor(); + } + + private function getManyToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null) { $criteria = array(); $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); @@ -774,20 +805,7 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc); list($params, $types) = $this->expandParameters($criteria); - $stmt = $this->_conn->executeQuery($sql, $params, $types); - if ($coll) { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $coll->hydrateAdd($this->_createEntity($result)); - } - $stmt->closeCursor(); - } else { - $entities = array(); - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entities[] = $this->_createEntity($result); - } - $stmt->closeCursor(); - return $entities; - } + return $this->_conn->executeQuery($sql, $params, $types); } /** @@ -1187,16 +1205,56 @@ class BasicEntityPersister return $conditionSql; } + /** + * Return an array with (sliced or full list) of elements in the specified collection. + * + * @param array $assoc + * @param object $sourceEntity + * @param int $offset + * @param int $limit + * @return array + */ + public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) + { + $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit); + + $entities = array(); + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $entities[] = $this->_createEntity($result); + } + $stmt->closeCursor(); + return $entities; + } + /** * Loads a collection of entities in a one-to-many association. * - * @param OneToManyMapping $assoc - * @param array $criteria The criteria by which to select the entities. - * @param PersistentCollection The collection to load/fill. + * @param array $assoc + * @param object $sourceEntity + * @param PersistentCollection $coll The collection to load/fill. * @param int|null $offset * @param int|null $limit */ - public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll = null, $offset = null, $limit = null) + public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) + { + $stmt = $this->getOneToManyStatement($assoc, $sourceEntity); + + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $coll->hydrateAdd($this->_createEntity($result)); + } + $stmt->closeCursor(); + } + + /** + * Build criteria and execute SQL statement to fetch the one to many entities from. + * + * @param array $assoc + * @param object $sourceEntity + * @param int|null $offset + * @param int|null $limit + * @return Doctrine\DBAL\Statement + */ + private function getOneToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null) { $criteria = array(); $owningAssoc = $this->_class->associationMappings[$assoc['mappedBy']]; @@ -1215,6 +1273,7 @@ class BasicEntityPersister } } +<<<<<<< HEAD $sql = $this->_getSelectEntitiesSQL($criteria, $assoc); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); @@ -1231,6 +1290,13 @@ class BasicEntityPersister $stmt->closeCursor(); return $entities; } +======= + $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, LockMode::NONE, $limit, $offset); + $params = array_values($criteria); + $stmt = $this->_conn->executeQuery($sql, $params); + + return $stmt; +>>>>>>> Refactor DDC-546 persister approach. } /** diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index a5c861e30..65afe23f0 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -228,6 +228,6 @@ class ManyToManyPersister extends AbstractCollectionPersister $mapping = $coll->getMapping(); return $this->_em->getUnitOfWork() ->getEntityPersister($mapping['targetEntity']) - ->loadManyToManyCollection($mapping, $coll->getOwner(), null, $offset, $length); + ->getManyToManyCollection($mapping, $coll->getOwner(), $offset, $length); } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php index 1656f98e7..d60cc980f 100644 --- a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php @@ -151,6 +151,6 @@ class OneToManyPersister extends AbstractCollectionPersister $mapping = $coll->getMapping(); return $this->_em->getUnitOfWork() ->getEntityPersister($mapping['targetEntity']) - ->loadOneToManyCollection($mapping, $coll->getOwner(), null, $offset, $length); + ->getOneToManyCollection($mapping, $coll->getOwner(), $offset, $length); } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index 8be44bbea..fbd6451ac 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -28,6 +28,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsGroup'); $class->associationMappings['users']['fetch'] = ClassMetadataInfo::FETCH_EXTRALAZY; + $this->loadFixture(); } From 75d59d8695c8780054057a1c5cefd213c21b40d1 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 29 Dec 2010 12:27:14 +0100 Subject: [PATCH 04/12] DDC-546 - Added functionality for extra-lazy PersistentCollection::contains(). --- lib/Doctrine/ORM/PersistentCollection.php | 24 ++----- .../AbstractCollectionPersister.php | 4 +- .../ORM/Persisters/BasicEntityPersister.php | 14 ++-- .../ORM/Persisters/ManyToManyPersister.php | 52 +++++++++++++- .../ORM/Persisters/OneToManyPersister.php | 25 ++++++- .../Functional/ExtraLazyCollectionTest.php | 68 ++++++++++++++++++- 6 files changed, 156 insertions(+), 31 deletions(-) diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index b56d0930b..fc2a00ba1 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -404,22 +404,12 @@ final class PersistentCollection implements Collection */ public function contains($element) { - /* DRAFT - if ($this->initialized) { - return $this->coll->contains($element); - } else { - if ($element is MANAGED) { - if ($this->coll->contains($element)) { - return true; - } - $exists = check db for existence; - if ($exists) { - $this->coll->add($element); - } - return $exists; - } - return false; - }*/ + if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRALAZY) { + return $this->coll->contains($element) || + $this->em->getUnitOfWork() + ->getCollectionPersister($this->association) + ->contains($this, $element); + } $this->initialize(); return $this->coll->contains($element); @@ -481,7 +471,7 @@ final class PersistentCollection implements Collection if (!isset($this->doctrineCollectionCount)) { $this->doctrineCollectionCount = $this->em->getUnitOfWork() ->getCollectionPersister($this->association) - ->count($this) + count ($this->coll->toArray()); + ->count($this) + $this->coll->count(); } return $this->doctrineCollectionCount; } diff --git a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php index 1847c7136..189809697 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php @@ -135,12 +135,12 @@ abstract class AbstractCollectionPersister throw new \BadMethodCallException("Slicing elements is not supported by this CollectionPersister."); } - public function contains(PersistentCollection $coll, $key) + public function contains(PersistentCollection $coll, $element) { throw new \BadMethodCallException("Checking for existance of an element is not supported by this CollectionPersister."); } - public function containsKey(PersistentCollection $coll, $element) + public function containsKey(PersistentCollection $coll, $key) { throw new \BadMethodCallException("Checking for existance of a key is not supported by this CollectionPersister."); } diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 9d4974eac..0eb46edcb 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -1326,21 +1326,17 @@ class BasicEntityPersister * @param object $entity * @return boolean TRUE if the entity exists in the database, FALSE otherwise. */ - public function exists($entity) + public function exists($entity, array $extraConditions = array()) { $criteria = $this->_class->getIdentifierValues($entity); + if ($extraConditions) { + $criteria = array_merge($criteria, $extraConditions); + } + $sql = 'SELECT 1 FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($this->_class->name) . ' WHERE ' . $this->_getSelectConditionSQL($criteria); return (bool) $this->_conn->fetchColumn($sql, array_values($criteria)); } - - //TODO - /*protected function _getOneToOneEagerFetchSQL() - { - - }*/ - - #public function countCollection } diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index 65afe23f0..9e3dc93b5 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -21,7 +21,8 @@ namespace Doctrine\ORM\Persisters; -use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\PersistentCollection, + Doctrine\ORM\UnitOfWork; /** * Persister for many-to-many collections. @@ -230,4 +231,53 @@ class ManyToManyPersister extends AbstractCollectionPersister ->getEntityPersister($mapping['targetEntity']) ->getManyToManyCollection($mapping, $coll->getOwner(), $offset, $length); } + + /** + * @param PersistentCollection $coll + * @param object $element + */ + public function contains(PersistentCollection $coll, $element) + { + $uow = $this->_em->getUnitOfWork(); + + // shortcut for new entities + if ($uow->getEntityState($element, UnitOfWork::STATE_NEW) == UnitOfWork::STATE_NEW) { + return false; + } + + $params = array(); + $mapping = $coll->getMapping(); + $sourceClass = $this->_em->getClassMetadata($mapping['sourceEntity']); + $elementClass = $this->_em->getClassMetadata($mapping['targetEntity']); + $sourceId = $uow->getEntityIdentifier($coll->getOwner()); + $elementId = $uow->getEntityIdentifier($element); + + if ($mapping['isOwningSide']) { + $joinTable = $mapping['joinTable']; + } else { + $joinTable = $elementClass->associationMappings[$mapping['mappedBy']]['joinTable']; + } + + $whereClause = ''; + foreach ($mapping['joinTableColumns'] as $joinTableColumn) { + if (isset($mapping['relationToTargetKeyColumns'][$joinTableColumn])) { + if ($whereClause !== '') { + $whereClause .= ' AND '; + } + $whereClause .= "$joinTableColumn = ?"; + + $params[] = $elementId[$sourceClass->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]]; + } else if (isset($mapping['relationToSourceKeyColumns'][$joinTableColumn])) { + if ($whereClause !== '') { + $whereClause .= ' AND '; + } + $whereClause .= "$joinTableColumn = ?"; + + $params[] = $sourceId[$sourceClass->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]]; + } + } + $sql = 'SELECT 1 FROM ' . $joinTable['name'] . ' WHERE ' . $whereClause; + + return (bool)$this->_conn->fetchColumn($sql, $params); + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php index d60cc980f..4e17cb973 100644 --- a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php @@ -21,7 +21,8 @@ namespace Doctrine\ORM\Persisters; -use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\PersistentCollection, + Doctrine\ORM\UnitOfWork; /** * Persister for one-to-many collections. @@ -153,4 +154,26 @@ class OneToManyPersister extends AbstractCollectionPersister ->getEntityPersister($mapping['targetEntity']) ->getOneToManyCollection($mapping, $coll->getOwner(), $offset, $length); } + + /** + * @param PersistentCollection $coll + * @param object $element + */ + public function contains(PersistentCollection $coll, $element) + { + $mapping = $coll->getMapping(); + $uow = $this->_em->getUnitOfWork(); + + // shortcut for new entities + if ($uow->getEntityState($element, UnitOfWork::STATE_NEW) == UnitOfWork::STATE_NEW) { + return false; + } + + // 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()) ); + + return $uow->getEntityPersister($mapping['targetEntity']) + ->exists($element, array($mapping['mappedBy'] => $id)); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index fbd6451ac..c4de77eac 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -15,6 +15,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase { private $userId; private $groupId; + private $articleId; public function setUp() { @@ -215,6 +216,70 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); } + /** + * @group DDC-546 + */ + public function testContainsOneToMany() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $this->assertFalse($user->articles->isInitialized(), "Pre-Condition: Collection is not initialized."); + + $article = $this->_em->find('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId); + + $queryCount = $this->getCurrentQueryCount(); + $this->assertTrue($user->articles->contains($article)); + $this->assertFalse($user->articles->isInitialized(), "Post-Condition: Collection is not initialized."); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $article = new \Doctrine\Tests\Models\CMS\CmsArticle(); + $article->topic = "Testnew"; + $article->text = "blub"; + + $queryCount = $this->getCurrentQueryCount(); + $this->assertFalse($user->articles->contains($article)); + $this->assertEquals($queryCount, $this->getCurrentQueryCount(), "Checking for contains of new entity should cause no query to be executed."); + + $this->_em->persist($article); + $this->_em->flush(); + + $queryCount = $this->getCurrentQueryCount(); + $this->assertFalse($user->articles->contains($article)); + $this->assertEquals($queryCount+1, $this->getCurrentQueryCount(), "Checking for contains of managed entity should cause one query to be executed."); + $this->assertFalse($user->articles->isInitialized(), "Post-Condition: Collection is not initialized."); + } + + /** + * @group DDC-546 + */ + public function testContainsManyToMany() + { + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $this->assertFalse($user->groups->isInitialized(), "Pre-Condition: Collection is not initialized."); + + $group = $this->_em->find('Doctrine\Tests\Models\CMS\CmsGroup', $this->groupId); + + $queryCount = $this->getCurrentQueryCount(); + $this->assertTrue($user->groups->contains($group)); + $this->assertEquals($queryCount+1, $this->getCurrentQueryCount(), "Checking for contains of managed entity should cause one query to be executed."); + $this->assertFalse($user->groups->isInitialized(), "Post-Condition: Collection is not initialized."); + + $group = new \Doctrine\Tests\Models\CMS\CmsGroup(); + $group->name = "A New group!"; + + $queryCount = $this->getCurrentQueryCount(); + $this->assertFalse($user->groups->contains($group)); + $this->assertEquals($queryCount, $this->getCurrentQueryCount(), "Checking for contains of new entity should cause no query to be executed."); + $this->assertFalse($user->groups->isInitialized(), "Post-Condition: Collection is not initialized."); + + $this->_em->persist($group); + $this->_em->flush(); + + $queryCount = $this->getCurrentQueryCount(); + $this->assertFalse($user->groups->contains($group)); + $this->assertEquals($queryCount+1, $this->getCurrentQueryCount(), "Checking for contains of managed entity should cause one query to be executed."); + $this->assertFalse($user->groups->isInitialized(), "Post-Condition: Collection is not initialized."); + } + private function loadFixture() { $user1 = new \Doctrine\Tests\Models\CMS\CmsUser(); @@ -278,7 +343,8 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->flush(); $this->_em->clear(); - + + $this->articleId = $article1->id; $this->userId = $user1->getId(); $this->groupId = $group1->id; } From f572be92e2d2005bd058398386b8435e70a074c9 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 29 Dec 2010 12:34:09 +0100 Subject: [PATCH 05/12] DDC-546 - Add EXTRALAZY to doctrine-mapping.xsd enumeration of fetch-types. --- doctrine-mapping.xsd | 1 + 1 file changed, 1 insertion(+) diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index 680123fd0..f1f8db814 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -121,6 +121,7 @@ + From 685e327b436b3fb6053abacf65d6465720512317 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 2 Jan 2011 12:54:55 +0100 Subject: [PATCH 06/12] DDC-546 - Fix some rebasing issues. --- .../ORM/Persisters/BasicEntityPersister.php | 27 +++---------------- .../Tests/Mocks/EntityPersisterMock.php | 2 +- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 0eb46edcb..e626d17a2 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -803,7 +803,7 @@ class BasicEntityPersister } } - $sql = $this->_getSelectEntitiesSQL($criteria, $assoc); + $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset); list($params, $types) = $this->expandParameters($criteria); return $this->_conn->executeQuery($sql, $params, $types); } @@ -1273,30 +1273,9 @@ class BasicEntityPersister } } -<<<<<<< HEAD - $sql = $this->_getSelectEntitiesSQL($criteria, $assoc); + $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset); list($params, $types) = $this->expandParameters($criteria); - $stmt = $this->_conn->executeQuery($sql, $params, $types); - if ($coll) { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $coll->hydrateAdd($this->_createEntity($result)); - } - $stmt->closeCursor(); - } else { - $entities = array(); - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entities[] = $this->_createEntity($result); - } - $stmt->closeCursor(); - return $entities; - } -======= - $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, LockMode::NONE, $limit, $offset); - $params = array_values($criteria); - $stmt = $this->_conn->executeQuery($sql, $params); - - return $stmt; ->>>>>>> Refactor DDC-546 persister approach. + return $this->_conn->executeQuery($sql, $params, $types); } /** diff --git a/tests/Doctrine/Tests/Mocks/EntityPersisterMock.php b/tests/Doctrine/Tests/Mocks/EntityPersisterMock.php index 61d4d855e..157c96e78 100644 --- a/tests/Doctrine/Tests/Mocks/EntityPersisterMock.php +++ b/tests/Doctrine/Tests/Mocks/EntityPersisterMock.php @@ -59,7 +59,7 @@ class EntityPersisterMock extends \Doctrine\ORM\Persisters\BasicEntityPersister $this->_updates[] = $entity; } - public function exists($entity) + public function exists($entity, array $extraConditions = array()) { $this->existsCalled = true; } From 3acc05d9536a38e5d0f253174f2061aa99888ab2 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 2 Jan 2011 13:37:29 +0100 Subject: [PATCH 07/12] DDC-546 - Fix bug in inverse many-to-many contains. --- .../ORM/Persisters/ManyToManyPersister.php | 21 +++++++++------- .../Functional/ExtraLazyCollectionTest.php | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index 9e3dc93b5..fcfdfc6c4 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -247,16 +247,21 @@ class ManyToManyPersister extends AbstractCollectionPersister $params = array(); $mapping = $coll->getMapping(); - $sourceClass = $this->_em->getClassMetadata($mapping['sourceEntity']); - $elementClass = $this->_em->getClassMetadata($mapping['targetEntity']); - $sourceId = $uow->getEntityIdentifier($coll->getOwner()); - $elementId = $uow->getEntityIdentifier($element); - if ($mapping['isOwningSide']) { - $joinTable = $mapping['joinTable']; + if (!$mapping['isOwningSide']) { + $sourceClass = $this->_em->getClassMetadata($mapping['targetEntity']); + $targetClass = $this->_em->getClassMetadata($mapping['sourceEntity']); + $sourceId = $uow->getEntityIdentifier($element); + $targetId = $uow->getEntityIdentifier($coll->getOwner()); + + $mapping = $sourceClass->associationMappings[$mapping['mappedBy']]; } else { - $joinTable = $elementClass->associationMappings[$mapping['mappedBy']]['joinTable']; + $sourceClass = $this->_em->getClassMetadata($mapping['sourceEntity']); + $targetClass = $this->_em->getClassMetadata($mapping['targetEntity']); + $sourceId = $uow->getEntityIdentifier($coll->getOwner()); + $targetId = $uow->getEntityIdentifier($element); } + $joinTable = $mapping['joinTable']; $whereClause = ''; foreach ($mapping['joinTableColumns'] as $joinTableColumn) { @@ -266,7 +271,7 @@ class ManyToManyPersister extends AbstractCollectionPersister } $whereClause .= "$joinTableColumn = ?"; - $params[] = $elementId[$sourceClass->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]]; + $params[] = $targetId[$sourceClass->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]]; } else if (isset($mapping['relationToSourceKeyColumns'][$joinTableColumn])) { if ($whereClause !== '') { $whereClause .= ' AND '; diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index c4de77eac..7d771b557 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -280,6 +280,30 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertFalse($user->groups->isInitialized(), "Post-Condition: Collection is not initialized."); } + /** + * @group DDC-546 + */ + public function testContainsManyToManyInverse() + { + $group = $this->_em->find('Doctrine\Tests\Models\CMS\CmsGroup', $this->groupId); + $this->assertFalse($group->users->isInitialized(), "Pre-Condition: Collection is not initialized."); + + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + + $queryCount = $this->getCurrentQueryCount(); + $this->assertTrue($group->users->contains($user)); + $this->assertEquals($queryCount+1, $this->getCurrentQueryCount(), "Checking for contains of managed entity should cause one query to be executed."); + $this->assertFalse($user->groups->isInitialized(), "Post-Condition: Collection is not initialized."); + + $newUser = new \Doctrine\Tests\Models\CMS\CmsUser(); + $newUser->name = "A New group!"; + + $queryCount = $this->getCurrentQueryCount(); + $this->assertFalse($group->users->contains($newUser)); + $this->assertEquals($queryCount, $this->getCurrentQueryCount(), "Checking for contains of new entity should cause no query to be executed."); + $this->assertFalse($user->groups->isInitialized(), "Post-Condition: Collection is not initialized."); + } + private function loadFixture() { $user1 = new \Doctrine\Tests\Models\CMS\CmsUser(); From cbfdf6197632d822167b25be82df0866088ccfa8 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 2 Jan 2011 13:41:18 +0100 Subject: [PATCH 08/12] DDC-546 - Bugfix for PersistentCollection::count() in EXTRA LAZY special case. --- lib/Doctrine/ORM/PersistentCollection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index fc2a00ba1..0732dd01c 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -471,9 +471,9 @@ final class PersistentCollection implements Collection if (!isset($this->doctrineCollectionCount)) { $this->doctrineCollectionCount = $this->em->getUnitOfWork() ->getCollectionPersister($this->association) - ->count($this) + $this->coll->count(); + ->count($this); } - return $this->doctrineCollectionCount; + return $this->doctrineCollectionCount + $this->coll->count(); } $this->initialize(); From 89e7e8623c257698bc61c40debc42fab3dcfd476 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 2 Jan 2011 13:43:49 +0100 Subject: [PATCH 09/12] DDC-546 - Remove dynamic public property approach in PersistentCollection::count() EXTRA_LAZY. --- lib/Doctrine/ORM/PersistentCollection.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 0732dd01c..e104593bb 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -466,14 +466,9 @@ final class PersistentCollection implements Collection public function count() { if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRALAZY) { - // use a dynamic public property here. That may be slower, but its not using so much - // memory as having a count variable in each collection. - if (!isset($this->doctrineCollectionCount)) { - $this->doctrineCollectionCount = $this->em->getUnitOfWork() - ->getCollectionPersister($this->association) - ->count($this); - } - return $this->doctrineCollectionCount + $this->coll->count(); + return $this->em->getUnitOfWork() + ->getCollectionPersister($this->association) + ->count($this) + $this->coll->count(); } $this->initialize(); From a3cab174cab6ffe445db3d96592e54415f086ef3 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 2 Jan 2011 14:04:52 +0100 Subject: [PATCH 10/12] DDC-546 - Updated with support for DDC-117. --- lib/Doctrine/ORM/Persisters/ManyToManyPersister.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index fcfdfc6c4..bbea8c1f8 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -271,14 +271,22 @@ class ManyToManyPersister extends AbstractCollectionPersister } $whereClause .= "$joinTableColumn = ?"; - $params[] = $targetId[$sourceClass->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]]; + if ($targetClass->containsForeignIdentifier) { + $params[] = $targetId[$targetClass->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])]; + } else { + $params[] = $targetId[$targetClass->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]]; + } } else if (isset($mapping['relationToSourceKeyColumns'][$joinTableColumn])) { if ($whereClause !== '') { $whereClause .= ' AND '; } $whereClause .= "$joinTableColumn = ?"; - $params[] = $sourceId[$sourceClass->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]]; + if ($sourceClass->containsForeignIdentifier) { + $params[] = $sourceId[$sourceClass->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])]; + } else { + $params[] = $sourceId[$sourceClass->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]]; + } } } $sql = 'SELECT 1 FROM ' . $joinTable['name'] . ' WHERE ' . $whereClause; From 247fc43cefeea42a7c2fbfaba1940e3568f0bd4d Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 2 Jan 2011 15:10:47 +0100 Subject: [PATCH 11/12] DDC-546 - Rename ClassMetadataInfo::FETCH_EXTRALAZY to ClassMetadataInfo::FETCH_EXTRA_LAZY --- lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php | 2 +- lib/Doctrine/ORM/PersistentCollection.php | 6 +++--- .../Tests/ORM/Functional/ExtraLazyCollectionTest.php | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 42ebfe96b..4ac7e87c0 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -126,7 +126,7 @@ class ClassMetadataInfo * commands such as Collection#count, Collection#slice are issued directly against * the database if the collection is not yet initialized. */ - const FETCH_EXTRALAZY = 4; + const FETCH_EXTRA_LAZY = 4; /** * Identifies a one-to-one association. */ diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index e104593bb..ce3c2e45d 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -404,7 +404,7 @@ final class PersistentCollection implements Collection */ public function contains($element) { - if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRALAZY) { + if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) { return $this->coll->contains($element) || $this->em->getUnitOfWork() ->getCollectionPersister($this->association) @@ -465,7 +465,7 @@ final class PersistentCollection implements Collection */ public function count() { - if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRALAZY) { + if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) { return $this->em->getUnitOfWork() ->getCollectionPersister($this->association) ->count($this) + $this->coll->count(); @@ -671,7 +671,7 @@ final class PersistentCollection implements Collection */ public function slice($offset, $length = null) { - if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRALAZY) { + if (!$this->initialized && $this->association['fetch'] == Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) { return $this->em->getUnitOfWork() ->getCollectionPersister($this->association) ->slice($this, $offset, $length); diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index 7d771b557..031061b84 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -23,11 +23,11 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase parent::setUp(); $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); - $class->associationMappings['groups']['fetch'] = ClassMetadataInfo::FETCH_EXTRALAZY; - $class->associationMappings['articles']['fetch'] = ClassMetadataInfo::FETCH_EXTRALAZY; + $class->associationMappings['groups']['fetch'] = ClassMetadataInfo::FETCH_EXTRA_LAZY; + $class->associationMappings['articles']['fetch'] = ClassMetadataInfo::FETCH_EXTRA_LAZY; $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsGroup'); - $class->associationMappings['users']['fetch'] = ClassMetadataInfo::FETCH_EXTRALAZY; + $class->associationMappings['users']['fetch'] = ClassMetadataInfo::FETCH_EXTRA_LAZY; $this->loadFixture(); From 3539b32629b097d06aef9f96b51bad31cc8a4c2f Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 2 Jan 2011 15:14:12 +0100 Subject: [PATCH 12/12] DDC-546 - Found some more code that needs DDC-117 compliance. --- lib/Doctrine/ORM/Persisters/ManyToManyPersister.php | 6 +++++- lib/Doctrine/ORM/Persisters/OneToManyPersister.php | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index bbea8c1f8..6ca2b15a5 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -210,7 +210,11 @@ class ManyToManyPersister extends AbstractCollectionPersister } $whereClause .= "$joinTableColumn = ?"; - $params[] = $id[$class->fieldNames[$joinColumns[$joinTableColumn]]]; + if ($class->containsForeignIdentifier) { + $params[] = $id[$class->getFieldForColumn($joinColumns[$joinTableColumn])]; + } else { + $params[] = $id[$class->fieldNames[$joinColumns[$joinTableColumn]]]; + } } } $sql = 'SELECT count(*) FROM ' . $joinTable['name'] . ' WHERE ' . $whereClause; diff --git a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php index 4e17cb973..5e889ddb9 100644 --- a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php @@ -134,7 +134,11 @@ class OneToManyPersister extends AbstractCollectionPersister $where .= ' AND '; } $where .= $joinColumn['name'] . " = ?"; - $params[] = $id[$class->fieldNames[$joinColumn['referencedColumnName']]]; + if ($class->containsForeignIdentifier) { + $params[] = $id[$class->getFieldForColumn($joinColumn['referencedColumnName'])]; + } else { + $params[] = $id[$class->fieldNames[$joinColumn['referencedColumnName']]]; + } } $sql = "SELECT count(*) FROM " . $class->getQuotedTableName($this->_conn->getDatabasePlatform()) . " WHERE " . $where;