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()); + } }