1
0
mirror of synced 2025-01-18 06:21:40 +03:00

Fix cache misses using one-to-one inverse side

This commit is contained in:
fabios 2013-12-17 18:00:25 -05:00
parent 8554b04053
commit cf4c805427
11 changed files with 473 additions and 11 deletions

View File

@ -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);
}

View File

@ -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);

View File

@ -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));

View File

@ -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;

View File

@ -0,0 +1,66 @@
<?php
namespace Doctrine\Tests\Models\Cache;
/**
* @Entity
* @Table("cache_traveler_profile")
* @Cache("NONSTRICT_READ_WRITE")
*/
class TravelerProfile
{
const CLASSNAME = __CLASS__;
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Column(unique=true)
*/
private $name;
/**
* @OneToOne(targetEntity="TravelerProfileInfo", mappedBy="profile")
* @Cache()
*/
private $info;
public function __construct($name)
{
$this->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;
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Doctrine\Tests\Models\Cache;
/**
* @Entity
* @Table("cache_traveler_profile_info")
* @Cache("NONSTRICT_READ_WRITE")
*/
class TravelerProfileInfo
{
const CLASSNAME = __CLASS__;
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Column(unique=true)
*/
private $description;
/**
* @Cache()
* @JoinColumn(name="profile_id", referencedColumnName="id")
* @OneToOne(targetEntity="TravelerProfile", inversedBy="info")
*/
private $profile;
public function __construct(TravelerProfile $profile, $description)
{
$this->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;
}
}

View File

@ -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");

View File

@ -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;
@ -30,6 +32,7 @@ abstract class SecondLevelCacheAbstractTest extends OrmFunctionalTestCase
protected $travelers = array();
protected $attractions = array();
protected $attractionsInfo = array();
protected $travelersWithProfile = array();
/**
* @var \Doctrine\ORM\Cache
@ -119,6 +122,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()
{
$t1 = new Travel($this->travelers[0]);

View File

@ -0,0 +1,190 @@
<?php
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Tests\Models\Cache\Traveler;
use Doctrine\Tests\Models\Cache\TravelerProfile;
use Doctrine\Tests\Models\Cache\TravelerProfileInfo;
/**
* @group DDC-2183
*/
class SecondLevelCacheOneToOneTest extends SecondLevelCacheAbstractTest
{
public function testPutOneToOneOnUnidirectionalPersist()
{
$this->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());
}
}

View File

@ -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');