From 1438a59c0018d211ae618ebe713a4b8949344bf0 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Thu, 3 Oct 2013 23:02:42 -0400 Subject: [PATCH] Fix persister query cache invalidation --- lib/Doctrine/ORM/Cache.php | 4 +- lib/Doctrine/ORM/Cache/CacheFactory.php | 7 + .../ORM/Cache/DefaultCacheFactory.php | 37 ++++- lib/Doctrine/ORM/Cache/DefaultQueryCache.php | 52 +++++- .../ORM/Cache/Logging/CacheLoggerChain.php | 156 ++++++++++++++++++ .../Cache/Logging/StatisticsCacheLogger.php | 24 +++ .../Persister/AbstractEntityPersister.php | 32 +++- ...onStrictReadWriteCachedEntityPersister.php | 22 ++- .../ReadWriteCachedEntityPersister.php | 10 ++ lib/Doctrine/ORM/Cache/QueryCacheKey.php | 2 +- .../ORM/Cache/Region/DefaultRegion.php | 19 +-- .../ORM/Cache/Region/UpdateTimestampCache.php | 42 +++++ .../ORM/Cache/TimestampCacheEntry.php | 51 ++++++ lib/Doctrine/ORM/Cache/TimestampCacheKey.php | 38 +++++ lib/Doctrine/ORM/Cache/TimestampRegion.php | 39 +++++ lib/Doctrine/ORM/Mapping/Cache.php | 2 +- .../Tests/Mocks/TimestampRegionMock.php | 14 ++ .../Tests/ORM/Cache/CacheLoggerChainTest.php | 118 +++++++++++++ .../Tests/ORM/Cache/DefaultQueryCacheTest.php | 5 + .../ORM/Cache/StatisticsCacheLoggerTest.php | 134 +++++++++++++++ .../SecondLevelCacheConcurrentTest.php | 5 + .../SecondLevelCacheQueryCacheTest.php | 18 +- .../SecondLevelCacheRepositoryTest.php | 128 +++++++++++++- 23 files changed, 909 insertions(+), 50 deletions(-) create mode 100644 lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php create mode 100644 lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php create mode 100644 lib/Doctrine/ORM/Cache/TimestampCacheEntry.php create mode 100644 lib/Doctrine/ORM/Cache/TimestampCacheKey.php create mode 100644 lib/Doctrine/ORM/Cache/TimestampRegion.php create mode 100644 tests/Doctrine/Tests/Mocks/TimestampRegionMock.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php diff --git a/lib/Doctrine/ORM/Cache.php b/lib/Doctrine/ORM/Cache.php index fe0df9702..068591253 100644 --- a/lib/Doctrine/ORM/Cache.php +++ b/lib/Doctrine/ORM/Cache.php @@ -30,7 +30,9 @@ use Doctrine\ORM\EntityManagerInterface; */ interface Cache { - const DEFAULT_QUERY_REGION_NAME = 'query.cache.region'; + const DEFAULT_QUERY_REGION_NAME = 'query_cache_region'; + + const DEFAULT_TIMESTAMP_REGION_NAME = 'timestamp_cache_region'; /** * May read items from the cache, but will not add items. diff --git a/lib/Doctrine/ORM/Cache/CacheFactory.php b/lib/Doctrine/ORM/Cache/CacheFactory.php index 77bcb24a6..01ffa45c9 100644 --- a/lib/Doctrine/ORM/Cache/CacheFactory.php +++ b/lib/Doctrine/ORM/Cache/CacheFactory.php @@ -92,4 +92,11 @@ interface CacheFactory * @return \Doctrine\ORM\Cache\Region The cache region. */ public function getRegion(array $cache); + + /** + * Build timestamp cache region + * + * @return \Doctrine\ORM\Cache\TimestampRegion The timestamp region. + */ + public function getTimestampRegion(); } diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 360a97210..24898bb56 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -22,13 +22,15 @@ namespace Doctrine\ORM\Cache; use Doctrine\ORM\Cache; use Doctrine\ORM\Cache\Region; +use Doctrine\ORM\Cache\TimestampRegion; + use Doctrine\ORM\Cache\RegionsConfiguration; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Cache\Region\DefaultRegion; use Doctrine\ORM\Cache\Region\FileLockRegion; +use Doctrine\ORM\Cache\Region\UpdateTimestampCache; use Doctrine\Common\Cache\Cache as CacheDriver; - use Doctrine\ORM\Persisters\EntityPersister; use Doctrine\ORM\Persisters\CollectionPersister; use Doctrine\ORM\Cache\Persister\ReadOnlyCachedEntityPersister; @@ -54,6 +56,11 @@ class DefaultCacheFactory implements CacheFactory */ private $regionsConfig; + /** + * @var \Doctrine\ORM\Cache\TimestampRegion + */ + private $timestampRegion; + /** * @var array */ @@ -98,6 +105,15 @@ class DefaultCacheFactory implements CacheFactory $this->regions[$region->getName()] = $region; } + /** + * @param \Doctrine\ORM\Cache\TimestampRegion $region + */ + public function setTimestampRegion(TimestampRegion $region) + { + $this->timestampRegion = $region; + } + + /** * {@inheritdoc} */ @@ -180,9 +196,7 @@ class DefaultCacheFactory implements CacheFactory return $this->regions[$cache['region']]; } - $region = new DefaultRegion($cache['region'], clone $this->cache, array( - 'lifetime' => $this->regionsConfig->getLifetime($cache['region']) - )); + $region = new DefaultRegion($cache['region'], clone $this->cache, $this->regionsConfig->getLifetime($cache['region'])); if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { @@ -199,4 +213,19 @@ class DefaultCacheFactory implements CacheFactory return $this->regions[$cache['region']] = $region; } + + /** + * {@inheritdoc} + */ + public function getTimestampRegion() + { + if ($this->timestampRegion === null) { + $name = Cache::DEFAULT_TIMESTAMP_REGION_NAME; + $lifetime = $this->regionsConfig->getLifetime($name); + + $this->timestampRegion = new UpdateTimestampCache($name, clone $this->cache, $lifetime); + } + + return $this->timestampRegion; + } } diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php index 3ff1dd06b..c1648fb7b 100644 --- a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -61,6 +61,11 @@ class DefaultQueryCache implements QueryCache */ private $validator; + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + protected $cacheLogger; + /** * @var array */ @@ -72,12 +77,13 @@ class DefaultQueryCache implements QueryCache */ public function __construct(EntityManagerInterface $em, Region $region) { - $this->em = $em; - $this->region = $region; - $this->uow = $em->getUnitOfWork(); - $this->validator = $em->getConfiguration() - ->getSecondLevelCacheConfiguration() - ->getQueryValidator(); + $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration(); + + $this->em = $em; + $this->region = $region; + $this->uow = $em->getUnitOfWork(); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->validator = $cacheConfig->getQueryValidator(); } /** @@ -106,14 +112,24 @@ class DefaultQueryCache implements QueryCache $hasRelation = ( ! empty($rsm->relationMap)); $persister = $this->uow->getEntityPersister($entityName); $region = $persister->getCacheRegion(); + $regionName = $region->getName(); // @TODO - move to cache hydration componente foreach ($entry->result as $index => $entry) { - if (($entityEntry = $region->get(new EntityCacheKey($entityName, $entry['identifier']))) === null) { + if (($entityEntry = $region->get($entityKey = new EntityCacheKey($entityName, $entry['identifier']))) === null) { + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheMiss($regionName, $entityKey); + } + return null; } + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheHit($regionName, $entityKey); + } + if ( ! $hasRelation) { $result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->data, self::$hints); @@ -129,12 +145,21 @@ class DefaultQueryCache implements QueryCache if ($assoc['type'] & ClassMetadata::TO_ONE) { - if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) { + if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) { + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey); + } + return null; } $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints); + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey); + } + continue; } @@ -147,13 +172,22 @@ class DefaultQueryCache implements QueryCache foreach ($assoc['list'] as $assocIndex => $assocId) { - if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) { + if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) { + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey); + } + return null; } $element = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints); $collection->hydrateSet($assocIndex, $element); + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey); + } } $data[$name] = $collection; diff --git a/lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php b/lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php new file mode 100644 index 000000000..694b35ca5 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php @@ -0,0 +1,156 @@ +. + */ + +namespace Doctrine\ORM\Cache\Logging; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\QueryCacheKey; + +/** + * Cache logger chain + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CacheLoggerChain implements CacheLogger +{ + /** + * @var array<\Doctrine\ORM\Cache\Logging\CacheLogger> + */ + private $loggers = array(); + + /** + * @param string $name + * @param \Doctrine\ORM\Cache\Logging\CacheLogger $logger + */ + public function setLogger($name, CacheLogger $logger) + { + $this->loggers[$name] = $logger; + } + + /** + * @param string $name + * + * @return \Doctrine\ORM\Cache\Logging\CacheLogger|null + */ + public function getLogger($name) + { + return isset($this->loggers[$name]) ? $this->loggers[$name] : null; + } + + /** + * @return array<\Doctrine\ORM\Cache\Logging\CacheLogger> + */ + public function getLoggers() + { + return $this->loggers; + } + + /** + * {@inheritdoc} + */ + public function collectionCacheHit($regionName, CollectionCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->collectionCacheHit($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function collectionCacheMiss($regionName, CollectionCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->collectionCacheMiss($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function collectionCachePut($regionName, CollectionCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->collectionCachePut($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function entityCacheHit($regionName, EntityCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->entityCacheHit($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function entityCacheMiss($regionName, EntityCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->entityCacheMiss($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function entityCachePut($regionName, EntityCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->entityCachePut($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function queryCacheHit($regionName, QueryCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->queryCacheHit($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function queryCacheMiss($regionName, QueryCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->queryCacheMiss($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function queryCachePut($regionName, QueryCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->queryCachePut($regionName, $key); + } + } +} diff --git a/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php b/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php index 283bd4512..2fbba40be 100644 --- a/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php +++ b/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php @@ -173,6 +173,30 @@ class StatisticsCacheLogger implements CacheLogger return isset($this->cachePutCountMap[$regionName]) ? $this->cachePutCountMap[$regionName] : 0; } + /** + * @return array + */ + public function getRegionsMiss() + { + return $this->cacheMissCountMap; + } + + /** + * @return array + */ + public function getRegionsHit() + { + return $this->cacheHitCountMap; + } + + /** + * @return array + */ + public function getRegionsPut() + { + return $this->cachePutCountMap; + } + /** * Clear region statistics * diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php index ad113b609..4eeef07e7 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -24,6 +24,7 @@ use Doctrine\ORM\Cache; use Doctrine\ORM\Cache\Region; use Doctrine\ORM\Cache\EntityCacheKey; use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\TimestampCacheKey; use Doctrine\ORM\Cache\QueryCacheKey; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\PersistentCollection; @@ -69,6 +70,16 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ protected $region; + /** + * @var \Doctrine\ORM\Cache\TimestampRegion + */ + protected $timestampRegion; + + /** + * @var \Doctrine\ORM\Cache\TimestampCacheKey + */ + protected $timestampKey; + /** * @var \Doctrine\ORM\Cache\EntityHydrator */ @@ -109,7 +120,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->timestampRegion = $cacheFactory->getTimestampRegion(); $this->hydrator = $cacheFactory->buildEntityHydrator($em, $class); + $this->timestampKey = new TimestampCacheKey($this->class->getTableName()); } /** @@ -218,13 +231,20 @@ abstract class AbstractEntityPersister implements CachedEntityPersister /** * Generates a string of currently query * + * @param array $query + * @param string $criteria + * @param array $orderBy + * @param integer $limit + * @param integer $offset + * @param integer $timestamp + * * @return string */ - protected function getHash($query, $criteria, array $orderBy = null, $limit = null, $offset = null) + protected function getHash($query, $criteria, array $orderBy = null, $limit = null, $offset = null, $timestamp = null) { - list($params) = $this->expandParameters($criteria); + list($params) = $this->persister->expandParameters($criteria); - return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset); + return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset . $timestamp); } /** @@ -288,8 +308,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister } //handle only EntityRepository#findOneBy + $timestamp = $this->timestampRegion->get($this->timestampKey); $query = $this->persister->getSelectSQL($criteria, null, 0, $limit, 0, $orderBy); - $hash = $this->getHash($query, $criteria); + $hash = $this->getHash($query, $criteria, null, null, null, $timestamp ? $timestamp->time : null); $rsm = $this->getResultSetMapping(); $querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL); $queryCache = $this->cache->getQueryCache($this->regionName); @@ -326,8 +347,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null) { + $timestamp = $this->timestampRegion->get($this->timestampKey); $query = $this->persister->getSelectSQL($criteria, null, 0, $limit, $offset, $orderBy); - $hash = $this->getHash($query, $criteria); + $hash = $this->getHash($query, $criteria, null, null, null, $timestamp ? $timestamp->time : null); $rsm = $this->getResultSetMapping(); $querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL); $queryCache = $this->cache->getQueryCache($this->regionName); diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php index f6420ca21..a9a9a37e9 100644 --- a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php @@ -37,6 +37,8 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister */ public function afterTransactionComplete() { + $isChanged = false; + if (isset($this->queuedCache['insert'])) { foreach ($this->queuedCache['insert'] as $entity) { @@ -47,9 +49,10 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister $class = $this->metadataFactory->getMetadataFor($className); } - $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); - $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); - $cached = $this->region->put($key, $entry); + $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + $isChanged = $isChanged ?: $cached; if ($this->cacheLogger && $cached) { $this->cacheLogger->entityCachePut($this->regionName, $key); @@ -67,9 +70,10 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister $class = $this->metadataFactory->getMetadataFor($className); } - $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); - $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); - $cached = $this->region->put($key, $entry); + $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + $isChanged = $isChanged ?: $cached; if ($this->cacheLogger && $cached) { $this->cacheLogger->entityCachePut($this->regionName, $key); @@ -80,9 +84,15 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister if (isset($this->queuedCache['delete'])) { foreach ($this->queuedCache['delete'] as $key) { $this->region->evict($key); + + $isChanged = true; } } + if ($isChanged) { + $this->timestampRegion->update($this->timestampKey); + } + $this->queuedCache = array(); } diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php index e49c22541..d77aabf9e 100644 --- a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php @@ -56,18 +56,28 @@ class ReadWriteCachedEntityPersister extends AbstractEntityPersister */ public function afterTransactionComplete() { + $isChanged = true; + if (isset($this->queuedCache['update'])) { foreach ($this->queuedCache['update'] as $item) { $this->region->evict($item['key']); + + $isChanged = true; } } if (isset($this->queuedCache['delete'])) { foreach ($this->queuedCache['delete'] as $item) { $this->region->evict($item['key']); + + $isChanged = true; } } + if ($isChanged) { + $this->timestampRegion->update($this->timestampKey); + } + $this->queuedCache = array(); } diff --git a/lib/Doctrine/ORM/Cache/QueryCacheKey.php b/lib/Doctrine/ORM/Cache/QueryCacheKey.php index 36126582a..31a3d15d8 100644 --- a/lib/Doctrine/ORM/Cache/QueryCacheKey.php +++ b/lib/Doctrine/ORM/Cache/QueryCacheKey.php @@ -43,7 +43,7 @@ class QueryCacheKey extends CacheKey * @param integer $lifetime Query lifetime * @param integer $cacheMode Query cache mode */ - public function __construct($hash, $lifetime, $cacheMode = 3) + public function __construct($hash, $lifetime = 0, $cacheMode = 3) { $this->hash = $hash; $this->lifetime = $lifetime; diff --git a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php index 0b62c5b81..b5735e9f6 100644 --- a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -39,33 +39,30 @@ class DefaultRegion implements Region /** * @var \Doctrine\Common\Cache\CacheProvider */ - private $cache; + protected $cache; /** * @var string */ - private $name; + protected $name; /** * @var integer */ - private $lifetime = 0; + protected $lifetime = 0; /** * @param string $name * @param \Doctrine\Common\Cache\CacheProvider $cache - * @param array $configuration + * @param integer $lifetime */ - public function __construct($name, CacheProvider $cache, array $configuration = array()) + public function __construct($name, CacheProvider $cache, $lifetime = 0) { - $this->name = $name; - $this->cache = $cache; + $this->name = $name; + $this->cache = $cache; + $this->lifetime = $lifetime; $this->cache->setNamespace($this->name); - - if (isset($configuration['lifetime']) && $configuration['lifetime'] > 0) { - $this->lifetime = (integer) $configuration['lifetime']; - } } /** diff --git a/lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php b/lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php new file mode 100644 index 000000000..dfdf9062a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php @@ -0,0 +1,42 @@ +. + */ + +namespace Doctrine\ORM\Cache\Region; + +use Doctrine\ORM\Cache\TimestampCacheEntry; +use Doctrine\ORM\Cache\TimestampRegion; +use Doctrine\ORM\Cache\CacheKey; + +/** + * Tracks the timestamps of the most recent updates to particular keys. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class UpdateTimestampCache extends DefaultRegion implements TimestampRegion +{ + /** + * {@inheritdoc} + */ + public function update(CacheKey $key) + { + $this->put($key, new TimestampCacheEntry); + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php new file mode 100644 index 000000000..3ad05c96a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php @@ -0,0 +1,51 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Timestamp cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class TimestampCacheEntry implements CacheEntry +{ + /** + * @var integer + */ + public $time; + + /** + * @param array $result + */ + public function __construct($time = null) + { + $this->time = $time ?: microtime(true); + } + + /** + * @param array $values + */ + public static function __set_state(array $values) + { + return new self($values['time']); + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheKey.php b/lib/Doctrine/ORM/Cache/TimestampCacheKey.php new file mode 100644 index 000000000..2ae65d065 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampCacheKey.php @@ -0,0 +1,38 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * A key that identifies a timestamped space. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class TimestampCacheKey extends CacheKey +{ + /** + * @param string $space Result cache id + */ + public function __construct($space) + { + $this->hash = $space; + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampRegion.php b/lib/Doctrine/ORM/Cache/TimestampRegion.php new file mode 100644 index 000000000..8065a9411 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampRegion.php @@ -0,0 +1,39 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Defines the contract for a cache region which will specifically be used to store entity "update timestamps". + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface TimestampRegion extends Region +{ + /** + * Update an specific key into the cache region. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to lock. + * + * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. + */ + public function update(CacheKey $key); +} diff --git a/lib/Doctrine/ORM/Mapping/Cache.php b/lib/Doctrine/ORM/Mapping/Cache.php index 560a9c4eb..2bb8bb321 100644 --- a/lib/Doctrine/ORM/Mapping/Cache.php +++ b/lib/Doctrine/ORM/Mapping/Cache.php @@ -26,7 +26,7 @@ namespace Doctrine\ORM\Mapping; * @since 2.5 * * @Annotation - * @Target("CLASS") + * @Target({"CLASS","PROPERTY"}) */ final class Cache implements Annotation { diff --git a/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php new file mode 100644 index 000000000..ade513060 --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php @@ -0,0 +1,14 @@ +calls[__FUNCTION__][] = array('key' => $key); + } +} diff --git a/tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php b/tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php new file mode 100644 index 000000000..8f8c65c0f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php @@ -0,0 +1,118 @@ +logger = new CacheLoggerChain(); + $this->mock = $this->getMock('Doctrine\ORM\Cache\Logging\CacheLogger'); + } + + public function testGetAndSetLogger() + { + $this->assertEmpty($this->logger->getLoggers()); + + $this->assertNull($this->logger->getLogger('mock')); + + $this->logger->setLogger('mock', $this->mock); + + $this->assertSame($this->mock, $this->logger->getLogger('mock')); + $this->assertEquals(array('mock' => $this->mock), $this->logger->getLoggers()); + } + + public function testEntityCacheChain() + { + $name = 'my_entity_region'; + $key = new EntityCacheKey(State::CLASSNAME, array('id' => 1)); + + $this->logger->setLogger('mock', $this->mock); + + $this->mock->expects($this->once()) + ->method('entityCacheHit') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('entityCachePut') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('entityCacheMiss') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->logger->entityCacheHit($name, $key); + $this->logger->entityCachePut($name, $key); + $this->logger->entityCacheMiss($name, $key); + } + + public function testCollectionCacheChain() + { + $name = 'my_collection_region'; + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id' => 1)); + + $this->logger->setLogger('mock', $this->mock); + + $this->mock->expects($this->once()) + ->method('collectionCacheHit') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('collectionCachePut') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('collectionCacheMiss') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->logger->collectionCacheHit($name, $key); + $this->logger->collectionCachePut($name, $key); + $this->logger->collectionCacheMiss($name, $key); + } + + public function testQueryCacheChain() + { + $name = 'my_query_region'; + $key = new QueryCacheKey('my_query_hash'); + + $this->logger->setLogger('mock', $this->mock); + + $this->mock->expects($this->once()) + ->method('queryCacheHit') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('queryCachePut') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('queryCacheMiss') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->logger->queryCacheHit($name, $key); + $this->logger->queryCachePut($name, $key); + $this->logger->queryCacheMiss($name, $key); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php index fd58fedc2..fed80adc0 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -525,4 +525,9 @@ class CacheFactoryDefaultQueryCacheTest extends \Doctrine\ORM\Cache\DefaultCache { return $this->region; } + + public function getTimestampRegion() + { + return new \Doctrine\Tests\Mocks\TimestampRegionMock(); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php b/tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php new file mode 100644 index 000000000..16c91729b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php @@ -0,0 +1,134 @@ +logger = new StatisticsCacheLogger(); + } + + public function testEntityCache() + { + $name = 'my_entity_region'; + $key = new EntityCacheKey(State::CLASSNAME, array('id' => 1)); + + $this->logger->entityCacheHit($name, $key); + $this->logger->entityCachePut($name, $key); + $this->logger->entityCacheMiss($name, $key); + + $this->assertEquals(1, $this->logger->getHitCount()); + $this->assertEquals(1, $this->logger->getPutCount()); + $this->assertEquals(1, $this->logger->getMissCount()); + $this->assertEquals(1, $this->logger->getRegionHitCount($name)); + $this->assertEquals(1, $this->logger->getRegionPutCount($name)); + $this->assertEquals(1, $this->logger->getRegionMissCount($name)); + } + + public function testCollectionCache() + { + $name = 'my_collection_region'; + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id' => 1)); + + $this->logger->collectionCacheHit($name, $key); + $this->logger->collectionCachePut($name, $key); + $this->logger->collectionCacheMiss($name, $key); + + $this->assertEquals(1, $this->logger->getHitCount()); + $this->assertEquals(1, $this->logger->getPutCount()); + $this->assertEquals(1, $this->logger->getMissCount()); + $this->assertEquals(1, $this->logger->getRegionHitCount($name)); + $this->assertEquals(1, $this->logger->getRegionPutCount($name)); + $this->assertEquals(1, $this->logger->getRegionMissCount($name)); + } + + public function testQueryCache() + { + $name = 'my_query_region'; + $key = new QueryCacheKey('my_query_hash'); + + $this->logger->queryCacheHit($name, $key); + $this->logger->queryCachePut($name, $key); + $this->logger->queryCacheMiss($name, $key); + + $this->assertEquals(1, $this->logger->getHitCount()); + $this->assertEquals(1, $this->logger->getPutCount()); + $this->assertEquals(1, $this->logger->getMissCount()); + $this->assertEquals(1, $this->logger->getRegionHitCount($name)); + $this->assertEquals(1, $this->logger->getRegionPutCount($name)); + $this->assertEquals(1, $this->logger->getRegionMissCount($name)); + } + + public function testMultipleCaches() + { + $coolRegion = 'my_collection_region'; + $entityRegion = 'my_entity_region'; + $queryRegion = 'my_query_region'; + + $coolKey = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id' => 1)); + $entityKey = new EntityCacheKey(State::CLASSNAME, array('id' => 1)); + $queryKey = new QueryCacheKey('my_query_hash'); + + $this->logger->queryCacheHit($queryRegion, $queryKey); + $this->logger->queryCachePut($queryRegion, $queryKey); + $this->logger->queryCacheMiss($queryRegion, $queryKey); + + $this->logger->entityCacheHit($entityRegion, $entityKey); + $this->logger->entityCachePut($entityRegion, $entityKey); + $this->logger->entityCacheMiss($entityRegion, $entityKey); + + $this->logger->collectionCacheHit($coolRegion, $coolKey); + $this->logger->collectionCachePut($coolRegion, $coolKey); + $this->logger->collectionCacheMiss($coolRegion, $coolKey); + + $this->assertEquals(3, $this->logger->getHitCount()); + $this->assertEquals(3, $this->logger->getPutCount()); + $this->assertEquals(3, $this->logger->getMissCount()); + + $this->assertEquals(1, $this->logger->getRegionHitCount($queryRegion)); + $this->assertEquals(1, $this->logger->getRegionPutCount($queryRegion)); + $this->assertEquals(1, $this->logger->getRegionMissCount($queryRegion)); + + $this->assertEquals(1, $this->logger->getRegionHitCount($coolRegion)); + $this->assertEquals(1, $this->logger->getRegionPutCount($coolRegion)); + $this->assertEquals(1, $this->logger->getRegionMissCount($coolRegion)); + + $this->assertEquals(1, $this->logger->getRegionHitCount($entityRegion)); + $this->assertEquals(1, $this->logger->getRegionPutCount($entityRegion)); + $this->assertEquals(1, $this->logger->getRegionMissCount($entityRegion)); + + $miss = $this->logger->getRegionsMiss(); + $hit = $this->logger->getRegionsHit(); + $put = $this->logger->getRegionsPut(); + + $this->assertArrayHasKey($coolRegion, $miss); + $this->assertArrayHasKey($queryRegion, $miss); + $this->assertArrayHasKey($entityRegion, $miss); + + $this->assertArrayHasKey($coolRegion, $put); + $this->assertArrayHasKey($queryRegion, $put); + $this->assertArrayHasKey($entityRegion, $put); + + $this->assertArrayHasKey($coolRegion, $hit); + $this->assertArrayHasKey($queryRegion, $hit); + $this->assertArrayHasKey($entityRegion, $hit); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php index 4297e147a..298687b23 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php @@ -138,4 +138,9 @@ class CacheFactorySecondLevelCacheConcurrentTest extends \Doctrine\ORM\Cache\Def return $mock; } + + public function getTimestampRegion() + { + return new \Doctrine\Tests\Mocks\TimestampRegionMock(); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php index b7ebca196..c326128cb 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -53,7 +53,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -70,7 +70,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -256,7 +256,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -273,7 +273,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -357,7 +357,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(5, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); @@ -639,7 +639,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -656,7 +656,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -804,7 +804,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertNotEmpty($result3); $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount('foo_region')); @@ -818,7 +818,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertNotEmpty($result3); $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); - $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(6, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount('bar_region')); diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php index ca546bb10..71f7d952f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php @@ -3,6 +3,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\Cache\Country; +use Doctrine\Tests\Models\Cache\State; /** * @group DDC-2183 @@ -60,13 +61,61 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheAbstractTest $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); $this->assertInstanceOf(Country::CLASSNAME, $countries[1]); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); } + public function testRepositoryCacheFindAllInvalidation() + { + $this->loadFixturesCountries(); + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $repository = $this->_em->getRepository(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(2, $repository->findAll()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $queryCount = $this->getCurrentQueryCount(); + $countries = $repository->findAll(); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertCount(2, $countries); + $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); + $this->assertInstanceOf(Country::CLASSNAME, $countries[1]); + + $country = new Country('foo'); + + $this->_em->persist($country); + $this->_em->flush(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(3, $repository->findAll()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $country = $repository->find($country->getId()); + + $this->_em->remove($country); + $this->_em->flush(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(2, $repository->findAll()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + public function testRepositoryCacheFindBy() { $this->loadFixturesCountries(); @@ -91,7 +140,7 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheAbstractTest $this->assertCount(1, $countries); $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); @@ -120,9 +169,82 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheAbstractTest $this->assertInstanceOf(Country::CLASSNAME, $country); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); } + + public function testRepositoryCacheFindAllToOneAssociation() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + + $this->evictRegions(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + // load from database + $repository = $this->_em->getRepository(State::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(4, $entities); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + + // load from cache + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(4, $entities); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[1]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + + // invalidate cache + $this->_em->persist(new State('foo', $this->_em->find(Country::CLASSNAME, $this->countries[0]->getId()))); + $this->_em->flush(); + $this->_em->clear(); + + // load from database + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(5, $entities); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[1]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + + // load from cache + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(5, $entities); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[1]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + } } \ No newline at end of file