diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index 071f91e9d..c4cfd1aff 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -21,7 +21,6 @@ namespace Doctrine\ORM\Cache; use Doctrine\ORM\Query; -use Doctrine\Common\Proxy\Proxy; use Doctrine\ORM\Cache\EntityCacheKey; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; @@ -133,7 +132,7 @@ class DefaultEntityHydrator implements EntityHydrator return null; } - $data[$name] = $assoc['fetch'] === ClassMetadata::FETCH_EAGER + $data[$name] = $assoc['fetch'] === ClassMetadata::FETCH_EAGER || ($assoc['type'] === ClassMetadata::ONE_TO_ONE && ! $assoc['isOwningSide']) ? $this->uow->createEntity($assocEntry->class, $assocEntry->data, $hints) : $this->em->getReference($assocEntry->class, $assocId); } diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php index 4f78c7354..134f91f64 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -100,6 +100,13 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ protected $regionName; + /** + * Associations configured as FETCH_EAGER, as well as all inverse one-to-one associations. + * + * @var array + */ + protected $joinedAssociations; + /** * @param \Doctrine\ORM\Persisters\EntityPersister $persister The entity persister to cache. * @param \Doctrine\ORM\Cache\Region $region The entity cache region. @@ -227,6 +234,42 @@ abstract class AbstractEntityPersister implements CachedEntityPersister return $cached; } + /** + * @param object $entity + */ + private function storeJoinedAssociations($entity) + { + if ($this->joinedAssociations === null) { + $associations = array(); + + foreach ($this->class->associationMappings as $name => $assoc) { + if (isset($assoc['cache']) && + ($assoc['type'] & ClassMetadata::TO_ONE) && + ($assoc['fetch'] === ClassMetadata::FETCH_EAGER || ! $assoc['isOwningSide'])) { + + $associations[] = $name; + } + } + + $this->joinedAssociations = $associations; + } + + foreach ($this->joinedAssociations as $name) { + $assoc = $this->class->associationMappings[$name]; + $assocEntity = $this->class->getFieldValue($entity, $name); + + if ($assocEntity === null) { + continue; + } + + $assocId = $this->uow->getEntityIdentifier($assocEntity); + $assocKey = new EntityCacheKey($assoc['targetEntity'], $assocId); + $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + + $assocPersister->storeEntityCache($assocEntity, $assocKey); + } + } + /** * Generates a string of currently query * @@ -417,6 +460,10 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $cacheEntry = $this->hydrator->buildCacheEntry($class, $cacheKey, $entity); $cached = $this->region->put($cacheKey, $cacheEntry); + if ($cached && ($this->joinedAssociations === null || count($this->joinedAssociations) > 0)) { + $this->storeJoinedAssociations($entity); + } + if ($this->cacheLogger) { if ($cached) { $this->cacheLogger->entityCachePut($this->regionName, $cacheKey); diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 023f742ab..5877da94b 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -2602,6 +2602,18 @@ class UnitOfWork implements PropertyChangedListener switch (true) { case ($assoc['type'] & ClassMetadata::TO_ONE): if ( ! $assoc['isOwningSide']) { + + // use the given entity association + if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) { + + $this->originalEntityData[$oid][$field] = $data[$field]; + + $class->reflFields[$field]->setValue($entity, $data[$field]); + $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity); + + continue 2; + } + // Inverse side of x-to-one can never be lazy $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity)); diff --git a/tests/Doctrine/Tests/Models/Cache/Beach.php b/tests/Doctrine/Tests/Models/Cache/Beach.php index 7705012c8..61e623dc5 100644 --- a/tests/Doctrine/Tests/Models/Cache/Beach.php +++ b/tests/Doctrine/Tests/Models/Cache/Beach.php @@ -7,5 +7,5 @@ namespace Doctrine\Tests\Models\Cache; */ class Beach extends Attraction { - const CLASSNAME = __CLASS__; + const CLASSNAME = __CLASS__; } \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Traveler.php b/tests/Doctrine/Tests/Models/Cache/Traveler.php index ebc5b239c..32d7cf936 100644 --- a/tests/Doctrine/Tests/Models/Cache/Traveler.php +++ b/tests/Doctrine/Tests/Models/Cache/Traveler.php @@ -33,6 +33,12 @@ class Traveler */ public $travels; + /** + * @Cache + * @OneToOne(targetEntity="TravelerProfile") + */ + protected $profile; + /** * @param string $name */ @@ -62,6 +68,22 @@ class Traveler $this->name = $name; } + /** + * @return \Doctrine\Tests\Models\Cache\TravelerProfile + */ + public function getProfile() + { + return $this->profile; + } + + /** + * @param \Doctrine\Tests\Models\Cache\TravelerProfile $profile + */ + public function setProfile(TravelerProfile $profile) + { + $this->profile = $profile; + } + public function getTravels() { return $this->travels; @@ -88,4 +110,4 @@ class Traveler { $this->travels->removeElement($item); } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/Models/Cache/TravelerProfile.php b/tests/Doctrine/Tests/Models/Cache/TravelerProfile.php new file mode 100644 index 000000000..d4bea2457 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/TravelerProfile.php @@ -0,0 +1,66 @@ +name = $name; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($nae) + { + $this->name = $nae; + } + + public function getInfo() + { + return $this->info; + } + + public function setInfo(TravelerProfileInfo $info) + { + $this->info = $info; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/TravelerProfileInfo.php b/tests/Doctrine/Tests/Models/Cache/TravelerProfileInfo.php new file mode 100644 index 000000000..b7a23bea0 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/TravelerProfileInfo.php @@ -0,0 +1,68 @@ +profile = $profile; + $this->description = $description; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getDescription() + { + return $this->description; + } + + public function setDescription($description) + { + $this->description = $description; + } + + public function getProfile() + { + return $this->profile; + } + + public function setProfile(TravelerProfile $profile) + { + $this->profile = $profile; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index ad00c3993..724368f1c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -26,6 +26,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase } catch(\Exception $e) {} } + /** + * @group non-cacheable + */ public function testEagerLoadOneToOneOwningSide() { $train = new Train(new TrainOwner("Alexander")); @@ -48,6 +51,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); } + /** + * @group non-cacheable + */ public function testEagerLoadOneToOneNullOwningSide() { $train = new Train(new TrainOwner("Alexander")); @@ -65,6 +71,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); } + /** + * @group non-cacheable + */ public function testEagerLoadOneToOneInverseSide() { $owner = new TrainOwner("Alexander"); @@ -83,6 +92,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); } + /** + * @group non-cacheable + */ public function testEagerLoadOneToOneNullInverseSide() { $driver = new TrainDriver("Dagny Taggert"); diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php index 593073e8d..2866c146c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php @@ -8,6 +8,8 @@ use Doctrine\Tests\Models\Cache\Country; use Doctrine\Tests\Models\Cache\State; use Doctrine\Tests\Models\Cache\City; +use Doctrine\Tests\Models\Cache\TravelerProfileInfo; +use Doctrine\Tests\Models\Cache\TravelerProfile; use Doctrine\Tests\Models\Cache\Traveler; use Doctrine\Tests\Models\Cache\Travel; @@ -23,13 +25,14 @@ use Doctrine\Tests\Models\Cache\AttractionLocationInfo; */ abstract class SecondLevelCacheAbstractTest extends OrmFunctionalTestCase { - protected $countries = array(); - protected $states = array(); - protected $cities = array(); - protected $travels = array(); - protected $travelers = array(); - protected $attractions = array(); - protected $attractionsInfo = array(); + protected $countries = array(); + protected $states = array(); + protected $cities = array(); + protected $travels = array(); + protected $travelers = array(); + protected $attractions = array(); + protected $attractionsInfo = array(); + protected $travelersWithProfile = array(); /** * @var \Doctrine\ORM\Cache @@ -118,6 +121,45 @@ abstract class SecondLevelCacheAbstractTest extends OrmFunctionalTestCase $this->_em->flush(); } + + protected function loadFixturesTravelersWithProfile() + { + $t1 = new Traveler("Test traveler 1"); + $t2 = new Traveler("Test traveler 2"); + $p1 = new TravelerProfile("First Traveler Profile"); + $p2 = new TravelerProfile("Second Traveler Profile"); + + $t1->setProfile($p1); + $t2->setProfile($p2); + + $this->_em->persist($p1); + $this->_em->persist($p2); + $this->_em->persist($t1); + $this->_em->persist($t2); + + $this->travelersWithProfile[] = $t1; + $this->travelersWithProfile[] = $t2; + + $this->_em->flush(); + } + + protected function loadFixturesTravelersProfileInfo() + { + $p1 = $this->travelersWithProfile[0]->getProfile(); + $p2 = $this->travelersWithProfile[1]->getProfile(); + $i1 = new TravelerProfileInfo($p1, "First Profile Info ..."); + $i2 = new TravelerProfileInfo($p2, "Second Profile Info ..."); + + $p1->setInfo($i1); + $p2->setInfo($i2); + + $this->_em->persist($i1); + $this->_em->persist($i2); + $this->_em->persist($p1); + $this->_em->persist($p2); + + $this->_em->flush(); + } protected function loadFixturesTravels() { diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToOneTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToOneTest.php new file mode 100644 index 000000000..1f982261c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToOneTest.php @@ -0,0 +1,190 @@ +loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTravelersWithProfile(); + + $this->_em->clear(); + + $entity1 = $this->travelersWithProfile[0]; + $entity2 = $this->travelersWithProfile[1]; + + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity1->getId())); + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity2->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getProfile()->getId())); + } + + public function testPutOneToOneOnBidirectionalPersist() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTravelersWithProfile(); + $this->loadFixturesTravelersProfileInfo(); + + $this->_em->clear(); + + $entity1 = $this->travelersWithProfile[0]; + $entity2 = $this->travelersWithProfile[1]; + + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity1->getId())); + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity2->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getProfile()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity1->getProfile()->getInfo()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity2->getProfile()->getInfo()->getId())); + } + + public function testPutAndLoadOneToOneUnidirectionalRelation() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTravelersWithProfile(); + $this->loadFixturesTravelersProfileInfo(); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(Traveler::CLASSNAME); + $this->cache->evictEntityRegion(TravelerProfile::CLASSNAME); + + $entity1 = $this->travelersWithProfile[0]; + $entity2 = $this->travelersWithProfile[1]; + + $this->assertFalse($this->cache->containsEntity(Traveler::CLASSNAME, $entity1->getId())); + $this->assertFalse($this->cache->containsEntity(Traveler::CLASSNAME, $entity2->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getProfile()->getId())); + + $t1 = $this->_em->find(Traveler::CLASSNAME, $entity1->getId()); + $t2 = $this->_em->find(Traveler::CLASSNAME, $entity2->getId()); + + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity1->getId())); + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity2->getId())); + // The inverse side its not cached + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getProfile()->getId())); + + $this->assertInstanceOf(Traveler::CLASSNAME, $t1); + $this->assertInstanceOf(Traveler::CLASSNAME, $t2); + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $t1->getProfile()); + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $t2->getProfile()); + + $this->assertEquals($entity1->getId(), $t1->getId()); + $this->assertEquals($entity1->getName(), $t1->getName()); + $this->assertEquals($entity1->getProfile()->getId(), $t1->getProfile()->getId()); + $this->assertEquals($entity1->getProfile()->getName(), $t1->getProfile()->getName()); + + $this->assertEquals($entity2->getId(), $t2->getId()); + $this->assertEquals($entity2->getName(), $t2->getName()); + $this->assertEquals($entity2->getProfile()->getId(), $t2->getProfile()->getId()); + $this->assertEquals($entity2->getProfile()->getName(), $t2->getProfile()->getName()); + + // its all cached now + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity1->getId())); + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity2->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + // load from cache + $t3 = $this->_em->find(Traveler::CLASSNAME, $entity1->getId()); + $t4 = $this->_em->find(Traveler::CLASSNAME, $entity2->getId()); + + $this->assertInstanceOf(Traveler::CLASSNAME, $t3); + $this->assertInstanceOf(Traveler::CLASSNAME, $t4); + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $t3->getProfile()); + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $t4->getProfile()); + + $this->assertEquals($entity1->getProfile()->getId(), $t3->getProfile()->getId()); + $this->assertEquals($entity2->getProfile()->getId(), $t4->getProfile()->getId()); + + $this->assertEquals($entity1->getProfile()->getName(), $t3->getProfile()->getName()); + $this->assertEquals($entity2->getProfile()->getName(), $t4->getProfile()->getName()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + + public function testPutAndLoadOneToOneBidirectionalRelation() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTravelersWithProfile(); + $this->loadFixturesTravelersProfileInfo(); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(Traveler::CLASSNAME); + $this->cache->evictEntityRegion(TravelerProfile::CLASSNAME); + $this->cache->evictEntityRegion(TravelerProfileInfo::CLASSNAME); + + $entity1 = $this->travelersWithProfile[0]->getProfile(); + $entity2 = $this->travelersWithProfile[1]->getProfile(); + + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity1->getInfo()->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity2->getInfo()->getId())); + + $p1 = $this->_em->find(TravelerProfile::CLASSNAME, $entity1->getId()); + $p2 = $this->_em->find(TravelerProfile::CLASSNAME, $entity2->getId()); + + $this->assertEquals($entity1->getId(), $p1->getId()); + $this->assertEquals($entity1->getName(), $p1->getName()); + $this->assertEquals($entity1->getInfo()->getId(), $p1->getInfo()->getId()); + $this->assertEquals($entity1->getInfo()->getDescription(), $p1->getInfo()->getDescription()); + + $this->assertEquals($entity2->getId(), $p2->getId()); + $this->assertEquals($entity2->getName(), $p2->getName()); + $this->assertEquals($entity2->getInfo()->getId(), $p2->getInfo()->getId()); + $this->assertEquals($entity2->getInfo()->getDescription(), $p2->getInfo()->getDescription()); + + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity1->getInfo()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity2->getInfo()->getId())); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $p3 = $this->_em->find(TravelerProfile::CLASSNAME, $entity1->getId()); + $p4 = $this->_em->find(TravelerProfile::CLASSNAME, $entity2->getId()); + + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $p3); + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $p4); + $this->assertInstanceOf(TravelerProfileInfo::CLASSNAME, $p3->getInfo()); + $this->assertInstanceOf(TravelerProfileInfo::CLASSNAME, $p4->getInfo()); + + $this->assertEquals($entity1->getId(), $p3->getId()); + $this->assertEquals($entity1->getName(), $p3->getName()); + $this->assertEquals($entity1->getInfo()->getId(), $p3->getInfo()->getId()); + $this->assertEquals($entity1->getInfo()->getDescription(), $p3->getInfo()->getDescription()); + + $this->assertEquals($entity2->getId(), $p4->getId()); + $this->assertEquals($entity2->getName(), $p4->getName()); + $this->assertEquals($entity2->getInfo()->getId(), $p4->getInfo()->getId()); + $this->assertEquals($entity2->getInfo()->getDescription(), $p4->getInfo()->getDescription()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index aa6c063a8..997de8524 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -171,6 +171,8 @@ abstract class OrmFunctionalTestCase extends OrmTestCase 'Doctrine\Tests\Models\Cache\State', 'Doctrine\Tests\Models\Cache\City', 'Doctrine\Tests\Models\Cache\Traveler', + 'Doctrine\Tests\Models\Cache\TravelerProfileInfo', + 'Doctrine\Tests\Models\Cache\TravelerProfile', 'Doctrine\Tests\Models\Cache\Travel', 'Doctrine\Tests\Models\Cache\Attraction', 'Doctrine\Tests\Models\Cache\Restaurant', @@ -325,6 +327,8 @@ abstract class OrmFunctionalTestCase extends OrmTestCase $conn->executeUpdate('DELETE FROM cache_attraction'); $conn->executeUpdate('DELETE FROM cache_travel'); $conn->executeUpdate('DELETE FROM cache_traveler'); + $conn->executeUpdate('DELETE FROM cache_traveler_profile_info'); + $conn->executeUpdate('DELETE FROM cache_traveler_profile'); $conn->executeUpdate('DELETE FROM cache_city'); $conn->executeUpdate('DELETE FROM cache_state'); $conn->executeUpdate('DELETE FROM cache_country');