From 32df9451fd0d31ca7e5cd0d55721fd22844a13de Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 31 Dec 2010 10:54:20 +0100 Subject: [PATCH 01/71] DDC-952 - Implemented first approach for batching eager loads of ToOne associations. --- .../ORM/Internal/Hydration/ObjectHydrator.php | 3 ++ .../ORM/Persisters/BasicEntityPersister.php | 34 +++++++++++++++--- lib/Doctrine/ORM/UnitOfWork.php | 36 ++++++++++++++++--- .../ORM/Functional/Ticket/DDC633Test.php | 34 +++++++++++++++++- 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 202fdc7ff..4bf90d0ff 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -59,6 +59,7 @@ class ObjectHydrator extends AbstractHydrator $this->_resultPointers = $this->_idTemplate = array(); $this->_resultCounter = 0; + $this->_hints['deferEagerLoad'] = true; foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { $this->_identifierMap[$dqlAlias] = array(); @@ -132,6 +133,8 @@ class ObjectHydrator extends AbstractHydrator $coll->takeSnapshot(); } + $this->_em->getUnitOfWork()->triggerEagerLoads(); + return $result; } diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index c9ae5ddd9..57090ba83 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -560,7 +560,10 @@ class BasicEntityPersister $result = $stmt->fetch(PDO::FETCH_ASSOC); $stmt->closeCursor(); - return $this->_createEntity($result, $entity, $hints); + $hints['deferEagerLoad'] = true; + $entity = $this->_createEntity($result, $entity, $hints); + $this->_em->getUnitOfWork()->triggerEagerLoads(); + return $entity; } /** @@ -577,6 +580,10 @@ class BasicEntityPersister */ public function loadOneToOneEntity(array $assoc, $sourceEntity, $targetEntity, array $identifier = array()) { + if ($foundEntity = $this->_em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity'])) { + return $foundEntity; + } + $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); if ($assoc['isOwningSide']) { @@ -739,10 +746,13 @@ class BasicEntityPersister $result = $stmt->fetchAll(PDO::FETCH_ASSOC); $stmt->closeCursor(); + $hints = array('deferEagerLoads' => true); foreach ($result as $row) { - $entities[] = $this->_createEntity($row); + $entities[] = $this->_createEntity($row, null, $hints); } + $this->_em->getUnitOfWork()->triggerEagerLoads(); + return $entities; } @@ -870,7 +880,14 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset); list($params, $types) = $this->expandParameters($criteria); - return $this->_conn->executeQuery($sql, $params, $types); + $stmt = $this->_conn->executeQuery($sql, $params, $types); + $hints = array('deferEagerLoads' => true); + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $coll->hydrateAdd($this->_createEntity($result, null, $hints)); + } + $stmt->closeCursor(); + + $this->_em->getUnitOfWork()->triggerEagerLoads(); } /** @@ -1330,7 +1347,16 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset); list($params, $types) = $this->expandParameters($criteria); - return $this->_conn->executeQuery($sql, $params, $types); + + $stmt = $this->_conn->executeQuery($sql, $params, $types); + $hints = array('deferEagerLoads' => true); + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $coll->hydrateAdd($this->_createEntity($result, null, $hints)); + } + $stmt->closeCursor(); + + $this->_em->getUnitOfWork()->triggerEagerLoads(); + return $stmt; } /** diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index d0f5bc022..1cc0c47dc 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1928,10 +1928,18 @@ class UnitOfWork implements PropertyChangedListener $newValue = $this->getEntityPersister($assoc['targetEntity']) ->loadOneToOneEntity($assoc, $entity, null, $associatedId); } else { - if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { - // TODO: Maybe it could be optimized to do an eager fetch with a JOIN inside - // the persister instead of this rather unperformant approach. - $newValue = $this->em->find($assoc['targetEntity'], $associatedId); + if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER && isset($hints['deferEagerLoad'])) { + if (!isset($this->eagerLoadingEntities[$assoc['targetEntity']])) { + $this->eagerLoadingEntities[$assoc['targetEntity']] = array(); + } + + // TODO: Is there a faster approach? + $this->eagerLoadingEntities[$assoc['targetEntity']] = array_merge_recursive( + $this->eagerLoadingEntities[$assoc['targetEntity']], + array_map(function($id) { + return array($id); + }, $associatedId) + ); } else { $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); } @@ -1983,6 +1991,26 @@ class UnitOfWork implements PropertyChangedListener } /** +<<<<<<< HEAD +======= + * @return void + */ + public function triggerEagerLoads() + { + if (!$this->eagerLoadingEntities) { + return; + } + + $eagerLoadingEntities = $this->eagerLoadingEntities; + $this->eagerLoadingEntities = array(); + + foreach ($eagerLoadingEntities AS $entityName => $ids) { + $this->getEntityPersister($entityName)->loadAll($ids); + } + } + + /** +>>>>>>> DDC-952 - Implemented first approach for batching eager loads of ToOne associations. * Initializes (loads) an uninitialized persistent collection of an entity. * * @param PeristentCollection $collection The collection to initialize. diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php index d375f408c..d4b093da9 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php @@ -21,6 +21,10 @@ class DDC633Test extends \Doctrine\Tests\OrmFunctionalTestCase } } + /** + * @group DDC-633 + * @group DDC-952 + */ public function testOneToOneEager() { $app = new DDC633Appointment(); @@ -35,7 +39,35 @@ class DDC633Test extends \Doctrine\Tests\OrmFunctionalTestCase $eagerAppointment = $this->_em->find(__NAMESPACE__ . '\DDC633Appointment', $app->id); - $this->assertNotType('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); + // Eager loading still produces proxies + $this->assertType('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); + $this->assertTrue($eagerAppointment->patient->__isInitialized__, "Proxy should already be initialized due to eager loading!"); + } + + /** + * @group DDC-633 + * @group DDC-952 + */ + public function testDQLDeferredEagerLoad() + { + for ($i = 0; $i < 10; $i++) { + $app = new DDC633Appointment(); + $pat = new DDC633Patient(); + $app->patient = $pat; + $pat->appointment = $app; + + $this->_em->persist($app); + $this->_em->persist($pat); + } + $this->_em->flush(); + $this->_em->clear(); + + $appointments = $this->_em->createQuery("SELECT a FROM " . __NAMESPACE__ . "\DDC633Appointment a")->getResult(); + + foreach ($appointments AS $eagerAppointment) { + $this->assertType('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); + $this->assertTrue($eagerAppointment->patient->__isInitialized__, "Proxy should already be initialized due to eager loading!"); + } } } From 3d37e436dd0b5675b66c7df85ba2b181787f299a Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 31 Dec 2010 13:11:01 +0100 Subject: [PATCH 02/71] DDC-952 - Refactor eager loading entities, it is only allowed for non composite primary key entities. --- .../ORM/Persisters/BasicEntityPersister.php | 1 - lib/Doctrine/ORM/UnitOfWork.php | 30 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 57090ba83..65aae3c32 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -837,7 +837,6 @@ class BasicEntityPersister { $criteria = array(); $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); - $joinTableConditions = array(); if ($assoc['isOwningSide']) { foreach ($assoc['relationToSourceKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) { if ($sourceClass->containsForeignIdentifier) { diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 1cc0c47dc..0c603e175 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -218,6 +218,20 @@ class UnitOfWork implements PropertyChangedListener //private $_readOnlyObjects = array(); + /** + * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested. + * + * @var array + */ + private $eagerLoadingEntities = array(); + + /** + * Map of Collections that should be eager loaded when requested. + * + * @var array + */ + private $eagerLoadingCollections = array(); + /** * Initializes a new UnitOfWork instance, bound to the given EntityManager. * @@ -1928,9 +1942,16 @@ class UnitOfWork implements PropertyChangedListener $newValue = $this->getEntityPersister($assoc['targetEntity']) ->loadOneToOneEntity($assoc, $entity, null, $associatedId); } else { - if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER && isset($hints['deferEagerLoad'])) { - if (!isset($this->eagerLoadingEntities[$assoc['targetEntity']])) { - $this->eagerLoadingEntities[$assoc['targetEntity']] = array(); + // Deferred eager load only works for single identifier classes + if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { + if (isset($hints['deferEagerLoad']) && !$targetClass->isIdentifierComposite) { + // TODO: Is there a faster approach? + $this->eagerLoadingEntities[$assoc['targetEntity']][] = current($id); + + $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); + } else { + // TODO: This is very imperformant, ignore it? + $newValue = $this->em->find($assoc['targetEntity'], $associatedId); } // TODO: Is there a faster approach? @@ -2005,7 +2026,8 @@ class UnitOfWork implements PropertyChangedListener $this->eagerLoadingEntities = array(); foreach ($eagerLoadingEntities AS $entityName => $ids) { - $this->getEntityPersister($entityName)->loadAll($ids); + $class = $this->em->getClassMetadata($entityName); + $this->getEntityPersister($entityName)->loadAll(array_combine($class->identifier, array($ids))); } } From 03630df20d6a0476aa738694449b80a7ddc6c39c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 6 Mar 2011 11:15:56 +0100 Subject: [PATCH 03/71] Add support for IN(?) queries in repositories using the DBAL support for parameter lists. --- lib/Doctrine/ORM/Persisters/BasicEntityPersister.php | 7 ++++++- lib/Doctrine/ORM/UnitOfWork.php | 12 +----------- lib/vendor/doctrine-dbal | 2 +- .../Tests/ORM/Functional/Ticket/DDC633Test.php | 2 +- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 65aae3c32..033864e70 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -22,6 +22,7 @@ namespace Doctrine\ORM\Persisters; use PDO, Doctrine\DBAL\LockMode, Doctrine\DBAL\Types\Type, + Doctrine\DBAL\Connection, Doctrine\ORM\ORMException, Doctrine\ORM\OptimisticLockException, Doctrine\ORM\EntityManager, @@ -1281,7 +1282,7 @@ class BasicEntityPersister } else { throw ORMException::unrecognizedField($field); } - $conditionSql .= ' = ?'; + $conditionSql .= (is_array($value)) ? ' IN (?)' : ' = ?'; } return $conditionSql; } @@ -1373,6 +1374,10 @@ class BasicEntityPersister if (isset($this->_class->fieldMappings[$field])) { $type = Type::getType($this->_class->fieldMappings[$field]['type'])->getBindingType(); } + if (is_array($value)) { + $type += Connection::ARRAY_PARAM_OFFSET; + } + $params[] = $value; $types[] = $type; } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 0c603e175..2620fc893 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1953,14 +1953,6 @@ class UnitOfWork implements PropertyChangedListener // TODO: This is very imperformant, ignore it? $newValue = $this->em->find($assoc['targetEntity'], $associatedId); } - - // TODO: Is there a faster approach? - $this->eagerLoadingEntities[$assoc['targetEntity']] = array_merge_recursive( - $this->eagerLoadingEntities[$assoc['targetEntity']], - array_map(function($id) { - return array($id); - }, $associatedId) - ); } else { $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); } @@ -2012,8 +2004,6 @@ class UnitOfWork implements PropertyChangedListener } /** -<<<<<<< HEAD -======= * @return void */ public function triggerEagerLoads() @@ -2022,6 +2012,7 @@ class UnitOfWork implements PropertyChangedListener return; } + // avoid infinite recursion $eagerLoadingEntities = $this->eagerLoadingEntities; $this->eagerLoadingEntities = array(); @@ -2032,7 +2023,6 @@ class UnitOfWork implements PropertyChangedListener } /** ->>>>>>> DDC-952 - Implemented first approach for batching eager loads of ToOne associations. * Initializes (loads) an uninitialized persistent collection of an entity. * * @param PeristentCollection $collection The collection to initialize. diff --git a/lib/vendor/doctrine-dbal b/lib/vendor/doctrine-dbal index 556351d9d..9cd6df38d 160000 --- a/lib/vendor/doctrine-dbal +++ b/lib/vendor/doctrine-dbal @@ -1 +1 @@ -Subproject commit 556351d9d6b4a33506f2c1535cccee34faa65d62 +Subproject commit 9cd6df38d841abb4719286ea35a1b37aa2679f8d diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php index d4b093da9..1c7a3d171 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php @@ -63,7 +63,7 @@ class DDC633Test extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->clear(); $appointments = $this->_em->createQuery("SELECT a FROM " . __NAMESPACE__ . "\DDC633Appointment a")->getResult(); - + foreach ($appointments AS $eagerAppointment) { $this->assertType('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); $this->assertTrue($eagerAppointment->patient->__isInitialized__, "Proxy should already be initialized due to eager loading!"); From d9c8a9eecb1c06705ac1f4d1dcf662710ec7ec3f Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 6 Mar 2011 15:28:26 +0100 Subject: [PATCH 04/71] [DDC-952] Fix merge/rebase mistake. --- .../ORM/Persisters/BasicEntityPersister.php | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 033864e70..cd1500584 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -804,18 +804,21 @@ class BasicEntityPersister * @param PersistentCollection $coll */ private function loadCollectionFromStatement($assoc, $stmt, $coll) - { + { + $hints = array('deferEagerLoads' => true); if (isset($assoc['indexBy'])) { while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entity = $this->_createEntity($result); + $entity = $this->_createEntity($result, null, $hints); $coll->hydrateSet($this->_class->reflFields[$assoc['indexBy']]->getValue($entity), $entity); } } else { while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $coll->hydrateAdd($this->_createEntity($result)); + $coll->hydrateAdd($this->_createEntity($result, null, $hints)); } } $stmt->closeCursor(); + + $this->_em->getUnitOfWork()->triggerEagerLoads(); } /** @@ -880,14 +883,7 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset); list($params, $types) = $this->expandParameters($criteria); - $stmt = $this->_conn->executeQuery($sql, $params, $types); - $hints = array('deferEagerLoads' => true); - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $coll->hydrateAdd($this->_createEntity($result, null, $hints)); - } - $stmt->closeCursor(); - - $this->_em->getUnitOfWork()->triggerEagerLoads(); + return $this->_conn->executeQuery($sql, $params, $types); } /** @@ -1348,15 +1344,7 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset); list($params, $types) = $this->expandParameters($criteria); - $stmt = $this->_conn->executeQuery($sql, $params, $types); - $hints = array('deferEagerLoads' => true); - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $coll->hydrateAdd($this->_createEntity($result, null, $hints)); - } - $stmt->closeCursor(); - - $this->_em->getUnitOfWork()->triggerEagerLoads(); - return $stmt; + return $this->_conn->executeQuery($sql, $params, $types); } /** From 851f44a066ffbe7849c1402d5e192be9bbd4383e Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 6 Mar 2011 21:26:54 +0100 Subject: [PATCH 05/71] [DDC-952] [DDC-1050] Use ObjectHydrator inside Persisters, removing a bunch of duplicate code (step1, more necessary) --- .../Internal/Hydration/AbstractHydrator.php | 11 +- .../ORM/Internal/Hydration/ObjectHydrator.php | 12 +- .../AbstractEntityInheritancePersister.php | 2 + .../ORM/Persisters/BasicEntityPersister.php | 116 ++++-------------- .../Persisters/JoinedSubclassPersister.php | 9 +- .../ORM/Persisters/SingleTablePersister.php | 2 + lib/Doctrine/ORM/UnitOfWork.php | 34 +++-- .../ORM/Functional/ReferenceProxyTest.php | 2 +- .../StandardEntityPersisterTest.php | 2 + 9 files changed, 84 insertions(+), 106 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 302cc6b54..a9697c15a 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -211,7 +211,16 @@ abstract class AbstractHydrator } if (isset($cache[$key]['isMetaColumn'])) { - $rowData[$dqlAlias][$cache[$key]['fieldName']] = $value; + if (!isset($rowData[$dqlAlias][$cache[$key]['fieldName']]) || $value !== null) { + $rowData[$dqlAlias][$cache[$key]['fieldName']] = $value; + } + continue; + } + + // in an inheritance hierachy the same field could be defined several times. + // We overwrite this value so long we dont have a non-null value, that value we keep. + // Per definition it cannot be that a field is defined several times and has several values. + if (isset($rowData[$dqlAlias][$cache[$key]['fieldName']]) && $value === null) { continue; } diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 4bf90d0ff..b57d7ed10 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -59,7 +59,9 @@ class ObjectHydrator extends AbstractHydrator $this->_resultPointers = $this->_idTemplate = array(); $this->_resultCounter = 0; - $this->_hints['deferEagerLoad'] = true; + if (!isset($this->_hints['deferEagerLoad'])) { + $this->_hints['deferEagerLoad'] = true; + } foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { $this->_identifierMap[$dqlAlias] = array(); @@ -109,11 +111,17 @@ class ObjectHydrator extends AbstractHydrator */ protected function _cleanup() { + $eagerLoad = (isset($this->_hints['deferEagerLoad'])) && $this->_hints['deferEagerLoad'] == true; + parent::_cleanup(); $this->_identifierMap = $this->_initializedCollections = $this->_existingCollections = $this->_resultPointers = array(); + + if ($eagerLoad) { + $this->_em->getUnitOfWork()->triggerEagerLoads(); + } } /** @@ -133,8 +141,6 @@ class ObjectHydrator extends AbstractHydrator $coll->takeSnapshot(); } - $this->_em->getUnitOfWork()->triggerEagerLoads(); - return $result; } diff --git a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php index bfe1e60d9..e74bf5eb1 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php @@ -112,6 +112,7 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister $this->_resultColumnNames[$columnAlias] = $columnName; $this->declaringClassMap[$columnAlias] = $class; } + $this->_rsm->addFieldResult('r', $columnAlias, $field, $class->name); return "$sql AS $columnAlias"; } @@ -124,6 +125,7 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister $this->_resultColumnNames[$resultColumnName] = $joinColumnName; $this->declaringJoinColumnMap[$resultColumnName] = $className; } + $this->_rsm->addMetaResult('r', $resultColumnName, $joinColumnName); return $tableAlias . ".$joinColumnName AS $columnAlias"; } diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index cd1500584..4174e3dae 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -117,6 +117,15 @@ class BasicEntityPersister * @var array */ protected $_resultColumnNames = array(); + + /** + * ResultSetMapping that is used for all queries. Is generated lazily once per request. + * + * TODO: Evaluate Caching in combination with the other cached SQL snippets. + * + * @var Query\ResultSetMapping + */ + protected $_rsm; /** * The map of column names to DBAL mapping types of all prepared columns used @@ -558,13 +567,14 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, $lockMode); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt->closeCursor(); - - $hints['deferEagerLoad'] = true; - $entity = $this->_createEntity($result, $entity, $hints); - $this->_em->getUnitOfWork()->triggerEagerLoads(); - return $entity; + + if ($entity !== null) { + $hints[Query::HINT_REFRESH] = true; + } + + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + $entities = $hydrator->hydrateAll($stmt, $this->_rsm, $hints); + return $entities ? $entities[0] : null; } /** @@ -649,79 +659,9 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($id); list($params, $types) = $this->expandParameters($id); $stmt = $this->_conn->executeQuery($sql, $params, $types); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt->closeCursor(); - - $metaColumns = array(); - $newData = array(); - - // Refresh simple state - foreach ($result as $column => $value) { - $column = $this->_resultColumnNames[$column]; - if (isset($this->_class->fieldNames[$column])) { - $fieldName = $this->_class->fieldNames[$column]; - $newValue = $this->_conn->convertToPHPValue($value, $this->_class->fieldMappings[$fieldName]['type']); - $this->_class->reflFields[$fieldName]->setValue($entity, $newValue); - $newData[$fieldName] = $newValue; - } else { - $metaColumns[$column] = $value; - } - } - - // Refresh associations - foreach ($this->_class->associationMappings as $field => $assoc) { - $value = $this->_class->reflFields[$field]->getValue($entity); - if ($assoc['type'] & ClassMetadata::TO_ONE) { - if ($value instanceof Proxy && ! $value->__isInitialized__) { - continue; // skip uninitialized proxies - } - - if ($assoc['isOwningSide']) { - $joinColumnValues = array(); - foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { - if ($metaColumns[$srcColumn] !== null) { - $joinColumnValues[$targetColumn] = $metaColumns[$srcColumn]; - } - } - if ( ! $joinColumnValues && $value !== null) { - $this->_class->reflFields[$field]->setValue($entity, null); - $newData[$field] = null; - } else if ($value !== null) { - // Check identity map first, if the entity is not there, - // place a proxy in there instead. - $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); - if ($found = $this->_em->getUnitOfWork()->tryGetById($joinColumnValues, $targetClass->rootEntityName)) { - $this->_class->reflFields[$field]->setValue($entity, $found); - // Complete inverse side, if necessary. - if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) { - $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']]; - $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($found, $entity); - } - $newData[$field] = $found; - } else { - // FIXME: What is happening with subClassees here? - $proxy = $this->_em->getProxyFactory()->getProxy($assoc['targetEntity'], $joinColumnValues); - $this->_class->reflFields[$field]->setValue($entity, $proxy); - $newData[$field] = $proxy; - $this->_em->getUnitOfWork()->registerManaged($proxy, $joinColumnValues, array()); - } - } - } else { - // Inverse side of 1-1/1-x can never be lazy. - //$newData[$field] = $assoc->load($entity, null, $this->_em); - $newData[$field] = $this->_em->getUnitOfWork()->getEntityPersister($assoc['targetEntity']) - ->loadOneToOneEntity($assoc, $entity, null); - } - } else if ($value instanceof PersistentCollection && $value->isInitialized()) { - $value->setInitialized(false); - // no matter if dirty or non-dirty entities are already loaded, smoke them out! - // the beauty of it being, they are still in the identity map - $value->unwrap()->clear(); - $newData[$field] = $value; - } - } - - $this->_em->getUnitOfWork()->setOriginalEntityData($entity, $newData); + + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + $hydrator->hydrateAll($stmt, $this->_rsm, array(Query::HINT_REFRESH => true)); if (isset($this->_class->lifecycleCallbacks[Events::postLoad])) { $this->_class->invokeLifecycleCallbacks(Events::postLoad, $entity); @@ -744,17 +684,9 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); - $result = $stmt->fetchAll(PDO::FETCH_ASSOC); - $stmt->closeCursor(); - $hints = array('deferEagerLoads' => true); - foreach ($result as $row) { - $entities[] = $this->_createEntity($row, null, $hints); - } - - $this->_em->getUnitOfWork()->triggerEagerLoads(); - - return $entities; + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + return $hydrator->hydrateAll($stmt, $this->_rsm, array('deferEagerLoads' => true)); } /** @@ -1036,6 +968,8 @@ class BasicEntityPersister } $columnList = ''; + $this->_rsm = new Query\ResultSetMapping(); + $this->_rsm->addEntityResult($this->_class->name, 'r'); // r for root // Add regular columns to select list foreach ($this->_class->fieldNames as $field) { @@ -1054,6 +988,7 @@ class BasicEntityPersister if ( ! isset($this->_resultColumnNames[$resultColumnName])) { $this->_resultColumnNames[$resultColumnName] = $srcColumn; } + $this->_rsm->addMetaResult('r', $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); } } } @@ -1174,6 +1109,7 @@ class BasicEntityPersister if ( ! isset($this->_resultColumnNames[$columnAlias])) { $this->_resultColumnNames[$columnAlias] = $columnName; } + $this->_rsm->addFieldResult('r', $columnAlias, $field); return "$sql AS $columnAlias"; } diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 8aa784001..918f7e1ef 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -21,7 +21,8 @@ namespace Doctrine\ORM\Persisters; use Doctrine\ORM\ORMException, Doctrine\ORM\Mapping\ClassMetadata, - Doctrine\DBAL\LockMode; + Doctrine\DBAL\LockMode, + Doctrine\ORM\Query\ResultSetMapping; /** * The joined subclass persister maps a single entity instance to several tables in the @@ -243,6 +244,10 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister // Create the column list fragment only once if ($this->_selectColumnListSql === null) { + + $this->_rsm = new ResultSetMapping(); + $this->_rsm->addEntityResult($this->_class->name, 'r'); + // Add regular columns $columnList = ''; foreach ($this->_class->fieldMappings as $fieldName => $mapping) { @@ -279,6 +284,8 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn); $this->_resultColumnNames[$resultColumnName] = $discrColumn; + $this->_rsm->setDiscriminatorColumn('r', $discrColumn); + $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn); } // INNER JOIN parent tables diff --git a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php index 0ffa93826..c0d8c2bfa 100644 --- a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php +++ b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php @@ -49,6 +49,8 @@ class SingleTablePersister extends AbstractEntityInheritancePersister $tableAlias = $this->_getSQLTableAlias($rootClass->name); $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn); $this->_resultColumnNames[$resultColumnName] = $discrColumn; + $this->_rsm->setDiscriminatorColumn('r', $discrColumn); + $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn); // Append subclass columns foreach ($this->_class->subClasses as $subClassName) { diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 2620fc893..6b4f45199 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1966,6 +1966,11 @@ class UnitOfWork implements PropertyChangedListener } $this->originalEntityData[$oid][$field] = $newValue; $class->reflFields[$field]->setValue($entity, $newValue); + + if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) { + $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']]; + $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity); + } } } else { // Inverse side of x-to-one can never be lazy @@ -1973,18 +1978,27 @@ class UnitOfWork implements PropertyChangedListener ->loadOneToOneEntity($assoc, $entity, null)); } } else { - // Inject collection - $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection); - $pColl->setOwner($entity, $assoc); - $reflField = $class->reflFields[$field]; - $reflField->setValue($entity, $pColl); - - if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { - $this->loadCollection($pColl); - $pColl->takeSnapshot(); - } else { + if (isset($hints[Query::HINT_REFRESH])) { + $pColl = $this->_class->reflFields[$field]->getValue($entity); $pColl->setInitialized(false); + // no matter if dirty or non-dirty entities are already loaded, smoke them out! + // the beauty of it being, they are still in the identity map + $pColl->unwrap()->clear(); + } else { + // Inject collection + $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection); + $pColl->setOwner($entity, $assoc); + + $reflField = $class->reflFields[$field]; + $reflField->setValue($entity, $pColl); + + if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { + $this->loadCollection($pColl); + $pColl->takeSnapshot(); + } else { + $pColl->setInitialized(false); + } } $this->originalEntityData[$oid][$field] = $pColl; } diff --git a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php index d37442171..3e66e0b12 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php @@ -42,7 +42,7 @@ class ReferenceProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase { $id = $this->createProduct(); - $productProxy = $this->_factory->getProxy('Doctrine\Tests\Models\ECommerce\ECommerceProduct', array('id' => $id)); + $productProxy = $this->_em->getReference('Doctrine\Tests\Models\ECommerce\ECommerceProduct', array('id' => $id)); $this->assertEquals('Doctrine Cookbook', $productProxy->getName()); } diff --git a/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php b/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php index 9ed8f0821..8b37a02ba 100644 --- a/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php @@ -33,12 +33,14 @@ class StandardEntityPersisterTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->persist($customer); $this->_em->flush(); $this->_em->clear(); + $cardId = $cart->getId(); unset($cart); $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceCart'); $persister = $this->_em->getUnitOfWork()->getEntityPersister('Doctrine\Tests\Models\ECommerce\ECommerceCart'); $newCart = new ECommerceCart(); + $this->_em->getUnitOfWork()->registerManaged($newCart, array('id' => $cardId), array()); $persister->load(array('customer_id' => $customer->getId()), $newCart, $class->associationMappings['customer']); $this->assertEquals('Credit card', $newCart->getPayment()); } From 112f9d14804e207834079bcbc6a3f9ad0f990bf8 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 6 Mar 2011 21:49:02 +0100 Subject: [PATCH 06/71] [DDC-1050] Change refresh of collection back --- lib/Doctrine/ORM/UnitOfWork.php | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 6b4f45199..9f56416cb 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1978,27 +1978,18 @@ class UnitOfWork implements PropertyChangedListener ->loadOneToOneEntity($assoc, $entity, null)); } } else { - - if (isset($hints[Query::HINT_REFRESH])) { - $pColl = $this->_class->reflFields[$field]->getValue($entity); - $pColl->setInitialized(false); - // no matter if dirty or non-dirty entities are already loaded, smoke them out! - // the beauty of it being, they are still in the identity map - $pColl->unwrap()->clear(); + // Inject collection + $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection); + $pColl->setOwner($entity, $assoc); + + $reflField = $class->reflFields[$field]; + $reflField->setValue($entity, $pColl); + + if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { + $this->loadCollection($pColl); + $pColl->takeSnapshot(); } else { - // Inject collection - $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection); - $pColl->setOwner($entity, $assoc); - - $reflField = $class->reflFields[$field]; - $reflField->setValue($entity, $pColl); - - if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { - $this->loadCollection($pColl); - $pColl->takeSnapshot(); - } else { - $pColl->setInitialized(false); - } + $pColl->setInitialized(false); } $this->originalEntityData[$oid][$field] = $pColl; } From a31289b9d772e3e47da83bf9dabcf72a15b49452 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sun, 6 Mar 2011 18:45:09 -0300 Subject: [PATCH 07/71] Added support to NamedQueries through ClassMetadata. --- doctrine-mapping.xsd | 12 ++++ lib/Doctrine/ORM/EntityRepository.php | 11 ++++ lib/Doctrine/ORM/Mapping/ClassMetadata.php | 1 + .../ORM/Mapping/ClassMetadataInfo.php | 64 ++++++++++++++++++- .../ORM/Mapping/Driver/AnnotationDriver.php | 12 ++++ .../Mapping/Driver/DoctrineAnnotations.php | 6 ++ lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 10 +++ .../ORM/Mapping/Driver/YamlDriver.php | 15 +++++ lib/Doctrine/ORM/Mapping/MappingException.php | 9 +++ tests/Doctrine/Tests/Models/CMS/CmsUser.php | 3 + .../ORM/Functional/EntityRepositoryTest.php | 19 ++++++ .../Tests/ORM/Mapping/ClassMetadataTest.php | 56 ++++++++++++++++ .../php/Doctrine.Tests.ORM.Mapping.User.php | 4 ++ .../Doctrine.Tests.ORM.Mapping.User.dcm.xml | 4 ++ .../Doctrine.Tests.ORM.Mapping.User.dcm.yml | 2 + 15 files changed, 225 insertions(+), 3 deletions(-) diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index c0dbfafcc..19aacd237 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -56,6 +56,17 @@ + + + + + + + + + + + @@ -63,6 +74,7 @@ + diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index 60fc91760..810458cb3 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -78,6 +78,17 @@ class EntityRepository implements ObjectRepository ->from($this->_entityName, $alias); } + /** + * Create a new Query instance based on a predefined metadata named query. + * + * @param string $queryName + * @return Query + */ + public function createNamedQuery($queryName) + { + return $this->_em->createQuery($this->_class->getNamedQuery($queryName)); + } + /** * Clears the repository, causing all managed entities to become detached. */ diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index f1b374c5d..a8a8828f6 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -275,6 +275,7 @@ class ClassMetadata extends ClassMetadataInfo { // This metadata is always serialized/cached. $serialized = array( + 'namedQueries', 'associationMappings', 'columnNames', //TODO: Not really needed. Can use fieldMappings[$fieldName]['columnName'] 'fieldMappings', diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index b482b1c5e..7db39621d 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -204,6 +204,13 @@ class ClassMetadataInfo implements ClassMetadata */ public $subClasses = array(); + /** + * READ-ONLY: The named queries allowed to be called directly from Repository. + * + * @var array + */ + public $namedQueries = array(); + /** * READ-ONLY: The field names of all fields that are part of the identifier/primary key * of the mapped entity class. @@ -655,6 +662,32 @@ class ClassMetadataInfo implements ClassMetadata $this->fieldNames[$columnName] : $columnName; } + /** + * Gets the named query. + * + * @see ClassMetadataInfo::$namedQueries + * @throws MappingException + * @param string $queryName The query name + * @return string + */ + public function getNamedQuery($queryName) + { + if ( ! isset($this->namedQueries[$queryName])) { + throw MappingException::queryNotFound($this->name, $queryName); + } + return $this->namedQueries[$queryName]; + } + + /** + * Gets all named queries of the class. + * + * @return array + */ + public function getNamedQueries() + { + return $this->namedQueries; + } + /** * Validates & completes the given field mapping. * @@ -1368,8 +1401,7 @@ class ClassMetadataInfo implements ClassMetadata * Adds an association mapping without completing/validating it. * This is mainly used to add inherited association mappings to derived classes. * - * @param AssociationMapping $mapping - * @param string $owningClassName The name of the class that defined this mapping. + * @param array $mapping */ public function addInheritedAssociationMapping(array $mapping/*, $owningClassName = null*/) { @@ -1385,7 +1417,6 @@ class ClassMetadataInfo implements ClassMetadata * This is mainly used to add inherited field mappings to derived classes. * * @param array $mapping - * @todo Rename: addInheritedFieldMapping */ public function addInheritedFieldMapping(array $fieldMapping) { @@ -1394,6 +1425,22 @@ class ClassMetadataInfo implements ClassMetadata $this->fieldNames[$fieldMapping['columnName']] = $fieldMapping['fieldName']; } + /** + * INTERNAL: + * Adds a named query to this class. + * + * @throws MappingException + * @param array $queryMapping + */ + public function addNamedQuery(array $queryMapping) + { + if (isset($this->namedQueries[$queryMapping['name']])) { + throw MappingException::duplicateQueryMapping($this->name, $queryMapping['name']); + } + $query = str_replace('__CLASS__', $this->name, $queryMapping['query']); + $this->namedQueries[$queryMapping['name']] = $query; + } + /** * Adds a one-to-one mapping. * @@ -1584,6 +1631,17 @@ class ClassMetadataInfo implements ClassMetadata } } + /** + * Checks whether the class has a named query with the given query name. + * + * @param string $fieldName + * @return boolean + */ + public function hasNamedQuery($queryName) + { + return isset($this->namedQueries[$queryName]); + } + /** * Checks whether the class has a mapped association with the given field name. * diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 9eb83c2b8..4cf843d49 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -165,6 +165,18 @@ class AnnotationDriver implements Driver $metadata->setPrimaryTable($primaryTable); } + // Evaluate NamedQueries annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\NamedQueries'])) { + $namedQueriesAnnot = $classAnnotations['Doctrine\ORM\Mapping\NamedQueries']; + + foreach ($namedQueriesAnnot->value as $namedQuery) { + $metadata->addNamedQuery(array( + 'name' => $namedQuery->name, + 'query' => $namedQuery->query + )); + } + } + // Evaluate InheritanceType annotation if (isset($classAnnotations['Doctrine\ORM\Mapping\InheritanceType'])) { $inheritanceTypeAnnot = $classAnnotations['Doctrine\ORM\Mapping\InheritanceType']; diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index ef566a083..31e712d42 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -127,6 +127,12 @@ final class SequenceGenerator extends Annotation { final class ChangeTrackingPolicy extends Annotation {} final class OrderBy extends Annotation {} +final class NamedQueries extends Annotation {} +final class NamedQuery extends Annotation { + public $name; + public $query; +} + /* Annotations for lifecycle callbacks */ final class HasLifecycleCallbacks extends Annotation {} final class PrePersist extends Annotation {} diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index 3ec712b70..e42d89493 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -69,6 +69,16 @@ class XmlDriver extends AbstractFileDriver $metadata->setPrimaryTable($table); + // Evaluate named queries + if (isset($xmlRoot['named-queries'])) { + foreach ($xmlRoot->{'named-queries'}->{'named-query'} as $namedQueryElement) { + $metadata->addNamedQuery(array( + 'name' => (string)$namedQueryElement['name'], + 'query' => (string)$namedQueryElement['query'] + )); + } + } + /* not implemented specially anyway. use table = schema.table if (isset($xmlRoot['schema'])) { $metadata->table['schema'] = (string)$xmlRoot['schema']; diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 0a66a156e..75bfeec74 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -62,6 +62,21 @@ class YamlDriver extends AbstractFileDriver } $metadata->setPrimaryTable($table); + // Evaluate named queries + if (isset($element['namedQueries'])) { + foreach ($element['namedQueries'] as $name => $queryMapping) { + if (is_string($queryMapping)) { + $queryMapping = array('query' => $queryMapping); + } + + if ( ! isset($queryMapping['name'])) { + $queryMapping['name'] = $name; + } + + $metadata->addNamedQuery($queryMapping); + } + } + /* not implemented specially anyway. use table = schema.table if (isset($element['schema'])) { $metadata->table['schema'] = $element['schema']; diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index 11e35808c..b4aaa08e5 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -73,6 +73,11 @@ class MappingException extends \Doctrine\ORM\ORMException return new self("No mapping found for field '$fieldName' on class '$className'."); } + public static function queryNotFound($className, $queryName) + { + return new self("No query found named '$queryName' on class '$className'."); + } + public static function oneToManyRequiresMappedBy($fieldName) { return new self("OneToMany mapping on field '$fieldName' requires the 'mappedBy' attribute."); @@ -160,6 +165,10 @@ class MappingException extends \Doctrine\ORM\ORMException return new self('Property "'.$fieldName.'" in "'.$entity.'" was already declared, but it must be declared only once'); } + public static function duplicateQueryMapping($entity, $queryName) { + return new self('Query named "'.$queryName.'" in "'.$entity.'" was already declared, but it must be declared only once'); + } + public static function singleIdNotAllowedOnCompositePrimaryKey($entity) { return new self('Single id is not allowed on composite primary key in entity '.$entity); } diff --git a/tests/Doctrine/Tests/Models/CMS/CmsUser.php b/tests/Doctrine/Tests/Models/CMS/CmsUser.php index 57741cad1..d9ac982ff 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsUser.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsUser.php @@ -7,6 +7,9 @@ use Doctrine\Common\Collections\ArrayCollection; /** * @Entity * @Table(name="cms_users") + * @NamedQueries({ + * @NamedQuery(name="all", query="SELECT u FROM __CLASS__ u") + * }) */ class CmsUser { diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index ebe2c19f2..a82b09a47 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -288,5 +288,24 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertType('Doctrine\Tests\Models\CMS\CmsAddress', $address); $this->assertEquals($addressId, $address->id); } + + public function testValidNamedQueryRetrieval() + { + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + + $query = $repos->createNamedQuery('all'); + + $this->assertType('Doctrine\ORM\Query', $query); + $this->assertEquals('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u', $query->getDQL()); + } + + public function testInvalidNamedQueryRetrieval() + { + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + + $this->setExpectedException('Doctrine\ORM\Mapping\MappingException'); + + $repos->createNamedQuery('invalidNamedQuery'); + } } diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php index f618f6b3a..74889c0ae 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php @@ -390,4 +390,60 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); $cm->mapField(array('fieldName' => '')); } + + public function testRetrievalOfNamedQueries() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $this->assertEquals(0, count($cm->getNamedQueries())); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + + $this->assertEquals(1, count($cm->getNamedQueries())); + } + + public function testExistanceOfNamedQuery() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $cm->addNamedQuery(array( + 'name' => 'all', + 'query' => 'SELECT u FROM __CLASS__ u' + )); + + $this->assertTrue($cm->hasNamedQuery('all')); + $this->assertFalse($cm->hasNamedQuery('userById')); + } + + public function testRetrieveOfNamedQuery() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + + $this->assertEquals('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = ?1', $cm->getNamedQuery('userById')); + } + + public function testNamingCollisionNamedQueryShouldThrowException() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + + $this->setExpectedException('Doctrine\ORM\Mapping\MappingException'); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + + $cm->addNamedQuery(array( + 'name' => 'userById', + 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' + )); + } } diff --git a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php index 4aadffb30..2abe648ad 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php +++ b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php @@ -10,6 +10,10 @@ $metadata->setChangeTrackingPolicy(ClassMetadataInfo::CHANGETRACKING_DEFERRED_IM $metadata->addLifecycleCallback('doStuffOnPrePersist', 'prePersist'); $metadata->addLifecycleCallback('doOtherStuffOnPrePersistToo', 'prePersist'); $metadata->addLifecycleCallback('doStuffOnPostPersist', 'postPersist'); +$metadata->addNamedQuery(array( + 'name' => 'all', + 'query' => 'SELECT u FROM __CLASS__ u' +)); $metadata->mapField(array( 'id' => true, 'fieldName' => 'id', diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml index 948430c24..c066cbef1 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml @@ -22,6 +22,10 @@ + + + + diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml index b541c8877..a787a93c9 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml @@ -1,6 +1,8 @@ Doctrine\Tests\ORM\Mapping\User: type: entity table: cms_users + namedQueries: + all: SELECT u FROM __CLASS__ u id: id: type: integer From 60eb755fe95d7b53d106e5f8b18748317d7d3bdd Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 8 Mar 2011 22:22:54 +0100 Subject: [PATCH 08/71] DDC-952, DDC-734 Add DQL query hint to switch associations from lazy to eager for deferred initialization optimizations. --- lib/Doctrine/ORM/UnitOfWork.php | 14 ++++++++- .../Tests/ORM/Functional/QueryTest.php | 30 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 9f56416cb..45b2c612b 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1936,6 +1936,18 @@ class UnitOfWork implements PropertyChangedListener $relatedIdHash = implode(' ', $associatedId); if (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])) { $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash]; + + // if this is an uninitialized proxy, we are deferring eager loads, + // this association is marked as eager fetch, and its an uninitialized proxy (wtf!) + // then we cann append this entity for eager loading! + if (isset($hints['fetchEager'][$class->name][$field]) && + isset($hints['deferEagerLoad']) && + !$targetClass->isIdentifierComposite && + $newValue instanceof Proxy && + $newValue->__isInitialized__ === false) { + + $this->eagerLoadingEntities[$assoc['targetEntity']][] = $relatedIdHash; + } } else { if ($targetClass->subClasses) { // If it might be a subtype, it can not be lazy @@ -1943,7 +1955,7 @@ class UnitOfWork implements PropertyChangedListener ->loadOneToOneEntity($assoc, $entity, null, $associatedId); } else { // Deferred eager load only works for single identifier classes - if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { + if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || isset($hints['eagerFetch'][$class->name][$field])) { if (isset($hints['deferEagerLoad']) && !$targetClass->isIdentifierComposite) { // TODO: Is there a faster approach? $this->eagerLoadingEntities[$assoc['targetEntity']][] = current($id); diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index c753960f8..1519e1941 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -313,4 +313,34 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertTrue($result[0]->user instanceof \Doctrine\ORM\Proxy\Proxy); $this->assertFalse($result[0]->user->__isInitialized__); } + + /** + * @group DDC-952 + */ + public function testEnableFetchEagerMode() + { + for ($i = 0; $i < 10; $i++) { + $article = new CmsArticle; + $article->topic = "dr. dolittle"; + $article->text = "Once upon a time ..."; + $author = new CmsUser; + $author->name = "anonymous"; + $author->username = "anon".$i; + $author->status = "here"; + $article->user = $author; + $this->_em->persist($author); + $this->_em->persist($article); + } + $this->_em->flush(); + $this->_em->clear(); + + $articles = $this->_em->createQuery('select a from Doctrine\Tests\Models\CMS\CmsArticle a') + ->setHint('eagerFetch', array('Doctrine\Tests\Models\CMS\CmsArticle' => array('user' => true))) + ->getResult(); + + $this->assertEquals(10, count($articles)); + foreach ($articles AS $article) { + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $article); + } + } } \ No newline at end of file From fd502631c7c119ad752d0fd43c4d2535717b6052 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 8 Mar 2011 22:28:55 +0100 Subject: [PATCH 09/71] DDC-734 - REname query hint to fetchEager. --- lib/Doctrine/ORM/UnitOfWork.php | 2 +- tests/Doctrine/Tests/ORM/Functional/QueryTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 45b2c612b..8ed9dbb54 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1955,7 +1955,7 @@ class UnitOfWork implements PropertyChangedListener ->loadOneToOneEntity($assoc, $entity, null, $associatedId); } else { // Deferred eager load only works for single identifier classes - if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || isset($hints['eagerFetch'][$class->name][$field])) { + if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || isset($hints['fetchEager'][$class->name][$field])) { if (isset($hints['deferEagerLoad']) && !$targetClass->isIdentifierComposite) { // TODO: Is there a faster approach? $this->eagerLoadingEntities[$assoc['targetEntity']][] = current($id); diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index 1519e1941..4f0dc6879 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -335,7 +335,7 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->clear(); $articles = $this->_em->createQuery('select a from Doctrine\Tests\Models\CMS\CmsArticle a') - ->setHint('eagerFetch', array('Doctrine\Tests\Models\CMS\CmsArticle' => array('user' => true))) + ->setHint('fetchEager', array('Doctrine\Tests\Models\CMS\CmsArticle' => array('user' => true))) ->getResult(); $this->assertEquals(10, count($articles)); From 077ae9cee92cd4f2d084bc315e76e3376ff1904b Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 9 Mar 2011 23:14:54 +0100 Subject: [PATCH 10/71] [DDC-914] Fetch join many-to-one/one-to-one associations configured as FETCH_EAGER inside the persisters. --- .../AbstractEntityInheritancePersister.php | 6 +- .../ORM/Persisters/BasicEntityPersister.php | 98 ++++++++++++++++--- .../ORM/Persisters/SingleTablePersister.php | 4 +- lib/Doctrine/ORM/UnitOfWork.php | 1 + .../ORM/Functional/Ticket/DDC1050Test.php | 37 +++++++ .../ORM/Functional/Ticket/DDC633Test.php | 7 +- 6 files changed, 130 insertions(+), 23 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1050Test.php diff --git a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php index e74bf5eb1..da50b3be8 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php @@ -103,16 +103,16 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister /** * {@inheritdoc} */ - protected function _getSelectColumnSQL($field, ClassMetadata $class) + protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r') { $columnName = $class->columnNames[$field]; - $sql = $this->_getSQLTableAlias($class->name) . '.' . $class->getQuotedColumnName($field, $this->_platform); + $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); if ( ! isset($this->_resultColumnNames[$columnAlias])) { $this->_resultColumnNames[$columnAlias] = $columnName; $this->declaringClassMap[$columnAlias] = $class; } - $this->_rsm->addFieldResult('r', $columnAlias, $field, $class->name); + $this->_rsm->addFieldResult($alias, $columnAlias, $field, $class->name); return "$sql AS $columnAlias"; } diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 4174e3dae..d956fe240 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -152,6 +152,14 @@ class BasicEntityPersister * @var string */ protected $_selectColumnListSql; + + /** + * The JOIN SQL fragement used to eagerly load all many-to-one and one-to-one + * associations configured as FETCH_EAGER, aswell as all inverse one-to-one associations. + * + * @var string + */ + protected $_selectJoinSql; /** * Counter for creating unique SQL table and column aliases. @@ -915,7 +923,7 @@ class BasicEntityPersister return $this->_platform->modifyLimitQuery('SELECT ' . $this->_getSelectColumnListSQL() . $this->_platform->appendLockHint(' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($this->_class->name), $lockMode) - . $joinSql + . $this->_selectJoinSql . $joinSql . ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql, $limit, $offset) . $lockSql; @@ -977,18 +985,54 @@ class BasicEntityPersister $columnList .= $this->_getSelectColumnSQL($field, $this->_class); } - foreach ($this->_class->associationMappings as $assoc) { - if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { - foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + $this->_selectJoinSql = ''; + $eagerAliasCounter = 0; + foreach ($this->_class->associationMappings as $assocField => $assoc) { + $assocColumnSQL = $this->_getSelectColumnAssociationSQL($assocField, $assoc, $this->_class); + if ($assocColumnSQL) { + if ($columnList) $columnList .= ', '; + $columnList .= $assocColumnSQL; + } + + if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER && $assoc['type'] & ClassMetadata::TO_ONE) { + $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); + if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) { + continue; // now this is why you shouldn't use inheritance + } + + $assocAlias = 'e' + ($eagerAliasCounter++); + $this->_rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField); + + foreach ($eagerEntity->fieldNames AS $field) { if ($columnList) $columnList .= ', '; - - $columnAlias = $srcColumn . $this->_sqlAliasCounter++; - $columnList .= $this->_getSQLTableAlias($this->_class->name) . ".$srcColumn AS $columnAlias"; - $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); - if ( ! isset($this->_resultColumnNames[$resultColumnName])) { - $this->_resultColumnNames[$resultColumnName] = $srcColumn; + $columnList .= $this->_getSelectColumnSQL($field, $eagerEntity, $assocAlias); + } + + foreach ($eagerEntity->associationMappings as $assoc2Field => $assoc2) { + $assoc2ColumnSQL = $this->_getSelectColumnAssociationSQL($assoc2Field, $assoc2, $eagerEntity); + if ($assoc2ColumnSQL) { + if ($columnList) $columnList .= ', '; + $columnList .= $assoc2ColumnSQL; + } + } + $this->_selectJoinSql .= ' LEFT JOIN'; + if ($assoc['isOwningSide']) { + $this->_selectJoinSql .= ' ' . $eagerEntity->table['name'] . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; + + foreach ($assoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { + $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.'.$sourceCol.' = ' . + $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias) . '.'.$targetCol.' '; + } + } else { + $eagerEntity = $this->_em->getClassMetadata($assoc['sourceEntity']); + $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']); + + $this->_selectJoinSql .= ' ' . $eagerEntity->table['name'] . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; + + foreach ($owningAssoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { + $this->_selectJoinSql .= $this->_getSQLTableAlias($owningAssoc['sourceEntity'], $assocAlias) . '.'.$sourceCol.' = ' . + $this->_getSQLTableAlias($assoc['targetEntity']) . '.' . $targetCol . ' '; } - $this->_rsm->addMetaResult('r', $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); } } } @@ -997,6 +1041,25 @@ class BasicEntityPersister return $this->_selectColumnListSql; } + + protected function _getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class) + { + $columnList = ''; + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { + foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + if ($columnList) $columnList .= ', '; + + $columnAlias = $srcColumn . $this->_sqlAliasCounter++; + $columnList .= $this->_getSQLTableAlias($class->name) . ".$srcColumn AS $columnAlias"; + $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); + if ( ! isset($this->_resultColumnNames[$resultColumnName])) { + $this->_resultColumnNames[$resultColumnName] = $srcColumn; + } + $this->_rsm->addMetaResult('r', $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); + } + } + return $columnList; + } /** * Gets the SQL join fragment used when selecting entities from a @@ -1100,16 +1163,17 @@ class BasicEntityPersister * @param string $field The field name. * @param ClassMetadata $class The class that declares this field. The table this class is * mapped to must own the column for the given field. + * @param string $alias */ - protected function _getSelectColumnSQL($field, ClassMetadata $class) + protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r') { $columnName = $class->columnNames[$field]; - $sql = $this->_getSQLTableAlias($class->name) . '.' . $class->getQuotedColumnName($field, $this->_platform); + $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); if ( ! isset($this->_resultColumnNames[$columnAlias])) { $this->_resultColumnNames[$columnAlias] = $columnName; } - $this->_rsm->addFieldResult('r', $columnAlias, $field); + $this->_rsm->addFieldResult($alias, $columnAlias, $field); return "$sql AS $columnAlias"; } @@ -1121,8 +1185,12 @@ class BasicEntityPersister * @return string The SQL table alias. * @todo Reconsider. Binding table aliases to class names is not such a good idea. */ - protected function _getSQLTableAlias($className) + protected function _getSQLTableAlias($className, $assocName = '') { + if ($assocName) { + $className .= '#'.$assocName; + } + if (isset($this->_sqlTableAliases[$className])) { return $this->_sqlTableAliases[$className]; } diff --git a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php index c0d8c2bfa..94450fd89 100644 --- a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php +++ b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php @@ -88,9 +88,9 @@ class SingleTablePersister extends AbstractEntityInheritancePersister } /** {@inheritdoc} */ - protected function _getSQLTableAlias($className) + protected function _getSQLTableAlias($className, $assocName = '') { - return parent::_getSQLTableAlias($this->_class->rootEntityName); + return parent::_getSQLTableAlias($this->_class->rootEntityName, $assocName); } /** {@inheritdoc} */ diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 8ed9dbb54..2326ef5d1 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1914,6 +1914,7 @@ class UnitOfWork implements PropertyChangedListener if ($assoc['type'] & ClassMetadata::TO_ONE) { if ($assoc['isOwningSide']) { $associatedId = array(); + // TODO: Is this even computed right in all cases of composite keys? foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null; if ($joinColumnValue !== null) { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1050Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1050Test.php new file mode 100644 index 000000000..82e9590c0 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1050Test.php @@ -0,0 +1,37 @@ +markTestSkipped('performance skipped'); + $this->useModelSet('cms'); + parent::setUp(); + } + + public function testPerformance() + { + for ($i = 2; $i < 10000; ++$i) { + $user = new \Doctrine\Tests\Models\CMS\CmsUser(); + $user->status = 'developer'; + $user->username = 'jwage'+$i; + $user->name = 'Jonathan'; + $this->_em->persist($user); + } + $this->_em->flush(); + $this->_em->clear(); + + $s = microtime(true); + $users = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->findAll(); + $e = microtime(true); + echo __FUNCTION__ . " - " . ($e - $s) . " seconds" . PHP_EOL; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php index 1c7a3d171..d51bdd361 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php @@ -24,6 +24,7 @@ class DDC633Test extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-633 * @group DDC-952 + * @group DDC-914 */ public function testOneToOneEager() { @@ -39,9 +40,9 @@ class DDC633Test extends \Doctrine\Tests\OrmFunctionalTestCase $eagerAppointment = $this->_em->find(__NAMESPACE__ . '\DDC633Appointment', $app->id); - // Eager loading still produces proxies - $this->assertType('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); - $this->assertTrue($eagerAppointment->patient->__isInitialized__, "Proxy should already be initialized due to eager loading!"); + // Eager loading of one to one leads to fetch-join + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); + $this->assertTrue($this->_em->contains($eagerAppointment->patient)); } /** From 595c19207cdaa8c9444ebe4d79442d9e3d895f7e Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 9 Mar 2011 23:21:33 +0100 Subject: [PATCH 11/71] [DDC-914] Always fetch joining inverse side one-to-one associations breaks a ton of DDC-117 tests, investigate why to make this working also. --- lib/Doctrine/ORM/Persisters/BasicEntityPersister.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index d956fe240..125f73bb8 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -994,7 +994,7 @@ class BasicEntityPersister $columnList .= $assocColumnSQL; } - if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER && $assoc['type'] & ClassMetadata::TO_ONE) { + if ($assoc['type'] & ClassMetadata::TO_ONE && ($assoc['fetch'] == ClassMetadata::FETCH_EAGER/* || !$assoc['isOwningSide']*/)) { $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) { continue; // now this is why you shouldn't use inheritance @@ -1024,7 +1024,7 @@ class BasicEntityPersister $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias) . '.'.$targetCol.' '; } } else { - $eagerEntity = $this->_em->getClassMetadata($assoc['sourceEntity']); + $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']); $this->_selectJoinSql .= ' ' . $eagerEntity->table['name'] . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; From 8794d35867edfab21468d23d388ad231fb306499 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 9 Mar 2011 23:30:35 +0100 Subject: [PATCH 12/71] DDC-952 - Woah this still needs tons of tests. --- lib/Doctrine/ORM/UnitOfWork.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 2326ef5d1..3704be04e 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1901,6 +1901,8 @@ class UnitOfWork implements PropertyChangedListener } } + unset($this->eagerLoadingEntities[$class->name][$idHash]); + // Properly initialize any unfetched associations, if partial objects are not allowed. if ( ! isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { foreach ($class->associationMappings as $field => $assoc) { @@ -1947,7 +1949,7 @@ class UnitOfWork implements PropertyChangedListener $newValue instanceof Proxy && $newValue->__isInitialized__ === false) { - $this->eagerLoadingEntities[$assoc['targetEntity']][] = $relatedIdHash; + $this->eagerLoadingEntities[$assoc['targetEntity']][$relatedIdHash] = current($associatedId); } } else { if ($targetClass->subClasses) { @@ -1959,7 +1961,7 @@ class UnitOfWork implements PropertyChangedListener if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || isset($hints['fetchEager'][$class->name][$field])) { if (isset($hints['deferEagerLoad']) && !$targetClass->isIdentifierComposite) { // TODO: Is there a faster approach? - $this->eagerLoadingEntities[$assoc['targetEntity']][] = current($id); + $this->eagerLoadingEntities[$assoc['targetEntity']][$relatedIdHash] = current($associatedId); $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); } else { From 4677883acde3b356a6998a4186a53336956462a4 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Mar 2011 14:01:51 +0100 Subject: [PATCH 13/71] [DDC-952] Added modelset and tests for Eager Loading, detected a bug with inverse one-to-one eager fetching that needs to be addressed. --- .../Tests/ORM/Functional/AllTests.php | 1 + .../Functional/OneToOneEagerLoadingTest.php | 184 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index 319d5bb50..077ee4a7c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -35,6 +35,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\AdvancedDqlQueryTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToOneUnidirectionalAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToOneBidirectionalAssociationTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToOneEagerLoadingTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToManyBidirectionalAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToManyUnidirectionalAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ManyToManyBasicAssociationTest'); diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php new file mode 100644 index 000000000..6378cd24f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -0,0 +1,184 @@ +_em); + try { + $schemaTool->createSchema(array( + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\Train'), + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\TrainDriver'), + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\Waggon'), + )); + } catch(\Exception $e) {} + } + + public function testEagerLoadOneToOneOwningSide() + { + $train = new Train(); + $driver = new TrainDriver("Benjamin"); + $waggon = new Waggon(); + + $train->setDriver($driver); + $train->addWaggon($waggon); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $train = $this->_em->find(get_class($train), $train->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $train->driver); + $this->assertEquals("Benjamin", $train->driver->name); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } + + public function testEagerLoadOneToOneNullOwningSide() + { + $train = new Train(); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $train = $this->_em->find(get_class($train), $train->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $train->driver); + $this->assertNull($train->driver); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } + + public function testEagerLoadOneToOneInverseSide() + { + $train = new Train(); + $driver = new TrainDriver("Benjamin"); + $train->setDriver($driver); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $driver = $this->_em->find(get_class($driver), $driver->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $driver->train); + $this->assertNotNull($driver->train); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } + + public function testEagerLoadOneToOneNullInverseSide() + { + $driver = new TrainDriver("Benjamin"); + + $this->_em->persist($driver); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertNull($driver->train); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $driver = $this->_em->find(get_class($driver), $driver->id); + var_dump($this->_sqlLoggerStack->queries); // wrong table aliasing! + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $driver->train); + $this->assertNull($driver->train); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } +} + +/** + * @Entity + */ +class Train +{ + /** + * @id @column(type="integer") @generatedValue + * @var int + */ + public $id; + /** + * Owning side + * @OneToOne(targetEntity="TrainDriver", inversedBy="train", fetch="EAGER", cascade={"persist"}) + */ + public $driver; + /** + * @oneToMany(targetEntity="Waggon", mappedBy="train", cascade={"persist"}) + */ + public $waggons; + + public function __construct() + { + $this->waggons = new \Doctrine\Common\Collections\ArrayCollection(); + } + + public function setDriver(TrainDriver $driver) + { + $this->driver = $driver; + $driver->setTrain($this); + } + + public function addWaggon(Waggon $w) + { + $w->setTrain($this); + $this->waggons[] = $w; + } +} + +/** + * @Entity + */ +class TrainDriver +{ + /** @Id @Column(type="integer") @GeneratedValue */ + public $id; + /** @column(type="string") */ + public $name; + /** + * Inverse side + * @OneToOne(targetEntity="Train", mappedBy="driver", fetch="EAGER") + */ + public $train; + + public function __construct($name) + { + $this->name = $name; + } + + public function setTrain(Train $t) + { + $this->train = $t; + } +} + +/** + * @Entity + */ +class Waggon +{ + /** @id @generatedValue @column(type="integer") */ + public $id; + /** @ManyToOne(targetEntity="Train", inversedBy="waggons") */ + public $train; + + public function setTrain($train) + { + $this->train = $train; + } +} \ No newline at end of file From 7c7106b1c14e267bf5f24a47f90c0587ea190a8f Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Mar 2011 19:11:37 +0100 Subject: [PATCH 14/71] DDC-952 - Fix bug in inverse one-to-one eager loading sql code. --- .../ORM/Persisters/BasicEntityPersister.php | 2 +- .../Functional/OneToOneEagerLoadingTest.php | 20 ++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 125f73bb8..aed43d245 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -1031,7 +1031,7 @@ class BasicEntityPersister foreach ($owningAssoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { $this->_selectJoinSql .= $this->_getSQLTableAlias($owningAssoc['sourceEntity'], $assocAlias) . '.'.$sourceCol.' = ' . - $this->_getSQLTableAlias($assoc['targetEntity']) . '.' . $targetCol . ' '; + $this->_getSQLTableAlias($owningAssoc['targetEntity']) . '.' . $targetCol . ' '; } } } diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index 6378cd24f..044a17381 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -84,7 +84,7 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testEagerLoadOneToOneNullInverseSide() { - $driver = new TrainDriver("Benjamin"); + $driver = new TrainDriver("Dagny Taggert"); $this->_em->persist($driver); $this->_em->flush(); @@ -95,12 +95,26 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $sqlCount = count($this->_sqlLoggerStack->queries); $driver = $this->_em->find(get_class($driver), $driver->id); - var_dump($this->_sqlLoggerStack->queries); // wrong table aliasing! $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $driver->train); $this->assertNull($driver->train); $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); } + + public function testEagerLoadManyToOne() + { + $train = new Train(); + $waggon = new Waggon(); + $train->addWaggon($waggon); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $waggon = $this->_em->find(get_class($waggon), $waggon->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $waggon->train); + $this->assertNotNull($waggon->train); + } } /** @@ -174,7 +188,7 @@ class Waggon { /** @id @generatedValue @column(type="integer") */ public $id; - /** @ManyToOne(targetEntity="Train", inversedBy="waggons") */ + /** @ManyToOne(targetEntity="Train", inversedBy="waggons", fetch="EAGER") */ public $train; public function setTrain($train) From 1bc4b62805fc56c0ec83d665ade0145820e33ead Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 13 Mar 2011 00:15:50 +0100 Subject: [PATCH 15/71] [DDC-952] Make collection loading work with hydrators also. --- .../ORM/Internal/Hydration/ObjectHydrator.php | 8 +++ .../ORM/Persisters/BasicEntityPersister.php | 53 +++++++++---------- .../ORM/Functional/EntityRepositoryTest.php | 4 +- .../Functional/ExtraLazyCollectionTest.php | 4 +- 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index b57d7ed10..a820832f5 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -404,6 +404,10 @@ class ObjectHydrator extends AbstractHydrator $result[$key] = $element; $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $key; } + + if (isset($this->_hints['collection'])) { + $this->_hints['collection']->hydrateSet($key, $element); + } } else { if ($this->_rsm->isMixed) { $element = array(0 => $element); @@ -411,6 +415,10 @@ class ObjectHydrator extends AbstractHydrator $result[] = $element; $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; ++$this->_resultCounter; + + if (isset($this->_hints['collection'])) { + $this->_hints['collection']->hydrateAdd($element); + } } // Update result pointer diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index aed43d245..6ee526ca9 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -721,19 +721,17 @@ class BasicEntityPersister */ private function loadArrayFromStatement($assoc, $stmt) { - $entities = array(); + $hints = array('deferEagerLoads' => true); + if (isset($assoc['indexBy'])) { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entity = $this->_createEntity($result); - $entities[$this->_class->reflFields[$assoc['indexBy']]->getValue($entity)] = $entity; - } + $rsm = clone ($this->_rsm); // this is necessary because the "default rsm" should be changed. + $rsm->addIndexBy('r', $assoc['indexBy']); } else { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entities[] = $this->_createEntity($result); - } + $rsm = $this->_rsm; } - $stmt->closeCursor(); - return $entities; + + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + return $hydrator->hydrateAll($stmt, $rsm, $hints); } /** @@ -745,20 +743,17 @@ class BasicEntityPersister */ private function loadCollectionFromStatement($assoc, $stmt, $coll) { - $hints = array('deferEagerLoads' => true); + $hints = array('deferEagerLoads' => true, 'collection' => $coll); + if (isset($assoc['indexBy'])) { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entity = $this->_createEntity($result, null, $hints); - $coll->hydrateSet($this->_class->reflFields[$assoc['indexBy']]->getValue($entity), $entity); - } + $rsm = clone ($this->_rsm); // this is necessary because the "default rsm" should be changed. + $rsm->addIndexBy('r', $assoc['indexBy']); } else { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $coll->hydrateAdd($this->_createEntity($result, null, $hints)); - } + $rsm = $this->_rsm; } - $stmt->closeCursor(); - - $this->_em->getUnitOfWork()->triggerEagerLoads(); + + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + $hydrator->hydrateAll($stmt, $rsm, $hints); } /** @@ -994,13 +989,13 @@ class BasicEntityPersister $columnList .= $assocColumnSQL; } - if ($assoc['type'] & ClassMetadata::TO_ONE && ($assoc['fetch'] == ClassMetadata::FETCH_EAGER/* || !$assoc['isOwningSide']*/)) { + if ($assoc['type'] & ClassMetadata::TO_ONE && ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || !$assoc['isOwningSide'])) { $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) { continue; // now this is why you shouldn't use inheritance } - $assocAlias = 'e' + ($eagerAliasCounter++); + $assocAlias = 'e' . ($eagerAliasCounter++); $this->_rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField); foreach ($eagerEntity->fieldNames AS $field) { @@ -1009,13 +1004,13 @@ class BasicEntityPersister } foreach ($eagerEntity->associationMappings as $assoc2Field => $assoc2) { - $assoc2ColumnSQL = $this->_getSelectColumnAssociationSQL($assoc2Field, $assoc2, $eagerEntity); + $assoc2ColumnSQL = $this->_getSelectColumnAssociationSQL($assoc2Field, $assoc2, $eagerEntity, $assocAlias); if ($assoc2ColumnSQL) { if ($columnList) $columnList .= ', '; $columnList .= $assoc2ColumnSQL; } } - $this->_selectJoinSql .= ' LEFT JOIN'; + $this->_selectJoinSql .= ' LEFT JOIN'; // TODO: Inner join when all join columns are NOT nullable. if ($assoc['isOwningSide']) { $this->_selectJoinSql .= ' ' . $eagerEntity->table['name'] . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; @@ -1042,7 +1037,7 @@ class BasicEntityPersister return $this->_selectColumnListSql; } - protected function _getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class) + protected function _getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r') { $columnList = ''; if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { @@ -1050,12 +1045,12 @@ class BasicEntityPersister if ($columnList) $columnList .= ', '; $columnAlias = $srcColumn . $this->_sqlAliasCounter++; - $columnList .= $this->_getSQLTableAlias($class->name) . ".$srcColumn AS $columnAlias"; + $columnList .= $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) ) . ".$srcColumn AS $columnAlias"; $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); if ( ! isset($this->_resultColumnNames[$resultColumnName])) { $this->_resultColumnNames[$resultColumnName] = $srcColumn; } - $this->_rsm->addMetaResult('r', $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); + $this->_rsm->addMetaResult($alias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); } } return $columnList; @@ -1195,8 +1190,8 @@ class BasicEntityPersister return $this->_sqlTableAliases[$className]; } $tableAlias = 't' . $this->_sqlAliasCounter++; - $this->_sqlTableAliases[$className] = $tableAlias; + $this->_sqlTableAliases[$className] = $tableAlias; return $tableAlias; } diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index ebe2c19f2..778311b59 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -172,10 +172,10 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $userId = $user->id; - $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId); + $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $userId); $this->setExpectedException('Doctrine\ORM\OptimisticLockException'); - $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId, \Doctrine\DBAL\LockMode::OPTIMISTIC); + $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $userId, \Doctrine\DBAL\LockMode::OPTIMISTIC); } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index 031061b84..4bf010602 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -196,8 +196,8 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $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()); + // +2 queries executed by slice + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount(), "Slicing two parts should only execute two additional queries."); } /** From 38ad25ad4c967b54928f3a470f8f237bbbd3e0c9 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 13 Mar 2011 00:23:46 +0100 Subject: [PATCH 16/71] [DDC-952] Remove all the unnecessary hydration code from all Persisters. --- .../AbstractEntityInheritancePersister.php | 57 +------------ .../ORM/Persisters/BasicEntityPersister.php | 82 +------------------ .../Persisters/JoinedSubclassPersister.php | 2 +- .../ORM/Persisters/SingleTablePersister.php | 2 +- 4 files changed, 4 insertions(+), 139 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php index da50b3be8..425da798a 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php @@ -28,24 +28,11 @@ use Doctrine\ORM\Mapping\ClassMetadata, * types in the hierarchy. * * @author Roman Borschel + * @author Benjamin Eberlei * @since 2.0 */ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister { - /** - * Map from column names to class metadata instances that declare the field the column is mapped to. - * - * @var array - */ - private $declaringClassMap = array(); - - /** - * Map from column names to class names that declare the field the association with join column is mapped to. - * - * @var array - */ - private $declaringJoinColumnMap = array(); - /** * {@inheritdoc} */ @@ -66,40 +53,6 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister */ abstract protected function _getDiscriminatorColumnTableName(); - /** - * {@inheritdoc} - */ - protected function _processSQLResult(array $sqlResult) - { - $data = array(); - $discrColumnName = $this->_platform->getSQLResultCasing($this->_class->discriminatorColumn['name']); - $entityName = $this->_class->discriminatorMap[$sqlResult[$discrColumnName]]; - unset($sqlResult[$discrColumnName]); - foreach ($sqlResult as $column => $value) { - $realColumnName = $this->_resultColumnNames[$column]; - if (isset($this->declaringClassMap[$column])) { - $class = $this->declaringClassMap[$column]; - if ($class->name == $entityName || is_subclass_of($entityName, $class->name)) { - $field = $class->fieldNames[$realColumnName]; - if (isset($data[$field])) { - $data[$realColumnName] = $value; - } else { - $data[$field] = Type::getType($class->fieldMappings[$field]['type']) - ->convertToPHPValue($value, $this->_platform); - } - } - } else if (isset($this->declaringJoinColumnMap[$column])) { - if ($this->declaringJoinColumnMap[$column] == $entityName || is_subclass_of($entityName, $this->declaringJoinColumnMap[$column])) { - $data[$realColumnName] = $value; - } - } else { - $data[$realColumnName] = $value; - } - } - - return array($entityName, $data); - } - /** * {@inheritdoc} */ @@ -108,10 +61,6 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister $columnName = $class->columnNames[$field]; $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); - if ( ! isset($this->_resultColumnNames[$columnAlias])) { - $this->_resultColumnNames[$columnAlias] = $columnName; - $this->declaringClassMap[$columnAlias] = $class; - } $this->_rsm->addFieldResult($alias, $columnAlias, $field, $class->name); return "$sql AS $columnAlias"; @@ -121,10 +70,6 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister { $columnAlias = $joinColumnName . $this->_sqlAliasCounter++; $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); - if ( ! isset($this->_resultColumnNames[$resultColumnName])) { - $this->_resultColumnNames[$resultColumnName] = $joinColumnName; - $this->declaringJoinColumnMap[$resultColumnName] = $className; - } $this->_rsm->addMetaResult('r', $resultColumnName, $joinColumnName); return $tableAlias . ".$joinColumnName AS $columnAlias"; diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 6ee526ca9..016184679 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -70,6 +70,7 @@ use PDO, * * @author Roman Borschel * @author Giorgio Sironi + * @author Benjamin Eberlei * @since 2.0 */ class BasicEntityPersister @@ -108,15 +109,6 @@ class BasicEntityPersister * @var array */ protected $_queuedInserts = array(); - - /** - * Case-sensitive mappings of column names as they appear in an SQL result set - * to column names as they are defined in the mapping. This is necessary because different - * RDBMS vendors return column names in result sets in different casings. - * - * @var array - */ - protected $_resultColumnNames = array(); /** * ResultSetMapping that is used for all queries. Is generated lazily once per request. @@ -821,72 +813,6 @@ class BasicEntityPersister return $this->_conn->executeQuery($sql, $params, $types); } - /** - * Creates or fills a single entity object from an SQL result. - * - * @param $result The SQL result. - * @param object $entity The entity object to fill, if any. - * @param array $hints Hints for entity creation. - * @return object The filled and managed entity object or NULL, if the SQL result is empty. - */ - private function _createEntity($result, $entity = null, array $hints = array()) - { - if ($result === false) { - return null; - } - - list($entityName, $data) = $this->_processSQLResult($result); - - if ($entity !== null) { - $hints[Query::HINT_REFRESH] = true; - $id = array(); - if ($this->_class->isIdentifierComposite) { - foreach ($this->_class->identifier as $fieldName) { - $id[$fieldName] = $data[$fieldName]; - } - } else { - $id = array($this->_class->identifier[0] => $data[$this->_class->identifier[0]]); - } - $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data); - } - - return $this->_em->getUnitOfWork()->createEntity($entityName, $data, $hints); - } - - /** - * Processes an SQL result set row that contains data for an entity of the type - * this persister is responsible for. - * - * Subclasses are supposed to override this method if they need to change the - * hydration procedure for entities loaded through basic find operations or - * lazy-loading (not DQL). - * - * @param array $sqlResult The SQL result set row to process. - * @return array A tuple where the first value is the actual type of the entity and - * the second value the prepared data of the entity (a map from field - * names to values). - */ - protected function _processSQLResult(array $sqlResult) - { - $data = array(); - foreach ($sqlResult as $column => $value) { - $column = $this->_resultColumnNames[$column]; - if (isset($this->_class->fieldNames[$column])) { - $field = $this->_class->fieldNames[$column]; - if (isset($data[$field])) { - $data[$column] = $value; - } else { - $data[$field] = Type::getType($this->_class->fieldMappings[$field]['type']) - ->convertToPHPValue($value, $this->_platform); - } - } else { - $data[$column] = $value; - } - } - - return array($this->_class->name, $data); - } - /** * Gets the SELECT SQL to select one or more entities by a set of field criteria. * @@ -1047,9 +973,6 @@ class BasicEntityPersister $columnAlias = $srcColumn . $this->_sqlAliasCounter++; $columnList .= $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) ) . ".$srcColumn AS $columnAlias"; $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); - if ( ! isset($this->_resultColumnNames[$resultColumnName])) { - $this->_resultColumnNames[$resultColumnName] = $srcColumn; - } $this->_rsm->addMetaResult($alias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); } } @@ -1165,9 +1088,6 @@ class BasicEntityPersister $columnName = $class->columnNames[$field]; $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); - if ( ! isset($this->_resultColumnNames[$columnAlias])) { - $this->_resultColumnNames[$columnAlias] = $columnName; - } $this->_rsm->addFieldResult($alias, $columnAlias, $field); return "$sql AS $columnAlias"; diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 918f7e1ef..2490085cf 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -29,6 +29,7 @@ use Doctrine\ORM\ORMException, * database as it is defined by the Class Table Inheritance strategy. * * @author Roman Borschel + * @author Benjamin Eberlei * @since 2.0 * @see http://martinfowler.com/eaaCatalog/classTableInheritance.html */ @@ -283,7 +284,6 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister } $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn); - $this->_resultColumnNames[$resultColumnName] = $discrColumn; $this->_rsm->setDiscriminatorColumn('r', $discrColumn); $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn); } diff --git a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php index 94450fd89..bf9104580 100644 --- a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php +++ b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php @@ -26,6 +26,7 @@ use Doctrine\ORM\Mapping\ClassMetadata; * SINGLE_TABLE strategy. * * @author Roman Borschel + * @author Benjamin Eberlei * @since 2.0 * @link http://martinfowler.com/eaaCatalog/singleTableInheritance.html */ @@ -48,7 +49,6 @@ class SingleTablePersister extends AbstractEntityInheritancePersister $rootClass = $this->_em->getClassMetadata($this->_class->rootEntityName); $tableAlias = $this->_getSQLTableAlias($rootClass->name); $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn); - $this->_resultColumnNames[$resultColumnName] = $discrColumn; $this->_rsm->setDiscriminatorColumn('r', $discrColumn); $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn); From 180078d0f6fa97bf5f3398e8c5d2f56030236c33 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Mon, 14 Mar 2011 01:04:50 -0300 Subject: [PATCH 17/71] Added namedQueries as optional during serialization of ClassMetadata. --- lib/Doctrine/ORM/Mapping/ClassMetadata.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index a8a8828f6..222b38877 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -275,7 +275,6 @@ class ClassMetadata extends ClassMetadataInfo { // This metadata is always serialized/cached. $serialized = array( - 'namedQueries', 'associationMappings', 'columnNames', //TODO: Not really needed. Can use fieldMappings[$fieldName]['columnName'] 'fieldMappings', @@ -331,6 +330,10 @@ class ClassMetadata extends ClassMetadataInfo $serialized[] = 'lifecycleCallbacks'; } + if ($this->namedQueries) { + $serialized[] = 'namedQueries'; + } + return $serialized; } From 234d2e5f0f7313790e9717173511257b88a9cd85 Mon Sep 17 00:00:00 2001 From: Albert Jessurum Date: Tue, 15 Mar 2011 12:22:53 +0100 Subject: [PATCH 18/71] Fix typo on schema help messages --- .../ORM/Tools/Console/Command/SchemaTool/CreateCommand.php | 2 +- .../ORM/Tools/Console/Command/SchemaTool/DropCommand.php | 2 +- .../ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php index e18a9c56a..835835d28 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php @@ -65,7 +65,7 @@ EOT protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas) { - $output->write('ATTENTION: This operation should not be executed in an production enviroment.' . PHP_EOL . PHP_EOL); + $output->write('ATTENTION: This operation should not be executed in a production enviroment.' . PHP_EOL . PHP_EOL); if ($input->getOption('dump-sql') === true) { $sqls = $schemaTool->getCreateSchemaSql($metadatas); diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php index 82d91f4c6..f6f16cb79 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php @@ -92,7 +92,7 @@ EOT } $output->write('Database schema dropped successfully!' . PHP_EOL); } else { - $output->write('ATTENTION: This operation should not be executed in an production enviroment.' . PHP_EOL . PHP_EOL); + $output->write('ATTENTION: This operation should not be executed in a production enviroment.' . PHP_EOL . PHP_EOL); if ($isFullDatabaseDrop) { $sqls = $schemaTool->getDropDatabaseSQL(); diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php index f1a3a73cf..ed12358cd 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php @@ -86,7 +86,7 @@ EOT $schemaTool->updateSchema($metadatas, $saveMode); $output->write('Database schema updated successfully!' . PHP_EOL); } else { - $output->write('ATTENTION: This operation should not be executed in an production enviroment.' . PHP_EOL); + $output->write('ATTENTION: This operation should not be executed in a production enviroment.' . PHP_EOL); $output->write('Use the incremental update to detect changes during development and use' . PHP_EOL); $output->write('this SQL DDL to manually update your database in production.' . PHP_EOL . PHP_EOL); From a04ba4487469d890ea20063b5b6911cc780eccd4 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 15 Mar 2011 19:48:04 +0100 Subject: [PATCH 19/71] [DDC-952] Introduced SimpleObjectHydrator again for performance reasons. --- lib/Doctrine/ORM/AbstractQuery.php | 5 + lib/Doctrine/ORM/EntityManager.php | 3 + .../Hydration/SimpleObjectHydrator.php | 126 ++++++++++++++++++ .../ORM/Persisters/BasicEntityPersister.php | 6 +- 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index b1b24230b..677de4472 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -57,6 +57,11 @@ abstract class AbstractQuery */ const HYDRATE_SINGLE_SCALAR = 4; + /** + * Very simple object hydrator (optimized for performance). + */ + const HYDRATE_SIMPLEOBJECT = 5; + /** * @var array The parameter map of this query. */ diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 2839099fc..43b257788 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -679,6 +679,9 @@ class EntityManager implements ObjectManager case Query::HYDRATE_SINGLE_SCALAR: $hydrator = new Internal\Hydration\SingleScalarHydrator($this); break; + case Query::HYDRATE_SIMPLEOBJECT: + $hydrator = new Internal\Hydration\SimpleObjectHydrator($this); + break; default: if ($class = $this->config->getCustomHydrationMode($hydrationMode)) { $hydrator = new $class($this); diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php new file mode 100644 index 000000000..68cdc3653 --- /dev/null +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -0,0 +1,126 @@ +. + */ + + +namespace Doctrine\ORM\Internal\Hydration; + +use \PDO; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\DBAL\Types\Type; + +class SimpleObjectHydrator extends AbstractHydrator +{ + const REFRESH_ENTITY = 'doctrine_refresh_entity'; + + /** + * @var ClassMetadata + */ + private $class; + + protected function _hydrateAll() + { + $result = array(); + $cache = array(); + + while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { + $this->_hydrateRow($row, $cache, $result); + } + + $this->_em->getUnitOfWork()->triggerEagerLoads(); + + return $result; + } + + protected function _prepare() + { + if (count($this->_rsm->aliasMap) == 1) { + $this->class = $this->_em->getClassMetadata(current($this->_rsm->aliasMap)); + } else { + throw new \RuntimeException("Cannot use SimpleObjectHydrator with a ResultSetMapping not containing exactly one object result."); + } + if ($this->_rsm->scalarMappings) { + throw new \RuntimeException("Cannot use SimpleObjectHydrator with a ResultSetMapping that contains scalar mappings."); + } + } + + protected function _hydrateRow(array $sqlResult, array &$cache, array &$result) + { + $data = array(); + if ($this->class->inheritanceType == ClassMetadata::INHERITANCE_TYPE_NONE) { + foreach ($sqlResult as $column => $value) { + + if (isset($this->_rsm->fieldMappings[$column])) { + $column = $this->_rsm->fieldMappings[$column]; + $field = $this->class->fieldNames[$column]; + if (isset($data[$field])) { + $data[$column] = $value; + } else { + $data[$field] = Type::getType($this->class->fieldMappings[$field]['type']) + ->convertToPHPValue($value, $this->_platform); + } + } else { + $column = $this->_rsm->metaMappings[$column]; + $data[$column] = $value; + } + } + $entityName = $this->class->name; + } else { + $discrColumnName = $this->_platform->getSQLResultCasing($this->class->discriminatorColumn['name']); + $entityName = $this->class->discriminatorMap[$sqlResult[$discrColumnName]]; + unset($sqlResult[$discrColumnName]); + foreach ($sqlResult as $column => $value) { + if (isset($this->_rsm->fieldMappings[$column])) { + $realColumnName = $this->_rsm->fieldMappings[$column]; + $class = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$column]); + if ($class->name == $entityName || is_subclass_of($entityName, $class->name)) { + $field = $class->fieldNames[$realColumnName]; + if (isset($data[$field])) { + $data[$realColumnName] = $value; + } else { + $data[$field] = Type::getType($class->fieldMappings[$field]['type']) + ->convertToPHPValue($value, $this->_platform); + } + } + } else if (isset($this->_rsm->relationMap[$column])) { + if ($this->_rsm->relationMap[$column] == $entityName || is_subclass_of($entityName, $this->_rsm->relationMap[$column])) { + $data[$realColumnName] = $value; + } + } else { + $column = $this->_rsm->metaMappings[$column]; + $data[$realColumnName] = $value; + } + } + } + + if (isset($this->_hints[self::REFRESH_ENTITY])) { + $this->_hints[Query::HINT_REFRESH] = true; + $id = array(); + if ($this->_class->isIdentifierComposite) { + foreach ($this->_class->identifier as $fieldName) { + $id[$fieldName] = $data[$fieldName]; + } + } else { + $id = array($this->_class->identifier[0] => $data[$this->_class->identifier[0]]); + } + $this->_em->getUnitOfWork()->registerManaged($this->_hints[self::REFRESH_ENTITY], $id, $data); + } + + $result[] = $this->_em->getUnitOfWork()->createEntity($entityName, $data, $this->_hints); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 016184679..9671f945a 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -685,7 +685,11 @@ class BasicEntityPersister list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); - $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + if ($this->_selectJoinSql) { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + } else { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_SIMPLEOBJECT); + } return $hydrator->hydrateAll($stmt, $this->_rsm, array('deferEagerLoads' => true)); } From b3c01903b4f719714047342f43b590d9276b927a Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 15 Mar 2011 20:03:05 +0100 Subject: [PATCH 20/71] DDC-952 - Optimization --- .../ORM/Internal/Hydration/SimpleObjectHydrator.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php index 68cdc3653..eb6717b30 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -33,6 +33,8 @@ class SimpleObjectHydrator extends AbstractHydrator */ private $class; + private $declaringClasses = array(); + protected function _hydrateAll() { $result = array(); @@ -51,6 +53,11 @@ class SimpleObjectHydrator extends AbstractHydrator { if (count($this->_rsm->aliasMap) == 1) { $this->class = $this->_em->getClassMetadata(current($this->_rsm->aliasMap)); + if ($this->class->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { + foreach ($this->_rsm->declaringClasses AS $column => $class) { + $this->declaringClasses[$column] = $this->_em->getClassMetadata($class); + } + } } else { throw new \RuntimeException("Cannot use SimpleObjectHydrator with a ResultSetMapping not containing exactly one object result."); } @@ -87,7 +94,7 @@ class SimpleObjectHydrator extends AbstractHydrator foreach ($sqlResult as $column => $value) { if (isset($this->_rsm->fieldMappings[$column])) { $realColumnName = $this->_rsm->fieldMappings[$column]; - $class = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$column]); + $class = $this->declaringClasses[$column]; if ($class->name == $entityName || is_subclass_of($entityName, $class->name)) { $field = $class->fieldNames[$realColumnName]; if (isset($data[$field])) { From 1b46208aa5d57993601b70a2f53aa78da8739520 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 15 Mar 2011 21:34:47 +0100 Subject: [PATCH 21/71] [DDC-952] More fixes --- .../Hydration/SimpleObjectHydrator.php | 62 ++++++++++--------- .../ORM/Persisters/BasicEntityPersister.php | 8 ++- .../Tests/ORM/Functional/TypeTest.php | 2 +- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php index eb6717b30..54b39d90f 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -52,7 +52,7 @@ class SimpleObjectHydrator extends AbstractHydrator protected function _prepare() { if (count($this->_rsm->aliasMap) == 1) { - $this->class = $this->_em->getClassMetadata(current($this->_rsm->aliasMap)); + $this->class = $this->_em->getClassMetadata(reset($this->_rsm->aliasMap)); if ($this->class->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { foreach ($this->_rsm->declaringClasses AS $column => $class) { $this->declaringClasses[$column] = $this->_em->getClassMetadata($class); @@ -72,19 +72,20 @@ class SimpleObjectHydrator extends AbstractHydrator if ($this->class->inheritanceType == ClassMetadata::INHERITANCE_TYPE_NONE) { foreach ($sqlResult as $column => $value) { - if (isset($this->_rsm->fieldMappings[$column])) { - $column = $this->_rsm->fieldMappings[$column]; - $field = $this->class->fieldNames[$column]; - if (isset($data[$field])) { - $data[$column] = $value; + if (!isset($cache[$column])) { + if (isset($this->_rsm->fieldMappings[$column])) { + $cache[$column]['name'] = $this->_rsm->fieldMappings[$column]; + $cache[$column]['field'] = true; } else { - $data[$field] = Type::getType($this->class->fieldMappings[$field]['type']) - ->convertToPHPValue($value, $this->_platform); + $cache[$column]['name'] = $this->_rsm->metaMappings[$column]; } - } else { - $column = $this->_rsm->metaMappings[$column]; - $data[$column] = $value; } + + if (isset($cache[$column]['field'])) { + $value = Type::getType($this->class->fieldMappings[$cache[$column]['name']]['type']) + ->convertToPHPValue($value, $this->_platform); + } + $data[$cache[$column]['name']] = $value; } $entityName = $this->class->name; } else { @@ -92,25 +93,30 @@ class SimpleObjectHydrator extends AbstractHydrator $entityName = $this->class->discriminatorMap[$sqlResult[$discrColumnName]]; unset($sqlResult[$discrColumnName]); foreach ($sqlResult as $column => $value) { - if (isset($this->_rsm->fieldMappings[$column])) { - $realColumnName = $this->_rsm->fieldMappings[$column]; - $class = $this->declaringClasses[$column]; - if ($class->name == $entityName || is_subclass_of($entityName, $class->name)) { - $field = $class->fieldNames[$realColumnName]; - if (isset($data[$field])) { - $data[$realColumnName] = $value; - } else { - $data[$field] = Type::getType($class->fieldMappings[$field]['type']) - ->convertToPHPValue($value, $this->_platform); + if (!isset($cache[$column])) { + if (isset($this->_rsm->fieldMappings[$column])) { + $field = $this->_rsm->fieldMappings[$column]; + $class = $this->declaringClasses[$column]; + if ($class->name == $entityName || is_subclass_of($entityName, $class->name)) { + $cache[$column]['name'] = $field; + $cache[$column]['class'] = $class; } + } else if (isset($this->_rsm->relationMap[$column])) { + if ($this->_rsm->relationMap[$column] == $entityName || is_subclass_of($entityName, $this->_rsm->relationMap[$column])) { + $cache[$column]['name'] = $field; + } + } else { + $cache[$column]['name'] = $this->_rsm->metaMappings[$column]; } - } else if (isset($this->_rsm->relationMap[$column])) { - if ($this->_rsm->relationMap[$column] == $entityName || is_subclass_of($entityName, $this->_rsm->relationMap[$column])) { - $data[$realColumnName] = $value; - } - } else { - $column = $this->_rsm->metaMappings[$column]; - $data[$realColumnName] = $value; + } + + if (isset($cache[$column]['class'])) { + $value = Type::getType($cache[$column]['class']->fieldMappings[$cache[$column]['name']]['type']) + ->convertToPHPValue($value, $this->_platform); + } + + if (isset($cache[$column])) { + $data[$cache[$column]['name']] = $value; } } } diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 9671f945a..6999f85ce 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -571,8 +571,12 @@ class BasicEntityPersister if ($entity !== null) { $hints[Query::HINT_REFRESH] = true; } - - $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + + if ($this->_selectJoinSql) { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + } else { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_SIMPLEOBJECT); + } $entities = $hydrator->hydrateAll($stmt, $this->_rsm, $hints); return $entities ? $entities[0] : null; } diff --git a/tests/Doctrine/Tests/ORM/Functional/TypeTest.php b/tests/Doctrine/Tests/ORM/Functional/TypeTest.php index 8ecee7ac7..a2a738b56 100644 --- a/tests/Doctrine/Tests/ORM/Functional/TypeTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/TypeTest.php @@ -119,7 +119,7 @@ class TypeTest extends \Doctrine\Tests\OrmFunctionalTestCase $dateTimeDb = $this->_em->find('Doctrine\Tests\Models\Generic\DateTimeModel', $dateTime->id); - $this->assertInstanceOf('DateTime', $dateTime->datetime); + $this->assertInstanceOf('DateTime', $dateTimeDb->datetime); $this->assertEquals('2009-10-02 20:10:52', $dateTimeDb->datetime->format('Y-m-d H:i:s')); } From 6d27b4760fa2a9f14d035f022201cb35c0cbe13d Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 15 Mar 2011 23:22:37 +0100 Subject: [PATCH 22/71] [DDC-952] Add Persister hydration performance tests. --- .../InheritancePersisterPerformanceTest.php | 63 ++++++++++ .../Performance/PersisterPerformanceTest.php | 118 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Performance/InheritancePersisterPerformanceTest.php create mode 100644 tests/Doctrine/Tests/ORM/Performance/PersisterPerformanceTest.php diff --git a/tests/Doctrine/Tests/ORM/Performance/InheritancePersisterPerformanceTest.php b/tests/Doctrine/Tests/ORM/Performance/InheritancePersisterPerformanceTest.php new file mode 100644 index 000000000..01113f21b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Performance/InheritancePersisterPerformanceTest.php @@ -0,0 +1,63 @@ +useModelSet('company'); + parent::setUp(); + } + + public function testCompanyContract() + { + $person = new \Doctrine\Tests\Models\Company\CompanyEmployee(); + $person->setName('Poor Sales Guy'); + $person->setDepartment('Sales'); + $person->setSalary(100); + $this->_em->persist($person); + + for ($i = 0; $i < 33; $i++) { + $fix = new \Doctrine\Tests\Models\Company\CompanyFixContract(); + $fix->setFixPrice(1000); + $fix->setSalesPerson($person); + $fix->markCompleted(); + $this->_em->persist($fix); + + $flex = new \Doctrine\Tests\Models\Company\CompanyFlexContract(); + $flex->setSalesPerson($person); + $flex->setHoursWorked(100); + $flex->setPricePerHour(100); + $flex->markCompleted(); + $this->_em->persist($flex); + + $ultra = new \Doctrine\Tests\Models\Company\CompanyFlexUltraContract(); + $ultra->setSalesPerson($person); + $ultra->setHoursWorked(150); + $ultra->setPricePerHour(150); + $ultra->setMaxPrice(7000); + $this->_em->persist($ultra); + } + + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $contracts = $this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyContract')->findAll(); + echo "99 CompanyContract: " . number_format(microtime(true) - $start, 6) . "\n"; + $this->assertEquals(99, count($contracts)); + + $this->_em->clear(); + + $start = microtime(true); + $contracts = $this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyContract')->findAll(); + echo "99 CompanyContract: " . number_format(microtime(true) - $start, 6) . "\n"; + $this->assertEquals(99, count($contracts)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Performance/PersisterPerformanceTest.php b/tests/Doctrine/Tests/ORM/Performance/PersisterPerformanceTest.php new file mode 100644 index 000000000..bbd4f445f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Performance/PersisterPerformanceTest.php @@ -0,0 +1,118 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testFindCmsArticle() + { + $author = new CmsUser(); + $author->name = "beberlei"; + $author->status = "active"; + $author->username = "beberlei"; + $this->_em->persist($author); + + $ids = array(); + for ($i = 0; $i < 100; $i++) { + $article = new CmsArticle(); + $article->text = "foo"; + $article->topic = "bar"; + $article->user = $author; + $this->_em->persist($article); + $ids[] = $article; + } + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->findAll(); + echo "100 CmsArticle findAll(): " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->findAll(); + echo "100 CmsArticle findAll(): " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + for ($i = 0; $i < 100; $i++) { + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->find($ids[$i]->id); + } + echo "100 CmsArticle find(): " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + for ($i = 0; $i < 100; $i++) { + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->find($ids[$i]->id); + } + echo "100 CmsArticle find(): " . number_format(microtime(true) - $start, 6) . "\n"; + } + + public function testFindCmsGroup() + { + for ($i = 0; $i < 100; $i++) { + $group = new CmsGroup(); + $group->name = "foo" . $i; + $this->_em->persist($group); + } + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsGroup')->findAll(); + echo "100 CmsGroup: " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsGroup')->findAll(); + echo "100 CmsGroup: " . number_format(microtime(true) - $start, 6) . "\n"; + } + + public function testFindCmsUser() + { + for ($i = 0; $i < 100; $i++) { + $user = new CmsUser(); + $user->name = "beberlei"; + $user->status = "active"; + $user->username = "beberlei".$i; + $this->_em->persist($user); + } + + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->findAll(); + echo "100 CmsUser: " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->findAll(); + echo "100 CmsUser: " . number_format(microtime(true) - $start, 6) . "\n"; + } +} + + + From b7e522d7a73a182ad2137d761cfd13650a8aa86d Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 15 Mar 2011 23:39:19 +0100 Subject: [PATCH 23/71] DDC-952 - This nasty inheritance hydration bug slipped in again, fixed again now. --- lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php index 54b39d90f..3524f89e9 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -115,7 +115,9 @@ class SimpleObjectHydrator extends AbstractHydrator ->convertToPHPValue($value, $this->_platform); } - if (isset($cache[$column])) { + // the second and part is to prevent overwrites in case of multiple + // inheritance classes using the same property name (See AbstractHydrator) + if (isset($cache[$column]) && (!isset($data[$cache[$column]['name']]) || $value !== null)) { $data[$cache[$column]['name']] = $value; } } From 4b98e3ea8e3b31febfcdfea178f45c4e062f155b Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 16 Mar 2011 00:03:43 +0100 Subject: [PATCH 24/71] DDC-952 - Remove unnecessary instance variable and comment on one feature. --- lib/Doctrine/ORM/UnitOfWork.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 3704be04e..b40c1c3b9 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -225,13 +225,6 @@ class UnitOfWork implements PropertyChangedListener */ private $eagerLoadingEntities = array(); - /** - * Map of Collections that should be eager loaded when requested. - * - * @var array - */ - private $eagerLoadingCollections = array(); - /** * Initializes a new UnitOfWork instance, bound to the given EntityManager. * @@ -1900,7 +1893,8 @@ class UnitOfWork implements PropertyChangedListener $class->reflFields[$field]->setValue($entity, $value); } } - + + // Loading the entity right here, if its in the eager loading map get rid of it there. unset($this->eagerLoadingEntities[$class->name][$idHash]); // Properly initialize any unfetched associations, if partial objects are not allowed. From 5192306d39757b2e765692df7bf5356dc235b935 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 16 Mar 2011 22:51:32 +0100 Subject: [PATCH 25/71] [DDC-952] One last commit with some refactorings, additional comments and two new tests. Also added convenience method Query::setFetchMode($className, $assocName) --- lib/Doctrine/ORM/AbstractQuery.php | 20 ++++++++++++ lib/Doctrine/ORM/UnitOfWork.php | 21 ++++++++----- .../ORM/Functional/BasicFunctionalTest.php | 31 +++++++++++++++++++ .../Tests/ORM/Functional/QueryTest.php | 3 +- .../Functional/SingleTableInheritanceTest.php | 18 +++++++++++ 5 files changed, 85 insertions(+), 8 deletions(-) diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 677de4472..6c8d0e5ff 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -336,6 +336,26 @@ abstract class AbstractQuery return $this->_expireResultCache; } + /** + * Change the default fetch mode of an association for this query. + * + * $fetchMode can be one of ClassMetadata::FETCH_EAGER or ClassMetadata::FETCH_LAZY + * + * @param string $class + * @param string $assocName + * @param int $fetchMode + * @return AbstractQuery + */ + public function setFetchMode($class, $assocName, $fetchMode) + { + if ($fetchMode !== Mapping\ClassMetadata::FETCH_EAGER) { + $fetchMode = Mapping\ClassMetadata::FETCH_LAZY; + } + + $this->_hints['fetchMode'][$class][$assocName] = $fetchMode; + return $this; + } + /** * Defines the processing mode to be used during hydration / result set transformation. * diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index b40c1c3b9..db29d9c64 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1895,7 +1895,7 @@ class UnitOfWork implements PropertyChangedListener } // Loading the entity right here, if its in the eager loading map get rid of it there. - unset($this->eagerLoadingEntities[$class->name][$idHash]); + unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]); // Properly initialize any unfetched associations, if partial objects are not allowed. if ( ! isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { @@ -1926,6 +1926,10 @@ class UnitOfWork implements PropertyChangedListener $class->reflFields[$field]->setValue($entity, null); $this->originalEntityData[$oid][$field] = null; } else { + if (!isset($hints['fetchMode'][$class->name][$field])) { + $hints['fetchMode'][$class->name][$field] = $assoc['fetch']; + } + // Foreign key is set // Check identity map first // FIXME: Can break easily with composite keys if join column values are in @@ -1937,25 +1941,28 @@ class UnitOfWork implements PropertyChangedListener // if this is an uninitialized proxy, we are deferring eager loads, // this association is marked as eager fetch, and its an uninitialized proxy (wtf!) // then we cann append this entity for eager loading! - if (isset($hints['fetchEager'][$class->name][$field]) && + if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER && isset($hints['deferEagerLoad']) && !$targetClass->isIdentifierComposite && $newValue instanceof Proxy && $newValue->__isInitialized__ === false) { - $this->eagerLoadingEntities[$assoc['targetEntity']][$relatedIdHash] = current($associatedId); + $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); } } else { if ($targetClass->subClasses) { - // If it might be a subtype, it can not be lazy + // If it might be a subtype, it can not be lazy. There isn't even + // a way to solve this with deferred eager loading, which means putting + // an entity with subclasses at a *-to-one location is really bad! (performance-wise) $newValue = $this->getEntityPersister($assoc['targetEntity']) ->loadOneToOneEntity($assoc, $entity, null, $associatedId); } else { // Deferred eager load only works for single identifier classes - if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || isset($hints['fetchEager'][$class->name][$field])) { + + if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER) { if (isset($hints['deferEagerLoad']) && !$targetClass->isIdentifierComposite) { // TODO: Is there a faster approach? - $this->eagerLoadingEntities[$assoc['targetEntity']][$relatedIdHash] = current($associatedId); + $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); } else { @@ -2032,7 +2039,7 @@ class UnitOfWork implements PropertyChangedListener foreach ($eagerLoadingEntities AS $entityName => $ids) { $class = $this->em->getClassMetadata($entityName); - $this->getEntityPersister($entityName)->loadAll(array_combine($class->identifier, array($ids))); + $this->getEntityPersister($entityName)->loadAll(array_combine($class->identifier, array(array_values($ids)))); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 7a7da0c7a..b6ca444ca 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -946,4 +946,35 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertNull($this->_em->find(get_class($ph), $ph->phonenumber)->getUser()); } + + /** + * @group DDC-952 + */ + public function testManyToOneFetchModeQuery() + { + $user = new CmsUser(); + $user->username = "beberlei"; + $user->name = "Benjamin E."; + $user->status = 'active'; + + $article = new CmsArticle(); + $article->topic = "foo"; + $article->text = "bar"; + $article->user = $user; + + $this->_em->persist($article); + $this->_em->persist($user); + $this->_em->flush(); + $this->_em->clear(); + + $qc = $this->getCurrentQueryCount(); + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.id = ?1"; + $article = $this->_em->createQuery($dql) + ->setParameter(1, $article->id) + ->setFetchMode('Doctrine\Tests\Models\CMS\CmsArticle', 'user', \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER) + ->getSingleResult(); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $article->user, "It IS a proxy, ..."); + $this->assertTrue($article->user->__isInitialized__, "...but its initialized!"); + $this->assertEquals($qc+2, $this->getCurrentQueryCount()); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index 4f0dc6879..c4a0beda0 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\CMS\CmsUser, Doctrine\Tests\Models\CMS\CmsArticle; +use Doctrine\ORM\Mapping\ClassMetadata; require_once __DIR__ . '/../../TestInit.php'; @@ -335,7 +336,7 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->clear(); $articles = $this->_em->createQuery('select a from Doctrine\Tests\Models\CMS\CmsArticle a') - ->setHint('fetchEager', array('Doctrine\Tests\Models\CMS\CmsArticle' => array('user' => true))) + ->setFetchMode('Doctrine\Tests\Models\CMS\CmsArticle', 'user', ClassMetadata::FETCH_EAGER) ->getResult(); $this->assertEquals(10, count($articles)); diff --git a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php index ca3856679..7271f42c3 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php @@ -2,6 +2,8 @@ namespace Doctrine\Tests\ORM\Functional; +use Doctrine\ORM\Mapping\ClassMetadata; + require_once __DIR__ . '/../../TestInit.php'; class SingleTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase @@ -349,4 +351,20 @@ class SingleTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase $ref = $this->_em->getReference('Doctrine\Tests\Models\Company\CompanyFixContract', $this->fix->getId()); $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $ref, "A proxy can be generated only if no subclasses exists for the requested reference."); } + + /** + * @group DDC-952 + */ + public function testEagerLoadInheritanceHierachy() + { + $this->loadFullFixture(); + + $dql = 'SELECT f FROM Doctrine\Tests\Models\Company\CompanyFixContract f WHERE f.id = ?1'; + $contract = $this->_em->createQuery($dql) + ->setFetchMode('Doctrine\Tests\Models\Company\CompanyFixContract', 'salesPerson', ClassMetadata::FETCH_EAGER) + ->setParameter(1, $this->fix->getId()) + ->getSingleResult(); + + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $contract->getSalesPerson()); + } } From 62755cc647a0872d41b83d00ab2d5b888fc78fa0 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 20 Mar 2011 12:19:01 +0100 Subject: [PATCH 26/71] [DDC-1070] Fix in AbstractQuery::iterate() method not respecting hydrator and parameters. --- lib/Doctrine/ORM/AbstractQuery.php | 14 ++++++-- .../Tests/ORM/Functional/QueryTest.php | 33 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 6c8d0e5ff..bdad77973 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -501,10 +501,20 @@ abstract class AbstractQuery * @param integer $hydrationMode The hydration mode to use. * @return IterableResult */ - public function iterate(array $params = array(), $hydrationMode = self::HYDRATE_OBJECT) + public function iterate(array $params = array(), $hydrationMode = null) { + if ($hydrationMode !== null) { + $this->setHydrationMode($hydrationMode); + } + + if ($params) { + $this->setParameters($params); + } + + $stmt = $this->_doExecute(); + return $this->_em->newHydrator($this->_hydrationMode)->iterate( - $this->_doExecute($params, $hydrationMode), $this->_resultSetMapping, $this->_hints + $stmt, $this->_resultSetMapping, $this->_hints ); } diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index c4a0beda0..66785898e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -5,6 +5,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\CMS\CmsUser, Doctrine\Tests\Models\CMS\CmsArticle; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query; require_once __DIR__ . '/../../TestInit.php'; @@ -136,6 +137,38 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $users = $q->getResult(); } + /** + * @group DDC-1070 + */ + public function testIterateResultAsArrayAndParams() + { + $article1 = new CmsArticle; + $article1->topic = "Doctrine 2"; + $article1->text = "This is an introduction to Doctrine 2."; + + $article2 = new CmsArticle; + $article2->topic = "Symfony 2"; + $article2->text = "This is an introduction to Symfony 2."; + + $this->_em->persist($article1); + $this->_em->persist($article2); + + $this->_em->flush(); + $this->_em->clear(); + + $query = $this->_em->createQuery("select a from Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.topic = ?1"); + $articles = $query->iterate(array(1 => 'Doctrine 2'), Query::HYDRATE_ARRAY); + + $found = array(); + foreach ($articles AS $article) { + $found[] = $article; + } + $this->assertEquals(1, count($found)); + $this->assertEquals(array( + array(array('id' => 1, 'topic' => 'Doctrine 2', 'text' => 'This is an introduction to Doctrine 2.', 'version' => 1)) + ), $found); + } + public function testIterateResult_IterativelyBuildUpUnitOfWork() { $article1 = new CmsArticle; From c77dbd859b038938e643bc4186d1ef2e3a7e5fe8 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 20 Mar 2011 12:25:27 +0100 Subject: [PATCH 27/71] [DDC-1070] Fix global test state problem introduced with test. --- tests/Doctrine/Tests/ORM/Functional/QueryTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index 66785898e..567b3ff8e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -155,6 +155,7 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->flush(); $this->_em->clear(); + $articleId = $article1->id; $query = $this->_em->createQuery("select a from Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.topic = ?1"); $articles = $query->iterate(array(1 => 'Doctrine 2'), Query::HYDRATE_ARRAY); @@ -165,7 +166,7 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase } $this->assertEquals(1, count($found)); $this->assertEquals(array( - array(array('id' => 1, 'topic' => 'Doctrine 2', 'text' => 'This is an introduction to Doctrine 2.', 'version' => 1)) + array(array('id' => $articleId, 'topic' => 'Doctrine 2', 'text' => 'This is an introduction to Doctrine 2.', 'version' => 1)) ), $found); } From ac175d2c406fd7790cc032e94c819e441ca3a5db Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 20 Mar 2011 12:35:52 +0100 Subject: [PATCH 28/71] [DDC-1068] Fix case-sensitivity problems of first loading of Metadata. --- lib/Doctrine/ORM/Mapping/ClassMetadata.php | 2 +- tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index 222b38877..2465f1c3a 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -63,10 +63,10 @@ class ClassMetadata extends ClassMetadataInfo */ public function __construct($entityName) { - parent::__construct($entityName); $this->reflClass = new ReflectionClass($entityName); $this->namespace = $this->reflClass->getNamespaceName(); $this->table['name'] = $this->reflClass->getShortName(); + parent::__construct($this->reflClass->getName()); // do not use $entityName, possible case-problems } /** diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php index 74889c0ae..9f3bc357e 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php @@ -446,4 +446,14 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase 'query' => 'SELECT u FROM __CLASS__ u WHERE u.id = ?1' )); } + + /** + * @group DDC-1068 + */ + public function testClassCaseSensitivity() + { + $user = new \Doctrine\Tests\Models\CMS\CmsUser(); + $cm = new ClassMetadata('DOCTRINE\TESTS\MODELS\CMS\CMSUSER'); + $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $cm->name); + } } From e42a227a7c176a67c8629f999de5e772edab0266 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 20 Mar 2011 13:07:47 +0100 Subject: [PATCH 29/71] [DDC-1052] Fix bug with versioning and inheritance --- lib/Doctrine/ORM/Persisters/BasicEntityPersister.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 6999f85ce..3fdbe171e 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -375,7 +375,7 @@ class BasicEntityPersister $result = $this->_conn->executeUpdate($sql, $params, $types); - if ($this->_class->isVersioned && ! $result) { + if ($versioned && ! $result) { throw OptimisticLockException::lockFailed($entity); } } From edfdbe10a09b8372aa1496ccf11ded38fa0e10ee Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 20 Mar 2011 14:07:33 +0100 Subject: [PATCH 30/71] [DDC-1053] Fix bug with usage of identification variables in GroupByItem. --- lib/Doctrine/ORM/Query/Parser.php | 4 ++++ lib/Doctrine/ORM/Query/QueryException.php | 3 +-- lib/Doctrine/ORM/Query/SqlWalker.php | 22 ++++++++++++++--- .../ORM/Query/LanguageRecognitionTest.php | 24 +++++++++++++++++++ .../ORM/Query/SelectSqlGenerationTest.php | 22 +++++++++++++++++ 5 files changed, 70 insertions(+), 5 deletions(-) diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 4caee210c..a5e1fa1ab 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -1328,6 +1328,10 @@ class Parser $token = $this->_lexer->lookahead; $identVariable = $this->IdentificationVariable(); + if (!isset($this->_queryComponents[$identVariable])) { + $this->semanticalError('Cannot group by undefined identification variable.'); + } + return $identVariable; } diff --git a/lib/Doctrine/ORM/Query/QueryException.php b/lib/Doctrine/ORM/Query/QueryException.php index f9dfd0823..aafe1e9d7 100644 --- a/lib/Doctrine/ORM/Query/QueryException.php +++ b/lib/Doctrine/ORM/Query/QueryException.php @@ -75,8 +75,7 @@ class QueryException extends \Doctrine\ORM\ORMException public static function invalidPathExpression($pathExpr) { return new self( - "Invalid PathExpression '" . $pathExpr->identificationVariable . - "." . implode('.', $pathExpr->parts) . "'." + "Invalid PathExpression '" . $pathExpr->identificationVariable . "." . $pathExpr->field . "'." ); } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 5be2ee287..84a4f8308 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -1254,9 +1254,25 @@ class SqlWalker implements TreeWalker */ public function walkGroupByClause($groupByClause) { - return ' GROUP BY ' . implode( - ', ', array_map(array($this, 'walkGroupByItem'), $groupByClause->groupByItems) - ); + $sql = ''; + foreach ($groupByClause->groupByItems AS $groupByItem) { + if (is_string($groupByItem)) { + foreach ($this->_queryComponents[$groupByItem]['metadata']->identifier AS $idField) { + if ($sql != '') { + $sql .= ', '; + } + $groupByItem = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $idField); + $groupByItem->type = AST\PathExpression::TYPE_STATE_FIELD; + $sql .= $this->walkGroupByItem($groupByItem); + } + } else { + if ($sql != '') { + $sql .= ', '; + } + $sql .= $this->walkGroupByItem($groupByItem); + } + } + return ' GROUP BY ' . $sql; } /** diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index fae0d7350..ff9d94b1f 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -496,6 +496,30 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertInvalidDQL('SELECT g FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.groups g'); } + /** + * @group DDC-1053 + */ + public function testGroupBy() + { + $this->assertValidDQL('SELECT g.id, count(u.id) FROM Doctrine\Tests\Models\CMS\CmsGroup g JOIN g.users u GROUP BY g.id'); + } + + /** + * @group DDC-1053 + */ + public function testGroupByIdentificationVariable() + { + $this->assertValidDQL('SELECT g, count(u.id) FROM Doctrine\Tests\Models\CMS\CmsGroup g JOIN g.users u GROUP BY g'); + } + + /** + * @group DDC-1053 + */ + public function testGroupByUnknownIdentificationVariable() + { + $this->assertInvalidDQL('SELECT g, count(u.id) FROM Doctrine\Tests\Models\CMS\CmsGroup g JOIN g.users u GROUP BY m'); + } + /** * @group DDC-117 */ diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 7b9cb4815..08fb9edb0 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -851,6 +851,28 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase 'SELECT f0_.id AS id0, f0_.extension AS extension1, f0_.name AS name2 FROM "file" f0_ INNER JOIN Directory d1_ ON f0_.parentDirectory_id = d1_.id WHERE f0_.id = ?' ); } + + /** + * @group DDC-1053 + */ + public function testGroupBy() + { + $this->assertSqlGeneration( + 'SELECT g.id, count(u.id) FROM Doctrine\Tests\Models\CMS\CmsGroup g JOIN g.users u GROUP BY g.id', + 'SELECT c0_.id AS id0, count(c1_.id) AS sclr1 FROM cms_groups c0_ INNER JOIN cms_users_groups c2_ ON c0_.id = c2_.group_id INNER JOIN cms_users c1_ ON c1_.id = c2_.user_id GROUP BY c0_.id' + ); + } + + /** + * @group DDC-1053 + */ + public function testGroupByIdentificationVariable() + { + $this->assertSqlGeneration( + 'SELECT g, count(u.id) FROM Doctrine\Tests\Models\CMS\CmsGroup g JOIN g.users u GROUP BY g', + 'SELECT c0_.id AS id0, c0_.name AS name1, count(c1_.id) AS sclr2 FROM cms_groups c0_ INNER JOIN cms_users_groups c2_ ON c0_.id = c2_.group_id INNER JOIN cms_users c1_ ON c1_.id = c2_.user_id GROUP BY c0_.id' + ); + } } From 7a41a205eeb18be48a7165f559045840bab9d0a0 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 20 Mar 2011 17:07:19 +0100 Subject: [PATCH 31/71] [DDC-992] Fix criteria usage of column names clashing with field or associations by prefixing with table names or alias. --- .../ORM/Persisters/BasicEntityPersister.php | 34 ++-- .../Functional/ClassTableInheritanceTest.php | 25 +++ .../ORM/Functional/Ticket/DDC992Test.php | 147 ++++++++++++++++++ 3 files changed, 192 insertions(+), 14 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC992Test.php diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 3fdbe171e..3213b8670 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -777,6 +777,7 @@ class BasicEntityPersister $criteria = array(); $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); if ($assoc['isOwningSide']) { + $quotedJoinTable = $sourceClass->getQuotedJoinTableName($assoc, $this->_platform); foreach ($assoc['relationToSourceKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) { if ($sourceClass->containsForeignIdentifier) { $field = $sourceClass->getFieldForColumn($sourceKeyColumn); @@ -785,9 +786,10 @@ class BasicEntityPersister $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]]; } - $criteria[$relationKeyColumn] = $value; + + $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $value; } else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) { - $criteria[$relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); } else { throw MappingException::joinColumnMustPointToMappedField( $sourceClass->name, $sourceKeyColumn @@ -796,6 +798,7 @@ class BasicEntityPersister } } else { $owningAssoc = $this->_em->getClassMetadata($assoc['targetEntity'])->associationMappings[$assoc['mappedBy']]; + $quotedJoinTable = $sourceClass->getQuotedJoinTableName($owningAssoc, $this->_platform); // TRICKY: since the association is inverted source and target are flipped foreach ($owningAssoc['relationToTargetKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) { if ($sourceClass->containsForeignIdentifier) { @@ -805,9 +808,9 @@ class BasicEntityPersister $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]]; } - $criteria[$relationKeyColumn] = $value; + $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $value; } else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) { - $criteria[$relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); } else { throw MappingException::joinColumnMustPointToMappedField( $sourceClass->name, $sourceKeyColumn @@ -1194,14 +1197,12 @@ class BasicEntityPersister } $conditionSql .= $this->_class->associationMappings[$field]['joinColumns'][0]['name']; - } else if ($assoc !== null) { - if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) { - $owningAssoc = $assoc['isOwningSide'] ? $assoc : $this->_em->getClassMetadata($assoc['targetEntity']) - ->associationMappings[$assoc['mappedBy']]; - $conditionSql .= $this->_class->getQuotedJoinTableName($owningAssoc, $this->_platform) . '.' . $field; - } else { - $conditionSql .= $field; - } + } else if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) { + // very careless developers could potentially open up this normally hidden api for userland attacks, + // therefore checking for spaces and function calls which are not allowed. + + // found a join column condition, not really a "field" + $conditionSql .= $field; } else { throw ORMException::unrecognizedField($field); } @@ -1254,6 +1255,11 @@ class BasicEntityPersister $criteria = array(); $owningAssoc = $this->_class->associationMappings[$assoc['mappedBy']]; $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); + + $tableAlias = isset($owningAssoc['inherited']) ? + $this->_getSQLTableAlias($owningAssoc['inherited']) + : $this->_getSQLTableAlias($this->_class->name); + foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) { if ($sourceClass->containsForeignIdentifier) { $field = $sourceClass->getFieldForColumn($sourceKeyColumn); @@ -1262,9 +1268,9 @@ class BasicEntityPersister $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]]; } - $criteria[$targetKeyColumn] = $value; + $criteria[$tableAlias . "." . $targetKeyColumn] = $value; } else { - $criteria[$targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + $criteria[$tableAlias . "." . $targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php index 099afeb8a..0fbff503e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php @@ -410,4 +410,29 @@ class ClassTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase $ref = $this->_em->getReference('Doctrine\Tests\Models\Company\CompanyManager', $manager->getId()); $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $ref, "A proxy can be generated only if no subclasses exists for the requested reference."); } + + /** + * @group DDC-992 + */ + public function testGetSubClassManyToManyCollection() + { + $manager = new CompanyManager(); + $manager->setName('gblanco'); + $manager->setSalary(1234); + $manager->setTitle('Awesome!'); + $manager->setDepartment('IT'); + + $person = new CompanyPerson(); + $person->setName('friend'); + + $manager->addFriend($person); + + $this->_em->persist($manager); + $this->_em->persist($person); + $this->_em->flush(); + $this->_em->clear(); + + $manager = $this->_em->find('Doctrine\Tests\Models\Company\CompanyManager', $manager->getId()); + $this->assertEquals(1, count($manager->getFriends())); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC992Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC992Test.php new file mode 100644 index 000000000..36d9a392f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC992Test.php @@ -0,0 +1,147 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC992Role'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC992Parent'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC992Child'), + )); + } catch(\Exception $e) { + + } + } + + public function testIssue() + { + $role = new DDC992Role(); + $role->name = "Parent"; + $child = new DDC992Role(); + $child->name = "child"; + + $role->extendedBy[] = $child; + $child->extends[] = $role; + + $this->_em->persist($role); + $this->_em->persist($child); + $this->_em->flush(); + $this->_em->clear(); + + $child = $this->_em->getRepository(get_class($role))->find($child->roleID); + $parents = count($child->extends); + $this->assertEquals(1, $parents); + foreach ($child->extends AS $parent) { + $this->assertEquals($role->getRoleID(), $parent->getRoleID()); + } + } + + public function testOneToManyChild() + { + $parent = new DDC992Parent(); + $child = new DDC992Child(); + $child->parent = $parent; + $parent->childs[] = $child; + + $this->_em->persist($parent); + $this->_em->persist($child); + $this->_em->flush(); + $this->_em->clear(); + + $parentRepository = $this->_em->getRepository(get_class($parent)); + $childRepository = $this->_em->getRepository(get_class($child)); + + $parent = $parentRepository->find($parent->id); + $this->assertEquals(1, count($parent->childs)); + $this->assertEquals(0, count($parent->childs[0]->childs())); + + $child = $parentRepository->findOneBy(array("id" => $child->id)); + $this->assertSame($parent->childs[0], $child); + + $this->_em->clear(); + + $child = $parentRepository->find($child->id); + $this->assertEquals(0, count($child->childs)); + + $this->_em->clear(); + + $child = $childRepository->find($child->id); + $this->assertEquals(0, count($child->childs)); + } +} + +/** + * @Entity + * @InheritanceType("JOINED") + * @DiscriminatorMap({"child" = "DDC992Child", "parent" = "DDC992Parent"}) + */ +class DDC992Parent +{ + /** @Id @GeneratedValue @Column(type="integer") */ + public $id; + /** @ManyToOne(targetEntity="DDC992Parent", inversedBy="childs") */ + public $parent; + /** @OneToMany(targetEntity="DDC992Child", mappedBy="parent") */ + public $childs; +} + +/** + * @Entity + */ +class DDC992Child extends DDC992Parent +{ + public function childs() + { + return $this->childs; + } +} + +/** + * @Entity + */ +class DDC992Role +{ + public function getRoleID() + { + return $this->roleID; + } + + /** + * @Id @Column(name="roleID", type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $roleID; + /** + * @Column (name="name", type="string", length="45") + */ + public $name; + /** + * @ManyToMany (targetEntity="DDC992Role", mappedBy="extends") + */ + public $extendedBy; + /** + * @ManyToMany (targetEntity="DDC992Role", inversedBy="extendedBy") + * @JoinTable (name="RoleRelations", + * joinColumns={@JoinColumn(name="roleID", referencedColumnName="roleID")}, + * inverseJoinColumns={@JoinColumn(name="extendsRoleID", referencedColumnName="roleID")} + * ) + */ + public $extends; + + public function __construct() { + $this->extends = new ArrayCollection; + $this->extendedBy = new ArrayCollection; + } +} \ No newline at end of file From e126315c1b3a20b98ede283b7df03f536120b10d Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 21 Mar 2011 23:54:40 +0100 Subject: [PATCH 32/71] Bump DBAL dependency to include Security Fix for AbstractPlatform::modifyLimitQuery() --- lib/vendor/doctrine-dbal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vendor/doctrine-dbal b/lib/vendor/doctrine-dbal index 9cd6df38d..b21fc4263 160000 --- a/lib/vendor/doctrine-dbal +++ b/lib/vendor/doctrine-dbal @@ -1 +1 @@ -Subproject commit 9cd6df38d841abb4719286ea35a1b37aa2679f8d +Subproject commit b21fc4263d69a8b969c2d127a7db858646418247 From 1f50dee8a84e8493f58fbb30052709e082455cce Mon Sep 17 00:00:00 2001 From: Michael Ridgway Date: Mon, 21 Mar 2011 23:17:08 -0400 Subject: [PATCH 33/71] DDC-696: Added onClear event --- lib/Doctrine/ORM/Event/OnClearEventArgs.php | 56 +++++++++++++++++++ lib/Doctrine/ORM/Events.php | 8 +++ lib/Doctrine/ORM/UnitOfWork.php | 4 ++ .../Tests/ORM/Functional/AllTests.php | 1 + .../Tests/ORM/Functional/ClearEventTest.php | 44 +++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 lib/Doctrine/ORM/Event/OnClearEventArgs.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php diff --git a/lib/Doctrine/ORM/Event/OnClearEventArgs.php b/lib/Doctrine/ORM/Event/OnClearEventArgs.php new file mode 100644 index 000000000..0fe4a4fdd --- /dev/null +++ b/lib/Doctrine/ORM/Event/OnClearEventArgs.php @@ -0,0 +1,56 @@ +. +*/ + +namespace Doctrine\ORM\Event; + +/** + * Provides event arguments for the onClear event. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @version $Revision$ + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class OnClearEventArgs extends \Doctrine\Common\EventArgs +{ + /** + * @var \Doctrine\ORM\EntityManager + */ + private $em; + + /** + * @param \Doctrine\ORM\EntityManager $em + */ + public function __construct($em) + { + $this->em = $em; + } + + /** + * @return \Doctrine\ORM\EntityManager + */ + public function getEntityManager() + { + return $this->em; + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Events.php b/lib/Doctrine/ORM/Events.php index e25de658a..8344b07c1 100644 --- a/lib/Doctrine/ORM/Events.php +++ b/lib/Doctrine/ORM/Events.php @@ -119,4 +119,12 @@ final class Events * @var string */ const onFlush = 'onFlush'; + + /** + * The onClear event occurs when the EntityManager#clear() operation is invoked, + * after all references to entities have been removed from the unit of work. + * + * @var string + */ + const onClear = 'onClear'; } \ No newline at end of file diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index db29d9c64..79c946f87 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1790,6 +1790,10 @@ class UnitOfWork implements PropertyChangedListener if ($this->commitOrderCalculator !== null) { $this->commitOrderCalculator->clear(); } + + if ($this->evm->hasListeners(Events::onClear)) { + $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em)); + } } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index 077ee4a7c..cac410281 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -58,6 +58,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\DatabaseDriverTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\PostgreSQLIdentityStrategyTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ExtraLazyCollectionTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ClearEventTest'); $suite->addTest(Locking\AllTests::suite()); $suite->addTest(Ticket\AllTests::suite()); diff --git a/tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php b/tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php new file mode 100644 index 000000000..e95a4cd61 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php @@ -0,0 +1,44 @@ +_em->getEventManager()->addEventListener(Events::onClear, $listener); + + $this->_em->clear(); + + $this->assertTrue($listener->called); + } +} + +class OnClearListener +{ + public $called = false; + + public function onClear(OnClearEventArgs $args) + { + $this->called = true; + } +} + + From 17cbb349523295a1c0879809a0a266b9075b7f56 Mon Sep 17 00:00:00 2001 From: Michael Ridgway Date: Mon, 21 Mar 2011 23:30:10 -0400 Subject: [PATCH 34/71] Clean up of test case --- tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php b/tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php index e95a4cd61..e86edb5b7 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ClearEventTest.php @@ -2,8 +2,6 @@ namespace Doctrine\Tests\ORM\Functional; -use Doctrine\Tests\Models\CMS\CmsUser; -use Doctrine\Tests\Models\CMS\CmsPhonenumber; use Doctrine\ORM\Event\OnClearEventArgs; use Doctrine\ORM\Events; @@ -12,7 +10,7 @@ require_once __DIR__ . '/../../TestInit.php'; /** * ClearEventTest * - * @author Michael Ridgway + * @author Michael Ridgway */ class ClearEventTest extends \Doctrine\Tests\OrmFunctionalTestCase { @@ -39,6 +37,4 @@ class OnClearListener { $this->called = true; } -} - - +} \ No newline at end of file From 706cc838e51a5f3583600ec58ed80229b17d3c21 Mon Sep 17 00:00:00 2001 From: Michael Ridgway Date: Tue, 22 Mar 2011 08:54:33 -0400 Subject: [PATCH 35/71] Removed svn variable --- lib/Doctrine/ORM/Event/OnClearEventArgs.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Doctrine/ORM/Event/OnClearEventArgs.php b/lib/Doctrine/ORM/Event/OnClearEventArgs.php index 0fe4a4fdd..ad89fbc90 100644 --- a/lib/Doctrine/ORM/Event/OnClearEventArgs.php +++ b/lib/Doctrine/ORM/Event/OnClearEventArgs.php @@ -1,7 +1,5 @@ Date: Sun, 27 Mar 2011 11:34:14 +0200 Subject: [PATCH 36/71] [DDC-1014] Update DBAL remote to include date arithmetics related functionality. --- lib/vendor/doctrine-dbal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vendor/doctrine-dbal b/lib/vendor/doctrine-dbal index b21fc4263..0a9943872 160000 --- a/lib/vendor/doctrine-dbal +++ b/lib/vendor/doctrine-dbal @@ -1 +1 @@ -Subproject commit b21fc4263d69a8b969c2d127a7db858646418247 +Subproject commit 0a99438729e59bcb5262b22c06c782de4dfacbb0 From 4f1af0114f27e5bb7e33e43ab59ffe56e78a701b Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 27 Mar 2011 12:18:47 +0200 Subject: [PATCH 37/71] [DDC-1014] Add DATE_ADD(), DATE_SUB(), DATE_DIFF() functions for DQL. --- .../Query/AST/Functions/DateAddFunction.php | 71 +++++++++++++++++++ .../Query/AST/Functions/DateDiffFunction.php | 58 +++++++++++++++ .../Query/AST/Functions/DateSubFunction.php | 58 +++++++++++++++ lib/Doctrine/ORM/Query/Parser.php | 17 +++-- .../ORM/Functional/QueryDqlFunctionTest.php | 44 +++++++++++- 5 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 lib/Doctrine/ORM/Query/AST/Functions/DateAddFunction.php create mode 100644 lib/Doctrine/ORM/Query/AST/Functions/DateDiffFunction.php create mode 100644 lib/Doctrine/ORM/Query/AST/Functions/DateSubFunction.php diff --git a/lib/Doctrine/ORM/Query/AST/Functions/DateAddFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/DateAddFunction.php new file mode 100644 index 000000000..1d840cc49 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/Functions/DateAddFunction.php @@ -0,0 +1,71 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\QueryException; + +/** + * "DATE_ADD(date1, interval, unit)" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + */ +class DateAddFunction extends FunctionNode +{ + public $firstDateExpression = null; + public $intervalExpression = null; + public $unit = null; + + public function getSql(SqlWalker $sqlWalker) + { + $unit = strtolower($this->unit); + if ($unit == "day") { + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddDaysExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->intervalExpression->dispatch($sqlWalker) + ); + } else if ($unit == "month") { + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddMonthExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->intervalExpression->dispatch($sqlWalker) + ); + } else { + throw QueryException::semanticalError('DATE_ADD() only supports units of type day and month.'); + } + } + + public function parse(Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->firstDateExpression = $parser->ArithmeticPrimary(); + $parser->match(Lexer::T_COMMA); + $this->intervalExpression = $parser->ArithmeticPrimary(); + $parser->match(Lexer::T_COMMA); + $this->unit = $parser->StringPrimary(); + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/AST/Functions/DateDiffFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/DateDiffFunction.php new file mode 100644 index 000000000..4de5411bd --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/Functions/DateDiffFunction.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\Parser; + +/** + * "DATE_DIFF(date1, date2)" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + */ +class DateDiffFunction extends FunctionNode +{ + public $date1; + public $date2; + + public function getSql(SqlWalker $sqlWalker) + { + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateDiffExpression( + $this->date1->dispatch($sqlWalker), + $this->date2->dispatch($sqlWalker) + ); + } + + public function parse(Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->date1 = $parser->ArithmeticPrimary(); + $parser->match(Lexer::T_COMMA); + $this->date2 = $parser->ArithmeticPrimary(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/AST/Functions/DateSubFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/DateSubFunction.php new file mode 100644 index 000000000..a608d49d4 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/Functions/DateSubFunction.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\QueryException; + +/** + * "DATE_ADD(date1, interval, unit)" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + */ +class DateSubFunction extends DateAddFunction +{ + public $firstDateExpression = null; + public $intervalExpression = null; + public $unit = null; + + public function getSql(SqlWalker $sqlWalker) + { + $unit = strtolower($this->unit); + if ($unit == "day") { + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubDaysExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->intervalExpression->dispatch($sqlWalker) + ); + } else if ($unit == "month") { + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubMonthExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->intervalExpression->dispatch($sqlWalker) + ); + } else { + throw QueryException::semanticalError('DATE_SUB() only supports units of type day and month.'); + } + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index a5e1fa1ab..6cd0bdad6 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -45,19 +45,22 @@ class Parser /** READ-ONLY: Maps BUILT-IN numeric function names to AST class names. */ private static $_NUMERIC_FUNCTIONS = array( - 'length' => 'Doctrine\ORM\Query\AST\Functions\LengthFunction', - 'locate' => 'Doctrine\ORM\Query\AST\Functions\LocateFunction', - 'abs' => 'Doctrine\ORM\Query\AST\Functions\AbsFunction', - 'sqrt' => 'Doctrine\ORM\Query\AST\Functions\SqrtFunction', - 'mod' => 'Doctrine\ORM\Query\AST\Functions\ModFunction', - 'size' => 'Doctrine\ORM\Query\AST\Functions\SizeFunction' + 'length' => 'Doctrine\ORM\Query\AST\Functions\LengthFunction', + 'locate' => 'Doctrine\ORM\Query\AST\Functions\LocateFunction', + 'abs' => 'Doctrine\ORM\Query\AST\Functions\AbsFunction', + 'sqrt' => 'Doctrine\ORM\Query\AST\Functions\SqrtFunction', + 'mod' => 'Doctrine\ORM\Query\AST\Functions\ModFunction', + 'size' => 'Doctrine\ORM\Query\AST\Functions\SizeFunction', + 'date_diff' => 'Doctrine\ORM\Query\AST\Functions\DateDiffFunction', ); /** READ-ONLY: Maps BUILT-IN datetime function names to AST class names. */ private static $_DATETIME_FUNCTIONS = array( 'current_date' => 'Doctrine\ORM\Query\AST\Functions\CurrentDateFunction', 'current_time' => 'Doctrine\ORM\Query\AST\Functions\CurrentTimeFunction', - 'current_timestamp' => 'Doctrine\ORM\Query\AST\Functions\CurrentTimestampFunction' + 'current_timestamp' => 'Doctrine\ORM\Query\AST\Functions\CurrentTimestampFunction', + 'date_add' => 'Doctrine\ORM\Query\AST\Functions\DateAddFunction', + 'date_sub' => 'Doctrine\ORM\Query\AST\Functions\DateSubFunction', ); /** diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php index 09ddaf0a3..42971e6b0 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryDqlFunctionTest.php @@ -268,7 +268,49 @@ class QueryDqlFunctionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals('Guilherme B.Complaint Department', $arg[2]['namedep']); $this->assertEquals('Benjamin E.HR', $arg[3]['namedep']); } - + + /** + * @group DDC-1014 + */ + public function testDateDiff() + { + $arg = $this->_em->createQuery("SELECT DATE_DIFF(CURRENT_TIMESTAMP(), '2011-01-01') AS diff FROM Doctrine\Tests\Models\Company\CompanyManager m") + ->getARrayResult(); + + $this->assertTrue($arg[0]['diff'] > 0); + } + + /** + * @group DDC-1014 + */ + public function testDateAdd() + { + $arg = $this->_em->createQuery("SELECT DATE_ADD(CURRENT_TIMESTAMP(), 10, 'day') AS add FROM Doctrine\Tests\Models\Company\CompanyManager m") + ->getArrayResult(); + + $this->assertTrue(strtotime($arg[0]['add']) > 0); + + $arg = $this->_em->createQuery("SELECT DATE_ADD(CURRENT_TIMESTAMP(), 10, 'month') AS add FROM Doctrine\Tests\Models\Company\CompanyManager m") + ->getArrayResult(); + + $this->assertTrue(strtotime($arg[0]['add']) > 0); + } + + /** + * @group DDC-1014 + */ + public function testDateSub() + { + $arg = $this->_em->createQuery("SELECT DATE_SUB(CURRENT_TIMESTAMP(), 10, 'day') AS add FROM Doctrine\Tests\Models\Company\CompanyManager m") + ->getArrayResult(); + + $this->assertTrue(strtotime($arg[0]['add']) > 0); + + $arg = $this->_em->createQuery("SELECT DATE_SUB(CURRENT_TIMESTAMP(), 10, 'month') AS add FROM Doctrine\Tests\Models\Company\CompanyManager m") + ->getArrayResult(); + + $this->assertTrue(strtotime($arg[0]['add']) > 0); + } protected function generateFixture() { From 6ed0ff0a12f383ee192c1aca77931ad4bee90adf Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 27 Mar 2011 14:04:53 +0200 Subject: [PATCH 38/71] [DDC-1079] Bugfix for shortcut for ArithmeticExpressions in SimpleSelectExpression that lead to literals not being valid. Problem was that ScalarExpression() did not handle AggregateExpressions() at all, which is now fixed. --- lib/Doctrine/ORM/Query/Parser.php | 19 ++++++++----------- .../ORM/Query/LanguageRecognitionTest.php | 9 +++++++++ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 6cd0bdad6..51d589095 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -1644,7 +1644,7 @@ class Parser return $this->StateFieldPathExpression(); } else if ($lookahead == Lexer::T_INTEGER || $lookahead == Lexer::T_FLOAT) { return $this->SimpleArithmeticExpression(); - } else if ($this->_isFunction()) { + } else if ($this->_isFunction() || $this->_isAggregateFunction($this->_lexer->lookahead['type'])) { // We may be in an ArithmeticExpression (find the matching ")" and inspect for Math operator) $this->_lexer->peek(); // "(" $peek = $this->_peekBeyondClosingParenthesis(); @@ -1652,8 +1652,12 @@ class Parser if ($this->_isMathOperator($peek)) { return $this->SimpleArithmeticExpression(); } - - return $this->FunctionDeclaration(); + + if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { + return $this->AggregateExpression(); + } else { + return $this->FunctionDeclaration(); + } } else if ($lookahead == Lexer::T_STRING) { return $this->StringPrimary(); } else if ($lookahead == Lexer::T_INPUT_PARAMETER) { @@ -1797,15 +1801,8 @@ class Parser } $this->_lexer->peek(); - $beyond = $this->_peekBeyondClosingParenthesis(); - if ($this->_isMathOperator($beyond)) { - $expression = $this->ScalarExpression(); - } else if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { - $expression = $this->AggregateExpression(); - } else { - $expression = $this->FunctionDeclaration(); - } + $expression = $this->ScalarExpression(); $expr = new AST\SimpleSelectExpression($expression); diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index ff9d94b1f..fa1da40d5 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -251,6 +251,15 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertValidDQL("SELECT (SELECT (SUM(u.id) / COUNT(u.id)) FROM Doctrine\Tests\Models\CMS\CmsUser u2) value FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name = 'jon'"); } + /** + * @group DDC-1079 + */ + public function testSelectLiteralInSubselect() + { + $this->assertValidDQL('SELECT (SELECT 1 FROM Doctrine\Tests\Models\CMS\CmsUser u2) value FROM Doctrine\Tests\Models\CMS\CmsUser u'); + $this->assertValidDQL('SELECT (SELECT 0 FROM Doctrine\Tests\Models\CMS\CmsUser u2) value FROM Doctrine\Tests\Models\CMS\CmsUser u'); + } + public function testDuplicateAliasInSubselectPart() { $this->assertInvalidDQL("SELECT (SELECT SUM(u.id) / COUNT(u.id) AS foo FROM Doctrine\Tests\Models\CMS\CmsUser u2) foo FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name = 'jon'"); From bda15231da790d9c1d523f92ff4636ae68ca2099 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 27 Mar 2011 21:10:50 +0200 Subject: [PATCH 39/71] [DDC-1077] Bugfix in not handling literals in Select Expressions. --- lib/Doctrine/ORM/Query/Parser.php | 3 ++- lib/Doctrine/ORM/Query/SqlWalker.php | 9 +++++++-- .../Tests/ORM/Query/LanguageRecognitionTest.php | 8 ++++++++ .../Tests/ORM/Query/SelectSqlGenerationTest.php | 13 ++++++++++++- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 51d589095..2ba869f22 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -1732,7 +1732,8 @@ class Parser $expression = $this->PartialObjectExpression(); $identVariable = $expression->identificationVariable; } else if ($this->_lexer->lookahead['type'] == Lexer::T_INTEGER || - $this->_lexer->lookahead['type'] == Lexer::T_FLOAT) { + $this->_lexer->lookahead['type'] == Lexer::T_FLOAT || + $this->_lexer->lookahead['type'] == Lexer::T_STRING) { // Shortcut: ScalarExpression => SimpleArithmeticExpression $expression = $this->SimpleArithmeticExpression(); } else { diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 84a4f8308..a14987eaa 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -961,7 +961,8 @@ class SqlWalker implements TreeWalker $expr instanceof AST\SimpleArithmeticExpression || $expr instanceof AST\ArithmeticTerm || $expr instanceof AST\ArithmeticFactor || - $expr instanceof AST\ArithmeticPrimary + $expr instanceof AST\ArithmeticPrimary || + $expr instanceof AST\Literal ) { if ( ! $selectExpression->fieldIdentificationVariable) { $resultAlias = $this->_scalarResultCounter++; @@ -970,7 +971,11 @@ class SqlWalker implements TreeWalker } $columnAlias = 'sclr' . $this->_aliasCounter++; - $sql .= $this->walkSimpleArithmeticExpression($expr) . ' AS ' . $columnAlias; + if ($expr instanceof AST\Literal) { + $sql .= $this->walkLiteral($expr) . ' AS ' .$columnAlias; + } else { + $sql .= $this->walkSimpleArithmeticExpression($expr) . ' AS ' . $columnAlias; + } $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index fa1da40d5..1ac05df90 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -260,6 +260,14 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertValidDQL('SELECT (SELECT 0 FROM Doctrine\Tests\Models\CMS\CmsUser u2) value FROM Doctrine\Tests\Models\CMS\CmsUser u'); } + /** + * @group DDC-1077 + */ + public function testConstantValueInSelect() + { + $this->assertValidDQL("SELECT u.name, 'foo' AS bar FROM Doctrine\Tests\Models\CMS\CmsUser u", true); + } + public function testDuplicateAliasInSubselectPart() { $this->assertInvalidDQL("SELECT (SELECT SUM(u.id) / COUNT(u.id) AS foo FROM Doctrine\Tests\Models\CMS\CmsUser u2) foo FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name = 'jon'"); diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 08fb9edb0..2d0101e03 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -42,7 +42,7 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase parent::assertEquals($sqlToBeConfirmed, $query->getSql()); $query->free(); } catch (\Exception $e) { - $this->fail($e->getMessage()); + $this->fail($e->getMessage() ."\n".$e->getTraceAsString()); } } @@ -170,6 +170,17 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase ); }*/ + /** + * @group DDC-1077 + */ + public function testConstantValueInSelect() + { + $this->assertSqlGeneration( + "SELECT u.name, 'foo' AS bar FROM Doctrine\Tests\Models\CMS\CmsUser u", + "SELECT c0_.name AS name0, 'foo' AS sclr1 FROM cms_users c0_" + ); + } + public function testSupportsOrderByWithAscAsDefault() { $this->assertSqlGeneration( From 9a75277dd40e4352d867ee3126b0e4d53e124381 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 29 Mar 2011 20:04:14 +0200 Subject: [PATCH 40/71] [DDC-692] Add ClassMetadataInfo::isReadOnly flag and ignore these entities in Change Tracking. --- lib/Doctrine/ORM/Mapping/ClassMetadata.php | 4 ++++ .../ORM/Mapping/ClassMetadataInfo.php | 21 +++++++++++++++++++ lib/Doctrine/ORM/UnitOfWork.php | 6 +++--- .../Tests/ORM/Mapping/ClassMetadataTest.php | 4 ++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index 2465f1c3a..4ec0ede53 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -334,6 +334,10 @@ class ClassMetadata extends ClassMetadataInfo $serialized[] = 'namedQueries'; } + if ($this->isReadOnly) { + $serialized[] = 'isReadOnly'; + } + return $serialized; } diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 7db39621d..a10d8c866 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -484,6 +484,17 @@ class ClassMetadataInfo implements ClassMetadata */ public $reflClass; + /** + * Is this entity marked as "read-only"? + * + * That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance + * optimization for entities that are immutable, either in your domain or through the relation database + * (coming from a view, or a history table for example). + * + * @var bool + */ + public $isReadOnly = false; + /** * Initializes a new ClassMetadata instance that will hold the object-relational mapping * metadata of the class with the given name. @@ -1818,4 +1829,14 @@ class ClassMetadataInfo implements ClassMetadata { $this->versionField = $versionField; } + + /** + * Mark this class as read only, no change tracking is applied to it. + * + * @return void + */ + public function markReadOnly() + { + $this->isReadOnly = true; + } } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 79c946f87..945815a69 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -514,9 +514,9 @@ class UnitOfWork implements PropertyChangedListener $class = $this->em->getClassMetadata($className); // Skip class if instances are read-only - //if ($class->isReadOnly) { - // continue; - //} + if ($class->isReadOnly) { + continue; + } // If change tracking is explicit or happens through notification, then only compute // changes on entities of that type that are explicitly marked for synchronization. diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php index 9f3bc357e..8f57280df 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php @@ -30,6 +30,8 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $cm->setCustomRepositoryClass("UserRepository"); $cm->setDiscriminatorColumn(array('name' => 'disc', 'type' => 'integer')); $cm->mapOneToOne(array('fieldName' => 'phonenumbers', 'targetEntity' => 'Bar', 'mappedBy' => 'foo')); + $cm->markReadOnly(); + $cm->addNamedQuery(array('name' => 'dql', 'query' => 'foo')); $this->assertEquals(1, count($cm->associationMappings)); $serialized = serialize($cm); @@ -51,6 +53,8 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $this->assertTrue($oneOneMapping['fetch'] == ClassMetadata::FETCH_LAZY); $this->assertEquals('phonenumbers', $oneOneMapping['fieldName']); $this->assertEquals('Doctrine\Tests\Models\CMS\Bar', $oneOneMapping['targetEntity']); + $this->assertTrue($cm->isReadOnly); + $this->assertEquals(array('dql' => 'foo'), $cm->namedQueries); } public function testFieldIsNullable() From 34ad308599bf6470e6d43f88985b781543d0bbc4 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 29 Mar 2011 20:17:44 +0200 Subject: [PATCH 41/71] [DDC-692] Add respective metadata mapping possiblities for read-only entities and a test. --- doctrine-mapping.xsd | 1 + .../ORM/Mapping/Driver/AnnotationDriver.php | 4 ++ .../Mapping/Driver/DoctrineAnnotations.php | 3 +- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 3 + .../ORM/Mapping/Driver/YamlDriver.php | 3 + .../Tests/ORM/Functional/AllTests.php | 1 + .../Tests/ORM/Functional/ReadOnlyTest.php | 61 +++++++++++++++++++ 7 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index 19aacd237..6216d75dc 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -88,6 +88,7 @@ + diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index f976d0aec..b74fc860e 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -132,6 +132,10 @@ class AnnotationDriver implements Driver if (isset($classAnnotations['Doctrine\ORM\Mapping\Entity'])) { $entityAnnot = $classAnnotations['Doctrine\ORM\Mapping\Entity']; $metadata->setCustomRepositoryClass($entityAnnot->repositoryClass); + + if ($entityAnnot->readOnly) { + $metadata->markReadOnly(); + } } else if (isset($classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'])) { $metadata->isMappedSuperclass = true; } else { diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index 31e712d42..84d2cd1c8 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -1,7 +1,5 @@ setCustomRepositoryClass( isset($xmlRoot['repository-class']) ? (string)$xmlRoot['repository-class'] : null ); + if (isset($xmlRoot['read-only']) && $xmlRoot['read-only'] == "true") { + $metadata->markReadOnly(); + } } else if ($xmlRoot->getName() == 'mapped-superclass') { $metadata->isMappedSuperclass = true; } else { diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 75bfeec74..97617a64d 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -49,6 +49,9 @@ class YamlDriver extends AbstractFileDriver $metadata->setCustomRepositoryClass( isset($element['repositoryClass']) ? $element['repositoryClass'] : null ); + if (isset($element['readOnly']) && $element['readOnly'] == true) { + $metadata->markReadOnly(); + } } else if ($element['type'] == 'mappedSuperclass') { $metadata->isMappedSuperclass = true; } else { diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index cac410281..4dd2b0644 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -59,6 +59,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\PostgreSQLIdentityStrategyTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ExtraLazyCollectionTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ClearEventTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ReadOnlyTest'); $suite->addTest(Locking\AllTests::suite()); $suite->addTest(Ticket\AllTests::suite()); diff --git a/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php new file mode 100644 index 000000000..8a5819956 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php @@ -0,0 +1,61 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\ReadOnlyEntity'), + )); + } + + public function testReadOnlyEntityNeverChangeTracked() + { + $readOnly = new ReadOnlyEntity("Test1", 1234); + $this->_em->persist($readOnly); + $this->_em->flush(); + + $readOnly->name = "Test2"; + $readOnly->number = 4321; + + $this->_em->flush(); + $this->_em->clear(); + + $dbReadOnly = $this->_em->find('Doctrine\Tests\ORM\Functional\ReadOnlyEntity', $readOnly->id); + $this->assertEquals("Test1", $dbReadOnly->name); + $this->assertEquals(1234, $dbReadOnly->number); + } +} + +/** + * @Entity(readOnly=true) + */ +class ReadOnlyEntity +{ + /** + * @Id @GeneratedValue @Column(type="integer") + * @var int + */ + public $id; + /** @column(type="string") */ + public $name; + /** @Column(type="integer") */ + public $number; + + public function __construct($name, $number) + { + $this->name = $name; + $this->number = $number; + } +} \ No newline at end of file From 20dc72ef9ae0d7c4afead35c249c224b95570aa7 Mon Sep 17 00:00:00 2001 From: Michael Ridgway Date: Tue, 29 Mar 2011 20:35:01 -0400 Subject: [PATCH 42/71] First pass on RSM helper functions for adding entities --- lib/Doctrine/ORM/Query/ResultSetMapping.php | 38 ++++++++- .../Tests/ORM/Functional/NativeQueryTest.php | 81 +++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index 7066405df..ededf1153 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -1,7 +1,5 @@ metaMappings[$columnName] = $fieldName; $this->columnOwnerMap[$columnName] = $alias; } + + /** + * Adds a root entity and all of its fields to the result set. + * + * @param ClassMetadata $classMetadata + * @param string $alias The unique alias to use for the root entity. + * @param string $prefix Prefix for columns + */ + public function addRootEntityFromClassMetadata(ClassMetadata $classMetadata, $alias, $prefix = '') + { + $this->addEntityResult($classMetadata->getReflectionClass()->getName(), $alias); + foreach ($classMetadata->getColumnNames() AS $columnName) { + // $columnName should use DBAL\Platforms\AbstractPlatform::getSQLResultCasing() but we don't have the EM + $this->addFieldResult($alias, $prefix . $columnName, $classMetadata->getFieldName($columnName)); + } + } + + /** + * Adds a joined entity and all of its fields to the result set. + * + * @param ClassMetadata $classMetadata + * @param string $alias The unique alias to use for the joined entity. + * @param string $parentAlias The alias of the entity result that is the parent of this joined result. + * @param object $relation The association field that connects the parent entity result with the joined entity result. + * @param string $prefix Prefix for columns + */ + public function addJoinedEntityFromClassMetadata(ClassMetadata $classMetadata, $alias, $parentAlias, $relation, $prefix = '') + { + $this->addJoinedEntityResult($classMetadata->getReflectionClass()->getName(), $alias, $parentAlias, $relation); + foreach ($classMetadata->getColumnNames() AS $columnName) { + // $columnName should use DBAL\Platforms\AbstractPlatform::getSQLResultCasing() but we don't have the EM + $this->addFieldResult($alias, $prefix . $columnName, $classMetadata->getFieldName($columnName)); + } + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php index 23dfa3a73..65778badf 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php @@ -156,5 +156,86 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertSame($q, $q2); } + + public function testJoinedOneToManyNativeQueryWithRSMHelper() + { + $user = new CmsUser; + $user->name = 'Roman'; + $user->username = 'romanb'; + $user->status = 'dev'; + + $phone = new CmsPhonenumber; + $phone->phonenumber = 424242; + + $user->addPhonenumber($phone); + + $this->_em->persist($user); + $this->_em->flush(); + + $this->_em->clear(); + + $rsm = new ResultSetMapping; + $userMetadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $phoneMetadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber'); + $rsm->addRootEntityFromClassMetadata($userMetadata, 'u'); + $rsm->addJoinedEntityFromClassMetadata($phoneMetadata, 'p', 'u', 'phonenumbers', 'p_'); + $query = $this->_em->createNativeQuery('SELECT u.*, p.phonenumber AS p_phonenumber FROM cms_users u INNER JOIN cms_phonenumbers p ON u.id = p.user_id WHERE username = ?', $rsm); + $query->setParameter(1, 'romanb'); + + $users = $query->getResult(); + $this->assertEquals(1, count($users)); + $this->assertTrue($users[0] instanceof CmsUser); + $this->assertEquals('Roman', $users[0]->name); + $this->assertTrue($users[0]->getPhonenumbers() instanceof \Doctrine\ORM\PersistentCollection); + $this->assertTrue($users[0]->getPhonenumbers()->isInitialized()); + $this->assertEquals(1, count($users[0]->getPhonenumbers())); + $phones = $users[0]->getPhonenumbers(); + $this->assertEquals(424242, $phones[0]->phonenumber); + $this->assertTrue($phones[0]->getUser() === $users[0]); + } + + public function testJoinedOneToOneNativeQueryWithRSMHelper() + { + $user = new CmsUser; + $user->name = 'Roman'; + $user->username = 'romanb'; + $user->status = 'dev'; + + $addr = new CmsAddress; + $addr->country = 'germany'; + $addr->zip = 10827; + $addr->city = 'Berlin'; + + + $user->setAddress($addr); + + $this->_em->persist($user); + $this->_em->flush(); + + $this->_em->clear(); + + + $rsm = new ResultSetMapping; + $userMetadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $addressMetadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress'); + $rsm->addRootEntityFromClassMetadata($userMetadata, 'u'); + $rsm->addJoinedEntityFromClassMetadata($addressMetadata, 'a', 'u', 'address', 'a_'); + + $query = $this->_em->createNativeQuery('SELECT u.id, u.name, u.status, a.id AS a_id, a.country AS a_country, a.zip AS a_zip, a.city AS a_city FROM cms_users u INNER JOIN cms_addresses a ON u.id = a.user_id WHERE u.username = ?', $rsm); + $query->setParameter(1, 'romanb'); + + $users = $query->getResult(); + + $this->assertEquals(1, count($users)); + $this->assertTrue($users[0] instanceof CmsUser); + $this->assertEquals('Roman', $users[0]->name); + $this->assertTrue($users[0]->getPhonenumbers() instanceof \Doctrine\ORM\PersistentCollection); + $this->assertFalse($users[0]->getPhonenumbers()->isInitialized()); + $this->assertTrue($users[0]->getAddress() instanceof CmsAddress); + $this->assertTrue($users[0]->getAddress()->getUser() == $users[0]); + $this->assertEquals('germany', $users[0]->getAddress()->getCountry()); + $this->assertEquals(10827, $users[0]->getAddress()->getZipCode()); + $this->assertEquals('Berlin', $users[0]->getAddress()->getCity()); + } } From c46d83514631841800c782890ad27a2b77de200f Mon Sep 17 00:00:00 2001 From: Michael Ridgway Date: Wed, 30 Mar 2011 10:27:31 -0400 Subject: [PATCH 43/71] Moved new functions to ResultSetMappingBuilder class --- lib/Doctrine/ORM/Query/ResultSetMapping.php | 37 +-------- .../ORM/Query/ResultSetMappingBuilder.php | 80 +++++++++++++++++++ .../Tests/ORM/Functional/NativeQueryTest.php | 21 +++-- 3 files changed, 90 insertions(+), 48 deletions(-) create mode 100644 lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index ededf1153..1de2d4459 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -392,39 +392,4 @@ class ResultSetMapping $this->metaMappings[$columnName] = $fieldName; $this->columnOwnerMap[$columnName] = $alias; } - - /** - * Adds a root entity and all of its fields to the result set. - * - * @param ClassMetadata $classMetadata - * @param string $alias The unique alias to use for the root entity. - * @param string $prefix Prefix for columns - */ - public function addRootEntityFromClassMetadata(ClassMetadata $classMetadata, $alias, $prefix = '') - { - $this->addEntityResult($classMetadata->getReflectionClass()->getName(), $alias); - foreach ($classMetadata->getColumnNames() AS $columnName) { - // $columnName should use DBAL\Platforms\AbstractPlatform::getSQLResultCasing() but we don't have the EM - $this->addFieldResult($alias, $prefix . $columnName, $classMetadata->getFieldName($columnName)); - } - } - - /** - * Adds a joined entity and all of its fields to the result set. - * - * @param ClassMetadata $classMetadata - * @param string $alias The unique alias to use for the joined entity. - * @param string $parentAlias The alias of the entity result that is the parent of this joined result. - * @param object $relation The association field that connects the parent entity result with the joined entity result. - * @param string $prefix Prefix for columns - */ - public function addJoinedEntityFromClassMetadata(ClassMetadata $classMetadata, $alias, $parentAlias, $relation, $prefix = '') - { - $this->addJoinedEntityResult($classMetadata->getReflectionClass()->getName(), $alias, $parentAlias, $relation); - foreach ($classMetadata->getColumnNames() AS $columnName) { - // $columnName should use DBAL\Platforms\AbstractPlatform::getSQLResultCasing() but we don't have the EM - $this->addFieldResult($alias, $prefix . $columnName, $classMetadata->getFieldName($columnName)); - } - } -} - +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php new file mode 100644 index 000000000..517fd0036 --- /dev/null +++ b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php @@ -0,0 +1,80 @@ +. + */ + +namespace Doctrine\ORM\Query; + +use Doctrine\ORM\EntityManager; + +/** + * A ResultSetMappingBuilder uses the EntityManager to automatically populate entity fields + * + * @author Michael Ridgway + * @since 2.1 + */ +class ResultSetMappingBuilder extends ResultSetMapping +{ + /** + * @var EntityManager + */ + private $em; + + /** + * @param EntityManager + */ + public function __construct(EntityManager $em) + { + $this->em = $em; + } + + /** + * Adds a root entity and all of its fields to the result set. + * + * @param string $class The class name of the root entity. + * @param string $alias The unique alias to use for the root entity. + * @param string $prefix Prefix for columns + */ + public function addRootEntityFromClassMetadata($class, $alias, $prefix = '') + { + $this->addEntityResult($class, $alias); + $classMetadata = $this->em->getClassMetadata($class); + $platform = $this->em->getConnection()->getDatabasePlatform(); + foreach ($classMetadata->getColumnNames() AS $columnName) { + $this->addFieldResult($alias, $platform->getSQLResultCasing($prefix . $columnName), $classMetadata->getFieldName($columnName)); + } + } + + /** + * Adds a joined entity and all of its fields to the result set. + * + * @param string $class The class name of the joined entity. + * @param string $alias The unique alias to use for the joined entity. + * @param string $parentAlias The alias of the entity result that is the parent of this joined result. + * @param object $relation The association field that connects the parent entity result with the joined entity result. + * @param string $prefix Prefix for columns + */ + public function addJoinedEntityFromClassMetadata($class, $alias, $parentAlias, $relation, $prefix = '') + { + $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation); + $classMetadata = $this->em->getClassMetadata($class); + $platform = $this->em->getConnection()->getDatabasePlatform(); + foreach ($classMetadata->getColumnNames() AS $columnName) { + $this->addFieldResult($alias, $platform->getSQLResultCasing($prefix . $columnName), $classMetadata->getFieldName($columnName)); + } + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php index 65778badf..1b1a713b8 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php @@ -3,6 +3,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\ORM\Query\ResultSetMapping; +use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\Tests\Models\CMS\CmsUser; use Doctrine\Tests\Models\CMS\CmsPhonenumber; use Doctrine\Tests\Models\CMS\CmsAddress; @@ -157,7 +158,7 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertSame($q, $q2); } - public function testJoinedOneToManyNativeQueryWithRSMHelper() + public function testJoinedOneToManyNativeQueryWithRSMBuilder() { $user = new CmsUser; $user->name = 'Roman'; @@ -174,11 +175,9 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->clear(); - $rsm = new ResultSetMapping; - $userMetadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); - $phoneMetadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber'); - $rsm->addRootEntityFromClassMetadata($userMetadata, 'u'); - $rsm->addJoinedEntityFromClassMetadata($phoneMetadata, 'p', 'u', 'phonenumbers', 'p_'); + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber', 'p', 'u', 'phonenumbers', 'p_'); $query = $this->_em->createNativeQuery('SELECT u.*, p.phonenumber AS p_phonenumber FROM cms_users u INNER JOIN cms_phonenumbers p ON u.id = p.user_id WHERE username = ?', $rsm); $query->setParameter(1, 'romanb'); @@ -194,7 +193,7 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertTrue($phones[0]->getUser() === $users[0]); } - public function testJoinedOneToOneNativeQueryWithRSMHelper() + public function testJoinedOneToOneNativeQueryWithRSMBuilder() { $user = new CmsUser; $user->name = 'Roman'; @@ -215,11 +214,9 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->clear(); - $rsm = new ResultSetMapping; - $userMetadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); - $addressMetadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress'); - $rsm->addRootEntityFromClassMetadata($userMetadata, 'u'); - $rsm->addJoinedEntityFromClassMetadata($addressMetadata, 'a', 'u', 'address', 'a_'); + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress', 'a', 'u', 'address', 'a_'); $query = $this->_em->createNativeQuery('SELECT u.id, u.name, u.status, a.id AS a_id, a.country AS a_country, a.zip AS a_zip, a.city AS a_city FROM cms_users u INNER JOIN cms_addresses a ON u.id = a.user_id WHERE u.username = ?', $rsm); $query->setParameter(1, 'romanb'); From b1b17376fffb80aac82ff7631b52457b2ee3e09a Mon Sep 17 00:00:00 2001 From: Michael Ridgway Date: Thu, 31 Mar 2011 17:22:13 -0400 Subject: [PATCH 44/71] Removing left over class import --- lib/Doctrine/ORM/Query/ResultSetMapping.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index 1de2d4459..b10b0d15d 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -19,8 +19,6 @@ namespace Doctrine\ORM\Query; -use Doctrine\ORM\Mapping\ClassMetadata; - /** * A ResultSetMapping describes how a result set of an SQL query maps to a Doctrine result. * From 24a7a72f59f807034ccaa3695f2da67dd2f33d82 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 31 Mar 2011 23:31:58 +0200 Subject: [PATCH 45/71] [DDC-991] add AbstractQuery::getOneResult() method that returns null instead of throwing an exception as getSingleResult() does. --- lib/Doctrine/ORM/AbstractQuery.php | 27 ++++++++- .../Tests/ORM/Functional/QueryTest.php | 60 +++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index bdad77973..628c09c7d 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -1,7 +1,5 @@ execute(array(), self::HYDRATE_SCALAR); } + /** + * Get exactly one result or null. + * + * @throws NonUniqueResultException + * @param int $hydrationMode + * @return mixed + */ + public function getOneResult($hydrationMode = null) + { + $result = $this->execute(array(), $hydrationMode); + + if ($this->_hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) { + return null; + } + + if (is_array($result)) { + if (count($result) > 1) { + throw new NonUniqueResultException; + } + return array_shift($result); + } + + return $result; + } + /** * Gets the single result of the query. * diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index 567b3ff8e..433c902d9 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -378,4 +378,64 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $article); } } + + /** + * @group DDC-991 + */ + public function testGetOneResult() + { + $user = new CmsUser; + $user->name = 'Guilherme'; + $user->username = 'gblanco'; + $user->status = 'developer'; + $this->_em->persist($user); + $this->_em->flush(); + $this->_em->clear(); + + $query = $this->_em->createQuery("select u from Doctrine\Tests\Models\CMS\CmsUser u where u.username = 'gblanco'"); + + $fetchedUser = $query->getOneResult(); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $fetchedUser); + $this->assertEquals('gblanco', $fetchedUser->username); + + $query = $this->_em->createQuery("select u.username from Doctrine\Tests\Models\CMS\CmsUser u where u.username = 'gblanco'"); + $fetchedUsername = $query->getOneResult(Query::HYDRATE_SINGLE_SCALAR); + $this->assertEquals('gblanco', $fetchedUsername); + } + + /** + * @group DDC-991 + */ + public function testGetOneResultSeveralRows() + { + $user = new CmsUser; + $user->name = 'Guilherme'; + $user->username = 'gblanco'; + $user->status = 'developer'; + $this->_em->persist($user); + $user = new CmsUser; + $user->name = 'Roman'; + $user->username = 'romanb'; + $user->status = 'developer'; + $this->_em->persist($user); + $this->_em->flush(); + $this->_em->clear(); + + $query = $this->_em->createQuery("select u from Doctrine\Tests\Models\CMS\CmsUser u"); + + $this->setExpectedException('Doctrine\ORM\NonUniqueResultException'); + $fetchedUser = $query->getOneResult(); + } + + /** + * @group DDC-991 + */ + public function testGetOneResultNoRows() + { + $query = $this->_em->createQuery("select u from Doctrine\Tests\Models\CMS\CmsUser u"); + $this->assertNull($query->getOneResult()); + + $query = $this->_em->createQuery("select u.username from Doctrine\Tests\Models\CMS\CmsUser u where u.username = 'gblanco'"); + $this->assertNull($query->getOneResult(Query::HYDRATE_SCALAR)); + } } \ No newline at end of file From ea52b3cc8f773ddeb00ad4669d94584b9b11d90d Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 31 Mar 2011 23:35:01 +0200 Subject: [PATCH 46/71] [DDC-991] Rename method to AbstractQuery::getOneOrNullResult(). --- lib/Doctrine/ORM/AbstractQuery.php | 2 +- .../Doctrine/Tests/ORM/Functional/QueryTest.php | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 628c09c7d..b52921644 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -420,7 +420,7 @@ abstract class AbstractQuery * @param int $hydrationMode * @return mixed */ - public function getOneResult($hydrationMode = null) + public function getOneOrNullResult($hydrationMode = null) { $result = $this->execute(array(), $hydrationMode); diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index 433c902d9..02b4d267e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -382,7 +382,7 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-991 */ - public function testGetOneResult() + public function testgetOneOrNullResult() { $user = new CmsUser; $user->name = 'Guilherme'; @@ -394,19 +394,19 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $query = $this->_em->createQuery("select u from Doctrine\Tests\Models\CMS\CmsUser u where u.username = 'gblanco'"); - $fetchedUser = $query->getOneResult(); + $fetchedUser = $query->getOneOrNullResult(); $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $fetchedUser); $this->assertEquals('gblanco', $fetchedUser->username); $query = $this->_em->createQuery("select u.username from Doctrine\Tests\Models\CMS\CmsUser u where u.username = 'gblanco'"); - $fetchedUsername = $query->getOneResult(Query::HYDRATE_SINGLE_SCALAR); + $fetchedUsername = $query->getOneOrNullResult(Query::HYDRATE_SINGLE_SCALAR); $this->assertEquals('gblanco', $fetchedUsername); } /** * @group DDC-991 */ - public function testGetOneResultSeveralRows() + public function testgetOneOrNullResultSeveralRows() { $user = new CmsUser; $user->name = 'Guilherme'; @@ -424,18 +424,18 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $query = $this->_em->createQuery("select u from Doctrine\Tests\Models\CMS\CmsUser u"); $this->setExpectedException('Doctrine\ORM\NonUniqueResultException'); - $fetchedUser = $query->getOneResult(); + $fetchedUser = $query->getOneOrNullResult(); } /** * @group DDC-991 */ - public function testGetOneResultNoRows() + public function testgetOneOrNullResultNoRows() { $query = $this->_em->createQuery("select u from Doctrine\Tests\Models\CMS\CmsUser u"); - $this->assertNull($query->getOneResult()); + $this->assertNull($query->getOneOrNullResult()); $query = $this->_em->createQuery("select u.username from Doctrine\Tests\Models\CMS\CmsUser u where u.username = 'gblanco'"); - $this->assertNull($query->getOneResult(Query::HYDRATE_SCALAR)); + $this->assertNull($query->getOneOrNullResult(Query::HYDRATE_SCALAR)); } } \ No newline at end of file From 5784c7bacd4f1f190e0be637b5a22f06675711b0 Mon Sep 17 00:00:00 2001 From: Chekote Date: Fri, 1 Apr 2011 12:54:12 -0500 Subject: [PATCH 47/71] Fixed phpdoc on Parser::match incorrectly stating that the token parameter can be a string value --- lib/Doctrine/ORM/Query/Parser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 2ba869f22..798afa0ce 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -234,7 +234,7 @@ class Parser * If they match, updates the lookahead token; otherwise raises a syntax * error. * - * @param int|string token type or value + * @param int token type * @return void * @throws QueryException If the tokens dont match. */ From a3290075260cdafdd17952ca14daa305fabccfe2 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 3 Apr 2011 09:03:43 +0200 Subject: [PATCH 48/71] [DDC-1087] Add missing resolution to IS NULL in EntityRepository when passing a null value as a criteria. --- .../ORM/Persisters/BasicEntityPersister.php | 6 +++++- .../Tests/ORM/Functional/EntityRepositoryTest.php | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 3213b8670..39bb967e6 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -1206,7 +1206,7 @@ class BasicEntityPersister } else { throw ORMException::unrecognizedField($field); } - $conditionSql .= (is_array($value)) ? ' IN (?)' : ' = ?'; + $conditionSql .= (is_array($value)) ? ' IN (?)' : (($value === null) ? ' IS NULL' : ' = ?'); } return $conditionSql; } @@ -1291,6 +1291,10 @@ class BasicEntityPersister $params = $types = array(); foreach ($criteria AS $field => $value) { + if ($value === null) { + continue; // skip null values. + } + $type = null; if (isset($this->_class->fieldMappings[$field])) { $type = Type::getType($this->_class->fieldMappings[$field]['type'])->getBindingType(); diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index f344ff159..0e38fe209 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -307,5 +307,18 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $repos->createNamedQuery('invalidNamedQuery'); } + + /** + * @group DDC-1087 + */ + public function testIsNullCriteria() + { + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $users = $repos->findBy(array('status' => null, 'username' => 'romanb')); + + $params = $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['params']; + $this->assertEquals(1, count($params), "Should only execute with one parameter."); + $this->assertEquals(array('romanb'), $params); + } } From e685d596042d24eb9864ee4ba0c09dbaf0b37ca6 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 3 Apr 2011 20:29:07 +0200 Subject: [PATCH 49/71] [DDC-1093] Fix docblock type hint --- lib/Doctrine/ORM/EntityManager.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 43b257788..d06d2dea9 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -97,12 +97,16 @@ class EntityManager implements ObjectManager private $proxyFactory; /** - * @var ExpressionBuilder The expression builder instance used to generate query expressions. + * The expression builder instance used to generate query expressions. + * + * @var Doctrine\ORM\Query\Expr */ private $expressionBuilder; /** * Whether the EntityManager is closed or not. + * + * @var bool */ private $closed = false; @@ -164,7 +168,7 @@ class EntityManager implements ObjectManager * ->where($expr->orX($expr->eq('u.id', 1), $expr->eq('u.id', 2))); * * - * @return ExpressionBuilder + * @return Doctrine\ORM\Query\Expr */ public function getExpressionBuilder() { From 7905f2a972ef93f3fc5a528136198a8fb38908d2 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 3 Apr 2011 23:03:39 +0200 Subject: [PATCH 50/71] [DDC-1040] Bugfix with named parameters and multiple entities passed as parameter. --- lib/Doctrine/ORM/Query.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index d428e2ddb..fa7fff2e4 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -249,7 +249,12 @@ final class Query extends AbstractQuery $idValues = $class->getIdentifierValues($value); } $sqlPositions = $paramMappings[$key]; - $sqlParams += array_combine((array)$sqlPositions, $idValues); + $cSqlPos = count($sqlPositions); + $cIdValues = count($idValues); + $idValues = array_values($idValues); + for ($i = 0; $i < $cSqlPos; $i++) { + $sqlParams[$sqlPositions[$i]] = $idValues[ ($i % $cIdValues) ]; + } } else { foreach ($paramMappings[$key] as $position) { $sqlParams[$position] = $value; From 822481d360b20de9683a7949896652602b6c0b01 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 3 Apr 2011 23:06:03 +0200 Subject: [PATCH 51/71] [DDC-1040] Add regression tests for entity as multiple named/positional parameters. --- .../ORM/Functional/Ticket/DDC1040Test.php | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1040Test.php diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1040Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1040Test.php new file mode 100644 index 000000000..21a042187 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1040Test.php @@ -0,0 +1,83 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testReuseNamedEntityParameter() + { + $user = new CmsUser(); + $user->name = "John Galt"; + $user->username = "jgalt"; + $user->status = "inactive"; + + $article = new CmsArticle(); + $article->topic = "This is John Galt speaking!"; + $article->text = "Yadda Yadda!"; + $article->setAuthor($user); + + $this->_em->persist($user); + $this->_em->persist($article); + $this->_em->flush(); + + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.user = :author"; + $this->_em->createQuery($dql) + ->setParameter('author', $user) + ->getResult(); + + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.user = :author AND a.user = :author"; + $this->_em->createQuery($dql) + ->setParameter('author', $user) + ->getResult(); + + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.topic = :topic AND a.user = :author AND a.user = :author AND a.text = :text"; + $farticle = $this->_em->createQuery($dql) + ->setParameter('author', $user) + ->setParameter('topic', 'This is John Galt speaking!') + ->setParameter('text', 'Yadda Yadda!') + ->getSingleResult(); + + $this->assertSame($article, $farticle); + } + + public function testUseMultiplePositionalParameters() + { + $user = new CmsUser(); + $user->name = "John Galt"; + $user->username = "jgalt"; + $user->status = "inactive"; + + $article = new CmsArticle(); + $article->topic = "This is John Galt speaking!"; + $article->text = "Yadda Yadda!"; + $article->setAuthor($user); + + $this->_em->persist($user); + $this->_em->persist($article); + $this->_em->flush(); + + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.topic = ?1 AND a.user = ?2 AND a.user = ?3 AND a.text = ?4"; + $farticle = $this->_em->createQuery($dql) + ->setParameter(1, 'This is John Galt speaking!') + ->setParameter(2, $user) + ->setParameter(3, $user) + ->setParameter(4, 'Yadda Yadda!') + ->getSingleResult(); + + $this->assertSame($article, $farticle); + } +} \ No newline at end of file From af4cf0d0ba8cd5d38689db8b9063ef8d8d1ae8d7 Mon Sep 17 00:00:00 2001 From: Michael Ridgway Date: Thu, 14 Apr 2011 20:55:03 -0400 Subject: [PATCH 52/71] Replaced prefix parameter with renamedColumns; Added exception when duplicate columns found --- .../ORM/Query/ResultSetMappingBuilder.php | 32 +++++++++++++++---- .../Tests/ORM/Functional/NativeQueryTest.php | 20 +++++++++--- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php index 517fd0036..ceacc9638 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php +++ b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php @@ -47,15 +47,25 @@ class ResultSetMappingBuilder extends ResultSetMapping * * @param string $class The class name of the root entity. * @param string $alias The unique alias to use for the root entity. - * @param string $prefix Prefix for columns + * @param array $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName) */ - public function addRootEntityFromClassMetadata($class, $alias, $prefix = '') + public function addRootEntityFromClassMetadata($class, $alias, $renamedColumns = array()) { $this->addEntityResult($class, $alias); $classMetadata = $this->em->getClassMetadata($class); + if ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined()) { + throw new \InvalidArgumentException('ResultSetMapping builder does not currently support inheritance.'); + } $platform = $this->em->getConnection()->getDatabasePlatform(); foreach ($classMetadata->getColumnNames() AS $columnName) { - $this->addFieldResult($alias, $platform->getSQLResultCasing($prefix . $columnName), $classMetadata->getFieldName($columnName)); + $propertyName = $classMetadata->getFieldName($columnName); + if (isset($renamedColumns[$columnName])) { + $columnName = $renamedColumns[$columnName]; + } + if (isset($this->fieldMappings[$columnName])) { + throw new \InvalidArgumentException("The column '$columnName' conflicts with another column in the mapper."); + } + $this->addFieldResult($alias, $platform->getSQLResultCasing($columnName), $propertyName); } } @@ -66,15 +76,25 @@ class ResultSetMappingBuilder extends ResultSetMapping * @param string $alias The unique alias to use for the joined entity. * @param string $parentAlias The alias of the entity result that is the parent of this joined result. * @param object $relation The association field that connects the parent entity result with the joined entity result. - * @param string $prefix Prefix for columns + * @param array $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName) */ - public function addJoinedEntityFromClassMetadata($class, $alias, $parentAlias, $relation, $prefix = '') + public function addJoinedEntityFromClassMetadata($class, $alias, $parentAlias, $relation, $renamedColumns = array()) { $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation); $classMetadata = $this->em->getClassMetadata($class); + if ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined()) { + throw new \InvalidArgumentException('ResultSetMapping builder does not currently support inheritance.'); + } $platform = $this->em->getConnection()->getDatabasePlatform(); foreach ($classMetadata->getColumnNames() AS $columnName) { - $this->addFieldResult($alias, $platform->getSQLResultCasing($prefix . $columnName), $classMetadata->getFieldName($columnName)); + $propertyName = $classMetadata->getFieldName($columnName); + if (isset($renamedColumns[$columnName])) { + $columnName = $renamedColumns[$columnName]; + } + if (isset($this->fieldMappings[$columnName])) { + throw new \InvalidArgumentException("The column '$columnName' conflicts with another column in the mapper."); + } + $this->addFieldResult($alias, $platform->getSQLResultCasing($columnName), $propertyName); } } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php index 1b1a713b8..2a09b8310 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php @@ -7,6 +7,8 @@ use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\Tests\Models\CMS\CmsUser; use Doctrine\Tests\Models\CMS\CmsPhonenumber; use Doctrine\Tests\Models\CMS\CmsAddress; +use Doctrine\Tests\Models\Company\CompanyFixContract; +use Doctrine\Tests\Models\Company\CompanyEmployee; require_once __DIR__ . '/../../TestInit.php'; @@ -177,8 +179,8 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $rsm = new ResultSetMappingBuilder($this->_em); $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); - $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber', 'p', 'u', 'phonenumbers', 'p_'); - $query = $this->_em->createNativeQuery('SELECT u.*, p.phonenumber AS p_phonenumber FROM cms_users u INNER JOIN cms_phonenumbers p ON u.id = p.user_id WHERE username = ?', $rsm); + $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber', 'p', 'u', 'phonenumbers'); + $query = $this->_em->createNativeQuery('SELECT u.*, p.* FROM cms_users u LEFT JOIN cms_phonenumbers p ON u.id = p.user_id WHERE username = ?', $rsm); $query->setParameter(1, 'romanb'); $users = $query->getResult(); @@ -216,9 +218,9 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $rsm = new ResultSetMappingBuilder($this->_em); $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); - $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress', 'a', 'u', 'address', 'a_'); + $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress', 'a', 'u', 'address', array('id' => 'a_id')); - $query = $this->_em->createNativeQuery('SELECT u.id, u.name, u.status, a.id AS a_id, a.country AS a_country, a.zip AS a_zip, a.city AS a_city FROM cms_users u INNER JOIN cms_addresses a ON u.id = a.user_id WHERE u.username = ?', $rsm); + $query = $this->_em->createNativeQuery('SELECT u.*, a.*, a.id AS a_id FROM cms_users u INNER JOIN cms_addresses a ON u.id = a.user_id WHERE u.username = ?', $rsm); $query->setParameter(1, 'romanb'); $users = $query->getResult(); @@ -234,5 +236,15 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(10827, $users[0]->getAddress()->getZipCode()); $this->assertEquals('Berlin', $users[0]->getAddress()->getCity()); } + + /** + * @expectedException \InvalidArgumentException + */ + public function testRSMBuilderThrowsExceptionOnColumnConflict() + { + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress', 'a', 'u', 'address'); + } } From 0b7feb359dff7ab499dd7024fbf8df718018a7de Mon Sep 17 00:00:00 2001 From: Rafael Dohms Date: Sun, 17 Apr 2011 23:39:59 -0300 Subject: [PATCH 53/71] Fixing outdated docblocks for SchemaTool --- lib/Doctrine/ORM/Tools/SchemaTool.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index 1edbd38a1..f7e3e9d7c 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -648,9 +648,11 @@ class SchemaTool /** * Updates the database schema of the given classes by comparing the ClassMetadata - * ins$tableNametances to the current database schema that is inspected. + * instances to the current database schema that is inspected. If $saveMode is set + * to true the command is executed in the Database, else SQL is returned. * * @param array $classes + * @param boolean $saveMode * @return void */ public function updateSchema(array $classes, $saveMode=false) @@ -666,8 +668,11 @@ class SchemaTool /** * Gets the sequence of SQL statements that need to be performed in order * to bring the given class mappings in-synch with the relational schema. + * If $saveMode is set to true the command is executed in the Database, + * else SQL is returned. * * @param array $classes The classes to consider. + * @param boolean $saveMode True for writing to DB, false for SQL string * @return array The sequence of SQL statements. */ public function getUpdateSchemaSql(array $classes, $saveMode=false) From 26bd3e381195ba0901c506e8257e0eb6e5d5c931 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Mon, 25 Apr 2011 18:32:43 -0300 Subject: [PATCH 54/71] Implemented support for closure return on EntityManager::transactional. Fixes DDC-1125 --- lib/Doctrine/ORM/EntityManager.php | 7 ++++++- tests/Doctrine/Tests/ORM/EntityManagerTest.php | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index d06d2dea9..0379cc435 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -203,13 +203,18 @@ class EntityManager implements ObjectManager public function transactional(Closure $func) { $this->conn->beginTransaction(); + try { - $func($this); + $return = $func($this); + $this->flush(); $this->conn->commit(); + + return $return ?: true; } catch (Exception $e) { $this->close(); $this->conn->rollback(); + throw $e; } } diff --git a/tests/Doctrine/Tests/ORM/EntityManagerTest.php b/tests/Doctrine/Tests/ORM/EntityManagerTest.php index 8f9045c34..ca896ad10 100644 --- a/tests/Doctrine/Tests/ORM/EntityManagerTest.php +++ b/tests/Doctrine/Tests/ORM/EntityManagerTest.php @@ -143,4 +143,16 @@ class EntityManagerTest extends \Doctrine\Tests\OrmTestCase $this->_em->close(); $this->_em->$methodName(new \stdClass()); } + + /** + * @group DDC-1125 + */ + public function testTransactionalAcceptsReturn() + { + $return = $this->_em->transactional(function ($em) { + return 'foo'; + }); + + $this->assertEquals('foo', $return); + } } \ No newline at end of file From fe66d8bc04de42ab13b83892df11a5ea1533b544 Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Tue, 26 Apr 2011 12:32:04 -0300 Subject: [PATCH 55/71] Fixed SchemaTool which was failing to dropSchema due to foreignKeyContraint checks. Fixes DDC-1126 --- lib/Doctrine/ORM/Tools/SchemaTool.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index 1edbd38a1..f1358708b 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -598,7 +598,11 @@ class SchemaTool */ public function getDropSchemaSQL(array $classes) { - $sm = $this->_em->getConnection()->getSchemaManager(); + /* @var $conn \Doctrine\DBAL\Connection */ + $conn = $this->_em->getConnection(); + + /* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */ + $sm = $conn->getSchemaManager(); $sql = array(); $orderedTables = array(); @@ -633,13 +637,18 @@ class SchemaTool } } + $supportsForeignKeyConstraints = $conn->getDatabasePlatform()->supportsForeignKeyConstraints(); $dropTablesSql = array(); + foreach ($orderedTables AS $tableName) { - /* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */ - $foreignKeys = $sm->listTableForeignKeys($tableName); - foreach ($foreignKeys AS $foreignKey) { - $sql[] = $this->_platform->getDropForeignKeySQL($foreignKey, $tableName); + if ($supportsForeignKeyConstraints) { + $foreignKeys = $sm->listTableForeignKeys($tableName); + + foreach ($foreignKeys AS $foreignKey) { + $sql[] = $this->_platform->getDropForeignKeySQL($foreignKey, $tableName); + } } + $dropTablesSql[] = $this->_platform->getDropTableSQL($tableName); } From 41b3a372d3306cddfe54aeb3a085f8c5300dfcff Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 30 Apr 2011 10:47:56 +0200 Subject: [PATCH 56/71] Add new performance test checking compute changeset performance. --- .../Performance/UnitOfWorkPerformanceTest.php | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Performance/UnitOfWorkPerformanceTest.php diff --git a/tests/Doctrine/Tests/ORM/Performance/UnitOfWorkPerformanceTest.php b/tests/Doctrine/Tests/ORM/Performance/UnitOfWorkPerformanceTest.php new file mode 100644 index 000000000..28d9ea6d3 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Performance/UnitOfWorkPerformanceTest.php @@ -0,0 +1,50 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testComputeChanges() + { + $n = 100; + + $users = array(); + for ($i=1; $i<=$n; ++$i) { + $user = new CmsUser; + $user->status = 'user'; + $user->username = 'user' . $i; + $user->name = 'Mr.Smith-' . $i; + $this->_em->persist($user); + $users[] = $user; + } + $this->_em->flush(); + + + foreach ($users AS $user) { + $user->status = 'other'; + $user->username = $user->username . '++'; + $user->name = str_replace('Mr.', 'Mrs.', $user->name); + } + + $s = microtime(true); + $this->_em->flush(); + $e = microtime(true); + + echo ' Compute ChangeSet '.$n.' objects in ' . ($e - $s) . ' seconds' . PHP_EOL; + } +} \ No newline at end of file From f09d2996606a19969ca03685adabc6e11faccedc Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 30 Apr 2011 11:15:45 +0200 Subject: [PATCH 57/71] [DDC-1132] Fix many to many table detection. --- .../ORM/Functional/DatabaseDriverTest.php | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php index 964c57119..37ce0b6ea 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php @@ -104,9 +104,42 @@ class DatabaseDriverTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertArrayHasKey('user', $metadatas['CmsGroups']->associationMappings); } + public function testIgnoreManyToManyTableWithoutFurtherForeignKeyDetails() + { + $tableB = new \Doctrine\DBAL\Schema\Table("dbdriver_bar"); + $tableB->addColumn('id', 'integer'); + $tableB->setPrimaryKey(array('id')); + + $tableA = new \Doctrine\DBAL\Schema\Table("dbdriver_baz"); + $tableA->addColumn('id', 'integer'); + $tableA->setPrimaryKey(array('id')); + + $tableMany = new \Doctrine\DBAL\Schema\Table("dbdriver_bar_baz"); + $tableMany->addColumn('bar_id', 'integer'); + $tableMany->addColumn('baz_id', 'integer'); + $tableMany->addForeignKeyConstraint('dbdriver_bar', array('bar_id'), array('id')); + + $metadatas = $this->convertToClassMetadata(array($tableA, $tableB), array($tableMany)); + + $this->assertEquals(0, count($metadatas['DbdriverBaz']->associationMappings), "no association mappings should be detected."); + } + + protected function convertToClassMetadata(array $entityTables, array $manyTables = array()) + { + $driver = new \Doctrine\ORM\Mapping\Driver\DatabaseDriver($this->_sm); + $driver->setTables($entityTables, $manyTables); + + $metadatas = array(); + foreach ($driver->getAllClassNames() AS $className) { + $class = new ClassMetadataInfo($className); + $driver->loadMetadataForClass($className, $class); + $metadatas[$className] = $class; + } + + return $metadatas; + } /** - * * @param string $className * @return ClassMetadata */ From 42230a4c511d9980ba975bea636b0c744867ade8 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 30 Apr 2011 11:16:30 +0200 Subject: [PATCH 58/71] [DDC-1132] Fix many to many table detection. --- .../ORM/Mapping/Driver/DatabaseDriver.php | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php index 0dc6a05e0..7f92df162 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php @@ -67,6 +67,26 @@ class DatabaseDriver implements Driver $this->_sm = $schemaManager; } + /** + * Set tables manually instead of relying on the reverse engeneering capabilities of SchemaManager. + * + * @param array $entityTables + * @param array $manyToManyTables + * @return void + */ + public function setTables($entityTables, $manyToManyTables) + { + $this->tables = $this->manyToManyTables = $this->classToTableNames = array(); + foreach ($entityTables AS $table) { + $className = Inflector::classify(strtolower($table->getName())); + $this->classToTableNames[$className] = $table->getName(); + $this->tables[$table->getName()] = $table; + } + foreach ($manyToManyTables AS $table) { + $this->manyToManyTables[$table->getName()] = $table; + } + } + private function reverseEngineerMappingFromDatabase() { if ($this->tables !== null) { @@ -77,7 +97,7 @@ class DatabaseDriver implements Driver $tables[$tableName] = $this->_sm->listTableDetails($tableName); } - $this->tables = array(); + $this->tables = $this->manyToManyTables = $this->classToTableNames = array(); foreach ($tables AS $tableName => $table) { /* @var $table Table */ if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { @@ -95,11 +115,7 @@ class DatabaseDriver implements Driver sort($pkColumns); sort($allForeignKeyColumns); - if ($pkColumns == $allForeignKeyColumns) { - if (count($table->getForeignKeys()) > 2) { - throw new \InvalidArgumentException("ManyToMany table '" . $tableName . "' with more or less than two foreign keys are not supported by the Database Reverese Engineering Driver."); - } - + if ($pkColumns == $allForeignKeyColumns && count($foreignKeys) == 2) { $this->manyToManyTables[$tableName] = $table; } else { // lower-casing is necessary because of Oracle Uppercase Tablenames, @@ -191,8 +207,10 @@ class DatabaseDriver implements Driver foreach ($this->manyToManyTables AS $manyTable) { foreach ($manyTable->getForeignKeys() AS $foreignKey) { + // foreign key maps to the table of the current entity, many to many association probably exists if (strtolower($tableName) == strtolower($foreignKey->getForeignTableName())) { $myFk = $foreignKey; + $otherFk = null; foreach ($manyTable->getForeignKeys() AS $foreignKey) { if ($foreignKey != $myFk) { $otherFk = $foreignKey; @@ -200,6 +218,12 @@ class DatabaseDriver implements Driver } } + if (!$otherFk) { + // the definition of this many to many table does not contain + // enough foreign key information to continue reverse engeneering. + continue; + } + $localColumn = current($myFk->getColumns()); $associationMapping = array(); $associationMapping['fieldName'] = Inflector::camelize(str_replace('_id', '', strtolower(current($otherFk->getColumns())))); From 67b89eaa4f46348cdca71d92bebd0118b67aaf87 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 30 Apr 2011 12:27:16 +0200 Subject: [PATCH 59/71] [DDC-1108] Fix bug with single char named input parameters in DQL lexer. --- lib/Doctrine/ORM/Query/Lexer.php | 2 +- .../Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Query/Lexer.php b/lib/Doctrine/ORM/Query/Lexer.php index 673ab0205..82355faec 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -126,7 +126,7 @@ class Lexer extends \Doctrine\Common\Lexer '[a-z_\\\][a-z0-9_\:\\\]*[a-z0-9_]{1}', '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', "'(?:[^']|'')*'", - '\?[1-9][0-9]*|:[a-z][a-z0-9_]+' + '\?[1-9][0-9]*|:[a-z]{1}[a-z0-9_]{0,}' ); } diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index 1ac05df90..8dccd2cba 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -513,6 +513,14 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertInvalidDQL('SELECT g FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.groups g'); } + /** + * @group DDC-1108 + */ + public function testInputParameterSingleChar() + { + $this->assertValidDQL('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name = :q'); + } + /** * @group DDC-1053 */ From 7dd0dd273ecdf6edf7c4d201c6d07ab06b05c256 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 30 Apr 2011 12:49:37 +0200 Subject: [PATCH 60/71] [DDC-1109] ltrim discriminator map for convenience. --- lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index a10d8c866..f556257f9 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -1628,6 +1628,7 @@ class ClassMetadataInfo implements ClassMetadata if (strpos($className, '\\') === false && strlen($this->namespace)) { $className = $this->namespace . '\\' . $className; } + $className = ltrim($className, '\\'); $this->discriminatorMap[$value] = $className; if ($this->name == $className) { $this->discriminatorValue = $value; From 261d3c892eb167f9069d7ebf23b3363594e6dbc4 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 30 Apr 2011 14:23:46 +0200 Subject: [PATCH 61/71] DDC-1133 - Ducktype AnnotationReader in AnnotationDriver --- lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index b74fc860e..22e6d4d7e 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -67,10 +67,10 @@ class AnnotationDriver implements Driver * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading * docblock annotations. * - * @param $reader The AnnotationReader to use. + * @param AnnotationReader $reader The AnnotationReader to use, duck-typed. * @param string|array $paths One or multiple paths where mapping classes can be found. */ - public function __construct(AnnotationReader $reader, $paths = null) + public function __construct($reader, $paths = null) { $this->_reader = $reader; if ($paths) { From 1f665e6ba8fbf67608dae82e8f35aab76129d2c3 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 30 Apr 2011 16:20:14 +0200 Subject: [PATCH 62/71] [DBAL-115] Bugfix in SchemaTool not quoting table names when dropping schema. --- lib/Doctrine/ORM/Tools/SchemaTool.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index ffeb15859..b52c584a4 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -617,9 +617,10 @@ class SchemaTool $associationTables = $this->_getAssociationTables($commitOrder); // Drop association tables first - foreach ($associationTables as $associationTable) { - if (!in_array($associationTable, $orderedTables)) { - $orderedTables[] = $associationTable; + foreach ($associationTables as $quotedAssociationTableName) { + // avoid adding duplicates + if (!in_array($quotedAssociationTableName, $orderedTables)) { + $orderedTables[] = $quotedAssociationTableName; } } @@ -633,7 +634,7 @@ class SchemaTool } if (!in_array($class->getTableName(), $orderedTables)) { - $orderedTables[] = $class->getTableName(); + $orderedTables[] = $class->getQuotedTableName($this->_platform); } } @@ -731,9 +732,10 @@ class SchemaTool $associationTables = array(); foreach ($classes as $class) { + /* @var $class \Doctrine\ORM\Mapping\ClassMetadata */ foreach ($class->associationMappings as $assoc) { if ($assoc['isOwningSide'] && $assoc['type'] == ClassMetadata::MANY_TO_MANY) { - $associationTables[] = $assoc['joinTable']['name']; + $associationTables[] = $class->getQuotedJoinTableName($assoc, $this->_platform); } } } From 5179ff921bd3dc6da953a66f545aa49e6d6ace43 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 30 Apr 2011 17:16:34 +0200 Subject: [PATCH 63/71] [DBAL-115] REALLY fix issues with SchemaTool::getDropSchemaSQL(). --- lib/Doctrine/ORM/Tools/SchemaTool.php | 112 +++--------------- .../SchemaTool/CompanySchemaTest.php | 15 +++ 2 files changed, 34 insertions(+), 93 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index b52c584a4..0ece01500 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -592,68 +592,35 @@ class SchemaTool } /** - * + * Get SQL to drop the tables defined by the passed classes. + * * @param array $classes * @return array */ public function getDropSchemaSQL(array $classes) { - /* @var $conn \Doctrine\DBAL\Connection */ - $conn = $this->_em->getConnection(); - - /* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */ - $sm = $conn->getSchemaManager(); - - $sql = array(); - $orderedTables = array(); + $visitor = new \Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector($this->_platform); + $schema = $this->getSchemaFromMetadata($classes); - foreach ($classes AS $class) { - if ($class->isIdGeneratorSequence() && !$class->isMappedSuperclass && $class->name == $class->rootEntityName && $this->_platform->supportsSequences()) { - $sql[] = $this->_platform->getDropSequenceSQL($class->sequenceGeneratorDefinition['sequenceName']); - } - } - - $commitOrder = $this->_getCommitOrder($classes); - $associationTables = $this->_getAssociationTables($commitOrder); - - // Drop association tables first - foreach ($associationTables as $quotedAssociationTableName) { - // avoid adding duplicates - if (!in_array($quotedAssociationTableName, $orderedTables)) { - $orderedTables[] = $quotedAssociationTableName; - } - } - - // Drop tables in reverse commit order - for ($i = count($commitOrder) - 1; $i >= 0; --$i) { - $class = $commitOrder[$i]; - - if (($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName) - || $class->isMappedSuperclass) { - continue; - } - - if (!in_array($class->getTableName(), $orderedTables)) { - $orderedTables[] = $class->getQuotedTableName($this->_platform); - } - } - - $supportsForeignKeyConstraints = $conn->getDatabasePlatform()->supportsForeignKeyConstraints(); - $dropTablesSql = array(); - - foreach ($orderedTables AS $tableName) { - if ($supportsForeignKeyConstraints) { - $foreignKeys = $sm->listTableForeignKeys($tableName); - - foreach ($foreignKeys AS $foreignKey) { - $sql[] = $this->_platform->getDropForeignKeySQL($foreignKey, $tableName); + $sm = $this->_em->getConnection()->getSchemaManager(); + $fullSchema = $sm->createSchema(); + foreach ($fullSchema->getTables() AS $table) { + if (!$schema->hasTable($table->getName())) { + foreach ($table->getForeignKeys() AS $foreignKey) { + /* @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */ + if ($schema->hasTable($foreignKey->getForeignTableName())) { + $visitor->acceptForeignKey($table, $foreignKey); + } + } + } else { + $visitor->acceptTable($table); + foreach ($table->getForeignKeys() AS $foreignKey) { + $visitor->acceptForeignKey($table, $foreignKey); } } - - $dropTablesSql[] = $this->_platform->getDropTableSQL($tableName); } - return array_merge($sql, $dropTablesSql); + return $visitor->getQueries(); } /** @@ -701,45 +668,4 @@ class SchemaTool return $schemaDiff->toSql($this->_platform); } } - - private function _getCommitOrder(array $classes) - { - $calc = new CommitOrderCalculator; - - // Calculate dependencies - foreach ($classes as $class) { - $calc->addClass($class); - - foreach ($class->associationMappings as $assoc) { - if ($assoc['isOwningSide']) { - $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); - - if ( ! $calc->hasClass($targetClass->name)) { - $calc->addClass($targetClass); - } - - // add dependency ($targetClass before $class) - $calc->addDependency($targetClass, $class); - } - } - } - - return $calc->getCommitOrder(); - } - - private function _getAssociationTables(array $classes) - { - $associationTables = array(); - - foreach ($classes as $class) { - /* @var $class \Doctrine\ORM\Mapping\ClassMetadata */ - foreach ($class->associationMappings as $assoc) { - if ($assoc['isOwningSide'] && $assoc['type'] == ClassMetadata::MANY_TO_MANY) { - $associationTables[] = $class->getQuotedJoinTableName($assoc, $this->_platform); - } - } - } - - return $associationTables; - } } diff --git a/tests/Doctrine/Tests/ORM/Functional/SchemaTool/CompanySchemaTest.php b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/CompanySchemaTest.php index 3a14056eb..797c202f6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SchemaTool/CompanySchemaTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/CompanySchemaTest.php @@ -50,4 +50,19 @@ class CompanySchemaTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertFalse($table->getColumn('pricePerHour')->getNotnull()); $this->assertFalse($table->getColumn('maxPrice')->getNotnull()); } + + /** + * @group DBAL-115 + */ + public function testDropPartSchemaWithForeignKeys() + { + if (!$this->_em->getConnection()->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $this->markTestSkipped("Foreign Key test"); + } + + $sql = $this->_schemaTool->getDropSchemaSQL(array( + $this->_em->getClassMetadata('Doctrine\Tests\Models\Company\CompanyManager'), + )); + $this->assertEquals(3, count($sql)); + } } \ No newline at end of file From 73c7605a5c067067993d2dc9f63a225628f0ba42 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 30 Apr 2011 23:18:24 +0200 Subject: [PATCH 64/71] [DDC-1094] Add support for limit, offset and orderby in EntityRepository::findBy(). --- lib/Doctrine/ORM/EntityRepository.php | 9 +++-- .../ORM/Persisters/BasicEntityPersister.php | 21 ++++++----- .../Persisters/JoinedSubclassPersister.php | 8 ++--- lib/vendor/doctrine-common | 2 +- .../ORM/Functional/EntityRepositoryTest.php | 35 +++++++++++++++++++ 5 files changed, 58 insertions(+), 17 deletions(-) diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index 7bc54c847..de2689db2 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -160,11 +160,14 @@ class EntityRepository implements ObjectRepository * Finds entities by a set of criteria. * * @param array $criteria - * @return array + * @param array|null $orderBy + * @param int|null $limit + * @param int|null $offset + * @return array The objects. */ - public function findBy(array $criteria) + public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) { - return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->loadAll($criteria); + return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->loadAll($criteria, $orderBy, $limit, $offset); } /** diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 39bb967e6..454078dfe 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -680,12 +680,15 @@ class BasicEntityPersister * Loads a list of entities by a list of field criteria. * * @param array $criteria + * @param array $orderBy + * @param int $limit + * @param int $offset * @return array */ - public function loadAll(array $criteria = array()) + public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null) { $entities = array(); - $sql = $this->_getSelectEntitiesSQL($criteria); + $sql = $this->_getSelectEntitiesSQL($criteria, null, 0, $limit, $offset, $orderBy); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); @@ -831,19 +834,21 @@ class BasicEntityPersister * @param AssociationMapping $assoc * @param string $orderBy * @param int $lockMode + * @param int $limit + * @param int $offset + * @param array $orderBy * @return string * @todo Refactor: _getSelectSQL(...) */ - protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null) + protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) { $joinSql = $assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY ? $this->_getSelectManyToManyJoinSQL($assoc) : ''; $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc); - $orderBySql = $assoc !== null && isset($assoc['orderBy']) ? - $this->_getCollectionOrderBySQL($assoc['orderBy'], $this->_getSQLTableAlias($this->_class->name)) - : ''; + $orderBy = ($assoc !== null && isset($assoc['orderBy'])) ? $assoc['orderBy'] : $orderBy; + $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $this->_getSQLTableAlias($this->_class->name)) : ''; $lockSql = ''; if ($lockMode == LockMode::PESSIMISTIC_READ) { @@ -869,12 +874,12 @@ class BasicEntityPersister * @return string * @todo Rename: _getOrderBySQL */ - protected final function _getCollectionOrderBySQL(array $orderBy, $baseTableAlias) + protected final function _getOrderBySQL(array $orderBy, $baseTableAlias) { $orderBySql = ''; foreach ($orderBy as $fieldName => $orientation) { if ( ! isset($this->_class->fieldMappings[$fieldName])) { - ORMException::unrecognizedField($fieldName); + throw ORMException::unrecognizedField($fieldName); } $tableAlias = isset($this->_class->fieldMappings[$fieldName]['inherited']) ? diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 2490085cf..1cb73a9d6 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -238,7 +238,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister /** * {@inheritdoc} */ - protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null) + protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) { $idColumns = $this->_class->getIdentifierColumnNames(); $baseTableAlias = $this->_getSQLTableAlias($this->_class->name); @@ -343,10 +343,8 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc); - $orderBySql = ''; - if ($assoc != null && isset($assoc['orderBy'])) { - $orderBySql = $this->_getCollectionOrderBySQL($assoc['orderBy'], $baseTableAlias); - } + $orderBy = ($assoc !== null && isset($assoc['orderBy'])) ? $assoc['orderBy'] : $orderBy; + $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $baseTableAlias) : ''; if ($this->_selectColumnListSql === null) { $this->_selectColumnListSql = $columnList; diff --git a/lib/vendor/doctrine-common b/lib/vendor/doctrine-common index ba63ae0f0..076a03f8f 160000 --- a/lib/vendor/doctrine-common +++ b/lib/vendor/doctrine-common @@ -1 +1 @@ -Subproject commit ba63ae0f0b6b62a2a8617f01386698730ff2b713 +Subproject commit 076a03f8f40b6e08f0ae2f4ee2678474e64b6f59 diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index 0e38fe209..6c620db38 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -320,5 +320,40 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(1, count($params), "Should only execute with one parameter."); $this->assertEquals(array('romanb'), $params); } + + /** + * @group DDC-1094 + */ + public function testFindByLimitOffset() + { + $this->loadFixture(); + + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + + $users1 = $repos->findBy(array(), null, 1, 0); + $users2 = $repos->findBy(array(), null, 1, 1); + + $this->assertEquals(2, count($repos->findBy(array()))); + $this->assertEquals(1, count($users1)); + $this->assertEquals(1, count($users2)); + $this->assertNotSame($users1[0], $users2[0]); + } + + /** + * @group DDC-1094 + */ + public function testFindByOrderBy() + { + $this->loadFixture(); + + $repos = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser'); + $usersAsc = $repos->findBy(array(), array("username" => "ASC")); + $usersDesc = $repos->findBy(array(), array("username" => "DESC")); + + $this->assertEquals(2, count($usersAsc), "Pre-condition: only two users in fixture"); + $this->assertEquals(2, count($usersDesc), "Pre-condition: only two users in fixture"); + $this->assertSame($usersAsc[0], $usersDesc[1]); + $this->assertSame($usersAsc[1], $usersDesc[0]); + } } From 0c955fe54fd4d8cec8f5284612feef09f7a2f0c8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 30 Apr 2011 10:42:38 +0200 Subject: [PATCH 65/71] Fix namespace/class parsing in the entity generator --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 38 +++++++++++---- .../Tests/ORM/Tools/EntityGeneratorTest.php | 48 +++++++++++++++++++ 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 182dd441b..9fbc21780 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -179,7 +179,7 @@ public function () $this->_isNew = !file_exists($path) || (file_exists($path) && $this->_regenerateEntityIfExists); if ( ! $this->_isNew) { - $this->_parseTokensInEntityFile($path); + $this->_parseTokensInEntityFile(file_get_contents($path)); } if ($this->_backupExisting && file_exists($path)) { @@ -400,24 +400,42 @@ public function () /** * @todo this won't work if there is a namespace in brackets and a class outside of it. - * @param string $path + * @param string $src */ - private function _parseTokensInEntityFile($path) + private function _parseTokensInEntityFile($src) { - $tokens = token_get_all(file_get_contents($path)); + $tokens = token_get_all($src); $lastSeenNamespace = ""; $lastSeenClass = false; + $inNamespace = false; + $inClass = false; for ($i = 0; $i < count($tokens); $i++) { $token = $tokens[$i]; - if ($token[0] == T_NAMESPACE) { - $lastSeenNamespace = $tokens[$i+2][1] . "\\"; - } else if ($token[0] == T_NS_SEPARATOR) { - $lastSeenNamespace .= $tokens[$i+1][1] . "\\"; - } else if ($token[0] == T_CLASS) { - $lastSeenClass = $lastSeenNamespace . $tokens[$i+2][1]; + if (in_array($token[0], array(T_WHITESPACE, T_COMMENT, T_DOC_COMMENT))) { + continue; + } + + if ($inNamespace) { + if ($token[0] == T_NS_SEPARATOR || $token[0] == T_STRING) { + $lastSeenNamespace .= $token[1]; + } else if (is_string($token) && in_array($token, array(';', '{'))) { + $inNamespace = false; + } + } + + if ($inClass) { + $inClass = false; + $lastSeenClass = $lastSeenNamespace . '\\' . $token[1]; $this->_staticReflection[$lastSeenClass]['properties'] = array(); $this->_staticReflection[$lastSeenClass]['methods'] = array(); + } + + if ($token[0] == T_NAMESPACE) { + $lastSeenNamespace = ""; + $inNamespace = true; + } else if ($token[0] == T_CLASS) { + $inClass = true; } else if ($token[0] == T_FUNCTION) { if ($tokens[$i+2][0] == T_STRING) { $this->_staticReflection[$lastSeenClass]['methods'][] = $tokens[$i+2][1]; diff --git a/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php b/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php index c8960f6a7..5a9c5d7eb 100644 --- a/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/EntityGeneratorTest.php @@ -200,6 +200,54 @@ class EntityGeneratorTest extends \Doctrine\Tests\OrmTestCase $this->assertEquals($cm->idGenerator, $metadata->idGenerator); $this->assertEquals($cm->customRepositoryClassName, $metadata->customRepositoryClassName); } + + /** + * @dataProvider getParseTokensInEntityFileData + */ + public function testParseTokensInEntityFile($php, $classes) + { + $r = new \ReflectionObject($this->_generator); + $m = $r->getMethod('_parseTokensInEntityFile'); + $m->setAccessible(true); + + $p = $r->getProperty('_staticReflection'); + $p->setAccessible(true); + + $ret = $m->invoke($this->_generator, $php); + $this->assertEquals($classes, array_keys($p->getValue($this->_generator))); + } + + public function getParseTokensInEntityFileData() + { + return array( + array( + ' Date: Sun, 1 May 2011 00:17:40 +0200 Subject: [PATCH 66/71] [PR-39] Throw exception when hydrating joined entity without existing parent alias (NativeQuery problem only) --- .../ORM/Internal/Hydration/ObjectHydrator.php | 28 +++++++++++-------- .../Tests/ORM/Functional/NativeQueryTest.php | 19 +++++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index a820832f5..381fd4ab1 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -39,9 +39,9 @@ class ObjectHydrator extends AbstractHydrator * This local cache is maintained between hydration runs and not cleared. */ private $_ce = array(); - + /* The following parts are reinitialized on every hydration run. */ - + private $_identifierMap; private $_resultPointers; private $_idTemplate; @@ -50,7 +50,7 @@ class ObjectHydrator extends AbstractHydrator private $_initializedCollections = array(); private $_existingCollections = array(); //private $_createdEntities; - + /** @override */ protected function _prepare() @@ -71,10 +71,14 @@ class ObjectHydrator extends AbstractHydrator if ( ! isset($this->_ce[$className])) { $this->_ce[$className] = $class; } - + // Remember which associations are "fetch joined", so that we know where to inject // collection stubs or proxies and where not. if (isset($this->_rsm->relationMap[$dqlAlias])) { + if ( ! isset($this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]])) { + throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $this->_rsm->parentAliasMap[$dqlAlias]); + } + $sourceClassName = $this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]]; $sourceClass = $this->_getClassMetadata($sourceClassName); $assoc = $sourceClass->associationMappings[$this->_rsm->relationMap[$dqlAlias]]; @@ -185,7 +189,7 @@ class ObjectHydrator extends AbstractHydrator return $value; } - + /** * Gets an entity instance. * @@ -195,7 +199,7 @@ class ObjectHydrator extends AbstractHydrator */ private function _getEntity(array $data, $dqlAlias) { - $className = $this->_rsm->aliasMap[$dqlAlias]; + $className = $this->_rsm->aliasMap[$dqlAlias]; if (isset($this->_rsm->discriminatorColumns[$dqlAlias])) { $discrColumn = $this->_rsm->metaMappings[$this->_rsm->discriminatorColumns[$dqlAlias]]; $className = $this->_ce[$className]->discriminatorMap[$data[$discrColumn]]; @@ -203,7 +207,7 @@ class ObjectHydrator extends AbstractHydrator } return $this->_uow->createEntity($className, $data, $this->_hints); } - + private function _getEntityFromIdentityMap($className, array $data) { $class = $this->_ce[$className]; @@ -217,7 +221,7 @@ class ObjectHydrator extends AbstractHydrator return $this->_uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName); } } - + /** * Gets a ClassMetadata instance from the local cache. * If the instance is not yet in the local cache, it is loaded into the @@ -275,7 +279,7 @@ class ObjectHydrator extends AbstractHydrator // Hydrate the data chunks foreach ($rowData as $dqlAlias => $data) { $entityName = $this->_rsm->aliasMap[$dqlAlias]; - + if (isset($this->_rsm->parentAliasMap[$dqlAlias])) { // It's a joined result @@ -286,7 +290,7 @@ class ObjectHydrator extends AbstractHydrator // Get a reference to the parent object to which the joined element belongs. if ($this->_rsm->isMixed && isset($this->_rootAliases[$parentAlias])) { - $first = reset($this->_resultPointers); + $first = reset($this->_resultPointers); $parentObject = $this->_resultPointers[$parentAlias][key($first)]; } else if (isset($this->_resultPointers[$parentAlias])) { $parentObject = $this->_resultPointers[$parentAlias]; @@ -311,11 +315,11 @@ class ObjectHydrator extends AbstractHydrator } else if ( ! isset($this->_existingCollections[$collKey])) { $reflFieldValue = $this->_initRelatedCollection($parentObject, $parentClass, $relationField); } - + $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) { if (isset($this->_existingCollections[$collKey])) { // Collection exists, only look for the element in the identity map. diff --git a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php index 2a09b8310..f997c6c01 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php @@ -246,5 +246,24 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress', 'a', 'u', 'address'); } + + /** + * @group PR-39 + */ + public function testUnknownParentAliasThrowsException() + { + $rsm = new ResultSetMappingBuilder($this->_em); + $rsm->addRootEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $rsm->addJoinedEntityFromClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress', 'a', 'un', 'address', array('id' => 'a_id')); + + $query = $this->_em->createNativeQuery('SELECT u.*, a.*, a.id AS a_id FROM cms_users u INNER JOIN cms_addresses a ON u.id = a.user_id WHERE u.username = ?', $rsm); + $query->setParameter(1, 'romanb'); + + $this->setExpectedException( + "Doctrine\ORM\Internal\Hydration\HydrationException", + "The parent object of entity result with alias 'a' was not found. The parent alias is 'un'." + ); + $users = $query->getResult(); + } } From 6b3dfaccfcdca7051b013a6a403696c50215c4be Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 1 May 2011 10:01:38 +0200 Subject: [PATCH 67/71] DDC-1102 - Typo in EntityGenerator --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 9fbc21780..9eb1f4d2a 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -520,7 +520,7 @@ public function () } if ($metadata->isMappedSuperclass) { - $lines[] = ' * @' . $this->_annotationsPrefix . 'MappedSupperClass'; + $lines[] = ' * @' . $this->_annotationsPrefix . 'MappedSuperClass'; } else { $lines[] = ' * @' . $this->_annotationsPrefix . 'Entity'; } From 7a068c206e8ba1ef0fd6a88d89d9d984b0b35e85 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 1 May 2011 10:21:47 +0200 Subject: [PATCH 68/71] DDC-1043 - Make computeChangeSet() algorithm more strict, possible leading to more updates to to values that are not exactly the same. However this is necessary to avoid bugs with certain PHP casting rules, i.e. +44 = 44 --- lib/Doctrine/ORM/UnitOfWork.php | 4 +-- .../ORM/Functional/Ticket/DDC1043Test.php | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1043Test.php diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 945815a69..7716855be 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -474,9 +474,7 @@ class UnitOfWork implements PropertyChangedListener } } else if ($isChangeTrackingNotify) { continue; - } else if (is_object($orgValue) && $orgValue !== $actualValue) { - $changeSet[$propName] = array($orgValue, $actualValue); - } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) { + } else if ($orgValue !== $actualValue) { $changeSet[$propName] = array($orgValue, $actualValue); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1043Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1043Test.php new file mode 100644 index 000000000..31bd8350f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1043Test.php @@ -0,0 +1,36 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testChangeSetPlusWeirdPHPCastingIntCastingRule() + { + $user = new \Doctrine\Tests\Models\CMS\CmsUser(); + $user->name = "John Galt"; + $user->username = "jgalt"; + $user->status = "+44"; + + $this->_em->persist($user); + $this->_em->flush(); + + $user->status = "44"; + $this->_em->flush(); + $this->_em->clear(); + + $user = $this->_em->find("Doctrine\Tests\Models\CMS\CmsUser", $user->id); + $this->assertSame("44", $user->status); + } +} \ No newline at end of file From c53baa993543fd951fe3beaad81c8183a4051636 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 1 May 2011 11:01:30 +0200 Subject: [PATCH 69/71] [DDC-1091] Fix bug with custom string functions in StringPrimary --- lib/Doctrine/ORM/Query/Parser.php | 3 ++- .../Tests/ORM/Query/LanguageRecognitionTest.php | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 798afa0ce..791f765e9 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -2308,7 +2308,8 @@ class Parser if ($peek['value'] == '.') { return $this->StateFieldPathExpression(); } else if ($peek['value'] == '(') { - return $this->FunctionsReturningStrings(); + // do NOT directly go to FunctionsReturningString() because it doesnt check for custom functions. + return $this->FunctionDeclaration(); } else { $this->syntaxError("'.' or '('"); } diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index 8dccd2cba..2733d80f7 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -482,6 +482,16 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase $this->assertValidDQL('SELECT u, u.id + ?1 AS someNumber FROM Doctrine\Tests\Models\CMS\CmsUser u'); } + /** + * @group DDC-1091 + */ + public function testCustomFunctionsReturningStringInStringPrimary() + { + $this->_em->getConfiguration()->addCustomStringFunction('CC', 'Doctrine\ORM\Query\AST\Functions\ConcatFunction'); + + $this->assertValidDQL("SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE CC('%', u.name) LIKE '%foo%'", true); + } + /** * @group DDC-505 */ From d4569baa11b6bea1ade4b91b6ae4a21c50c42774 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 1 May 2011 11:44:31 +0200 Subject: [PATCH 70/71] [DDC-1129] Fix bug in version changeset computation aswell as inline ClassMetadata::isCollectionValuedAssociation to increase performance by 2-5% --- lib/Doctrine/ORM/UnitOfWork.php | 7 ++- .../ORM/Functional/Ticket/DDC1129Test.php | 46 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1129Test.php diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 7716855be..90d3117e3 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -406,8 +406,11 @@ class UnitOfWork implements PropertyChangedListener $actualData = array(); foreach ($class->reflFields as $name => $refProp) { $value = $refProp->getValue($entity); - if ($class->isCollectionValuedAssociation($name) && $value !== null + if (isset($class->associationMappings[$name]) + && ($class->associationMappings[$name]['type'] & ClassMetadata::TO_MANY) + && $value !== null && ! ($value instanceof PersistentCollection)) { + // If $value is not a Collection then use an ArrayCollection. if ( ! $value instanceof Collection) { $value = new ArrayCollection($value); @@ -426,7 +429,7 @@ class UnitOfWork implements PropertyChangedListener $coll->setDirty( ! $coll->isEmpty()); $class->reflFields[$name]->setValue($entity, $coll); $actualData[$name] = $coll; - } else if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) { + } else if ( (! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField) ) { $actualData[$name] = $value; } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1129Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1129Test.php new file mode 100644 index 000000000..c481aa395 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1129Test.php @@ -0,0 +1,46 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testVersionFieldIgnoredInChangesetComputation() + { + $article = new \Doctrine\Tests\Models\CMS\CmsArticle(); + $article->text = "I don't know."; + $article->topic = "Who is John Galt?"; + + $this->_em->persist($article); + $this->_em->flush(); + + $this->assertEquals(1, $article->version); + + $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsArticle'); + $uow = $this->_em->getUnitOfWork(); + + $uow->computeChangeSet($class, $article); + $changeSet = $uow->getEntityChangeSet($article); + $this->assertEquals(0, count($changeSet), "No changesets should be computed."); + + $article->text = "This is John Galt speaking."; + $this->_em->flush(); + + $this->assertEquals(2, $article->version); + + $uow->computeChangeSet($class, $article); + $changeSet = $uow->getEntityChangeSet($article); + $this->assertEquals(0, count($changeSet), "No changesets should be computed."); + } +} \ No newline at end of file From 5d1905de13b1778a56435aa82d2eb59a2a309205 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 1 May 2011 12:17:09 +0200 Subject: [PATCH 71/71] DDC-1120 - Fix comment --- lib/Doctrine/ORM/AbstractQuery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index b52921644..787770a34 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -272,7 +272,7 @@ abstract class AbstractQuery * @param boolean $bool * @param integer $timeToLive * @param string $resultCacheId - * @return This query instance. + * @return Doctrine\ORM\AbstractQuery This query instance. */ public function useResultCache($bool, $timeToLive = null, $resultCacheId = null) {