diff --git a/.travis.yml b/.travis.yml index adbf643c2a9..2c35f8999fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,12 @@ php: - 5.5 env: - - DB=mysql - - DB=pgsql - - DB=sqlite + - DB=mysql ENABLE_SECOND_LEVEL_CACHE=1 + - DB=pgsql ENABLE_SECOND_LEVEL_CACHE=1 + - DB=sqlite ENABLE_SECOND_LEVEL_CACHE=1 + - DB=mysql ENABLE_SECOND_LEVEL_CACHE=0 + - DB=pgsql ENABLE_SECOND_LEVEL_CACHE=0 + - DB=sqlite ENABLE_SECOND_LEVEL_CACHE=0 before_script: - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS doctrine_tests;' -U postgres; fi" @@ -18,7 +21,7 @@ before_script: - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS doctrine_tests_tmp;create database IF NOT EXISTS doctrine_tests;'; fi" - composer install --prefer-dist --dev -script: phpunit --configuration tests/travis/$DB.travis.xml +script: phpunit -v --configuration tests/travis/$DB.travis.xml after_script: - php vendor/bin/coveralls -v diff --git a/docs/en/index.rst b/docs/en/index.rst index 72cac572716..7df56526d1a 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -78,6 +78,7 @@ Advanced Topics * :doc:`Change Tracking Policies ` * :doc:`Best Practices ` * :doc:`Metadata Drivers ` + * :doc:`Second Level Cache ` Tutorials --------- diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst new file mode 100644 index 00000000000..de930e6e100 --- /dev/null +++ b/docs/en/reference/second-level-cache.rst @@ -0,0 +1,802 @@ +The Second Level Cache +====================== + +The Second Level Cache is designed to reduces the amount of necessary database access. +It sits between your application and the database to avoid the number of database hits as many as possible. + +When this is turned on, entities will be first searched in cache and if they are not found, +a database query will be fired an then the entity result will be stored in a cache provider. + +There are some flavors of caching available, but is better to cache read-only data. + +Be aware that caches are not aware of changes made to the persistent store by another application. +They can, however, be configured to regularly expire cached data. + + +Caching Regions +--------------- + +Second level cache does not store instances of an entity, instead it caches only entity identifier and values. +Each entity class, collection association and query has its region, where values of each instance are stored. + +Caching Regions are specific region into the cache provider that might store entities, collection or queries. +Each cache region resides in a specific cache namespace and has its own lifetime configuration. + +Something like below for an entity region : + +.. code-block:: php + + ['id'=> 1, 'name' => 'FooBar', 'associationName'=>null], + 'region_name:entity_2_hash' => ['id'=> 2, 'name' => 'Foo', 'associationName'=>['id'=>11]], + 'region_name:entity_3_hash' => ['id'=> 3, 'name' => 'Bar', 'associationName'=>['id'=>22]] + ]; + + +If the entity holds a collection that also needs to be cached. +An collection region could look something like : + +.. code-block:: php + + ['ownerId'=> 1, 'list' => [1, 2, 3]], + 'region_name:entity_2_coll_assoc_name_hash' => ['ownerId'=> 2, 'list' => [2, 3]], + 'region_name:entity_3_coll_assoc_name_hash' => ['ownerId'=> 3, 'list' => [2, 4]] + ]; + +A query region might be something like : + +.. code-block:: php + + ['list' => [1, 2, 3]], + 'region_name:query_2_hash' => ['list' => [2, 3]], + 'region_name:query_3_hash' => ['list' => [2, 4]] + ]; + + +.. note:: + + Notice that when caching collection and queries only identifiers are stored. + The entity values will be stored in its own region + + +.. _reference-second-level-cache-regions: + +Cache Regions +------------- + +``Doctrine\ORM\Cache\Region\DefaultRegion`` Its the default implementation. + A simplest cache region compatible with all doctrine-cache drivers but does not support locking. + +``Doctrine\ORM\Cache\Region`` and ``Doctrine\ORM\Cache\ConcurrentRegion`` +Defines contracts that should be implemented by a cache provider. + +It allows you to provide your own cache implementation that might take advantage of specific cache driver. + +If you want to support locking for ``READ_WRITE`` strategies you should implement ``ConcurrentRegion``; ``CacheRegion`` otherwise. + + +``Doctrine\ORM\Cache\Region`` + +Defines a contract for accessing a particular cache region. + +.. code-block:: php + + setSecondLevelCacheEnabled(); + + //Cache factory + $config->setSecondLevelCacheFactory($factory); + + +Cache Factory +~~~~~~~~~~~~~ + +Cache Factory is the main point of extension. + +It allows you to provide a specific implementation of the following components : + +* ``QueryCache`` Store and retrieve query cache results. +* ``CachedEntityPersister`` Store and retrieve entity results. +* ``CachedCollectionPersister`` Store and retrieve query results. +* ``EntityHydrator`` Transform an entity into a cache entry and cache entry into entities +* ``CollectionHydrator`` Transform a collection into a cache entry and cache entry into collection + +.. code-block:: php + + setSecondLevelCacheRegionLifetime('my_entity_region', 3600); + $config->setSecondLevelCacheDefaultRegionLifetime(7200); + + +Cache Log +~~~~~~~~~ +By providing a cache logger you should be able to get information about all cache operations such as hits, miss put. + +``\Doctrine\ORM\Cache\Logging\StatisticsCacheLogger`` is a built-in implementation that provides basic statistics. + + .. code-block:: php + + setSecondLevelCacheLogger($logger); + + + // Collect cache statistics + + // Get the number of entries successfully retrieved from a specific region. + $logger->getRegionHitCount('my_entity_region'); + + // Get the number of cached entries *not* found in a specific region. + $logger->getRegionMissCount('my_entity_region'); + + // Get the number of cacheable entries put in cache. + $logger->getRegionPutCount('my_entity_region'); + + // Get the total number of put in all regions. + $logger->getPutCount(); + + // Get the total number of entries successfully retrieved from all regions. + $logger->getHitCount(); + + // Get the total number of cached entries *not* found in all regions. + $logger->getMissCount(); + +If you want to get more information you should implement ``\Doctrine\ORM\Cache\Logging\CacheLogger``. +and collect all information you want. + + .. code-block:: php + + + + + + + + + + + + + .. code-block:: yaml + + Country: + type: entity + cache: + usage : READ_ONLY + region : my_entity_region + id: + id: + type: integer + id: true + generator: + strategy: IDENTITY + fields: + name: + type: string + + +Association cache definition +---------------------------- +The most common use case is to cache entities. But we can also cache relationships. +It caches the primary keys of association and cache each element will be cached into its region. + + +.. configuration-block:: + + .. code-block:: php + + + + + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: yaml + + State: + type: entity + cache: + usage : NONSTRICT_READ_WRITE + id: + id: + type: integer + id: true + generator: + strategy: IDENTITY + fields: + name: + type: string + + manyToOne: + state: + targetEntity: Country + joinColumns: + country_id: + referencedColumnName: id + cache: + usage : NONSTRICT_READ_WRITE + + oneToMany: + cities: + targetEntity:City + mappedBy: state + cache: + usage : NONSTRICT_READ_WRITE + + +Cache usage +~~~~~~~~~~~ + +Basic entity cache + +.. code-block:: php + + persist(new Country($name)); + $em->flush(); // Hit database to insert the row and put into cache + + $em->clear(); // Clear entity manager + + $country = $em->find('Country', 1); // Retrieve item from cache + + $country->setName("New Name"); + $em->persist($state); + $em->flush(); // Hit database to update the row and update cache + + $em->clear(); // Clear entity manager + + $country = $em->find('Country', 1); // Retrieve item from cache + + +Association cache + +.. code-block:: php + + persist(new State($name, $country)); + $em->flush(); + + // Clear entity manager + $em->clear(); + + // Retrieve item from cache + $state = $em->find('State', 1); + + // Hit database to update the row and update cache entry + $state->setName("New Name"); + $em->persist($state); + $em->flush(); + + // Create a new collection item + $city = new City($name, $state); + $state->addCity($city); + + // Hit database to insert new collection item, + // put entity and collection cache into cache. + $em->persist($city); + $em->persist($state); + $em->flush(); + + // Clear entity manager + $em->clear(); + + // Retrieve item from cache + $state = $em->find('State', 1); + + // Retrieve association from cache + $country = $state->getCountry(); + + // Retrieve collection from cache + $cities = $state->getCities(); + + echo $country->getName(); + echo $state->getName(); + + // Retrieve each collection item from cache + foreach ($cities as $city) { + echo $city->getName(); + } + +.. note:: + + Notice that all entities should be marked as cacheable. + +Using the query cache +--------------------- + +The second level cache stores the entities, associations and collections. +The query cache stores the results of the query but as identifiers, +The entity values are actually stored in the 2nd level cache. +So, query cache is useless without a 2nd level cache. + +.. code-block:: php + + createQuery('SELECT c FROM Country c ORDER BY c.name') + ->setCacheable(true) + ->getResult(); + + // Check if query result is valid and load entities from cache + $result2 = $em->createQuery('SELECT c FROM Country c ORDER BY c.name') + ->setCacheable(true) + ->getResult(); + + +Cache API +--------- + +Caches are not aware of changes made by another application. +however, you can use the cache API to check / invalidate cache entries. + +.. code-block:: php + + getCache(); + + $cache->containsEntity('State', 1) // Check if the cache exists + $cache)->evictEntity('State', 1); // Remove an entity from cache + $cache->evictEntityRegion('State'); // Remove all entities from cache + + $cache->containsCollection('State', 'cities', 1); // Check if the cache exists + $cache->evictCollection('State', 'cities', 1); // Remove an entity collection from cache + $cache->evictCollectionRegion('State', 'cities'); // Remove all collections from cache + +Limitations +----------- + +Composite primary key +~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + Composite primary key are supported by second level cache, + However when one of the keys is an association + the cached entity should always be retrieved using the association identifier. + +.. code-block:: php + + 1, 'target' => 2); + $reference = $this->_em->find("Reference", $id); + + // NOT Supported + $id = array('source' => new Article(1), 'target' => new Article(2)); + $reference = $this->_em->find("Reference", $id); + + +Concurrent cache region +~~~~~~~~~~~~~~~~~~~~~~~ + +A ``Doctrine\\ORM\\Cache\\ConcurrentRegion`` is designed to store concurrently managed data region. +However doctrine provide an very simple implementation based on file locks ``Doctrine\\ORM\\Cache\\Region\\FileLockRegion``. + +If you want to use an ``READ_WRITE`` cache, you should consider providing your own cache region. +for more details about how to implement a cache region please see :ref:`reference-second-level-cache-regions` \ No newline at end of file diff --git a/docs/en/toc.rst b/docs/en/toc.rst index 1a331fa2384..9d5553704d3 100644 --- a/docs/en/toc.rst +++ b/docs/en/toc.rst @@ -53,10 +53,13 @@ Reference Guide reference/metadata-drivers reference/best-practices reference/limitations-and-known-issues - reference/filters.rst - reference/namingstrategy.rst - reference/advanced-configuration.rst - + tutorials/pagination + reference/filters + reference/namingstrategy + reference/installation + reference/advanced-configuration + reference/second-level-cache + Cookbook -------- @@ -81,4 +84,5 @@ Cookbook cookbook/mysql-enums cookbook/advanced-field-value-conversion-using-custom-mapping-types cookbook/entities-in-session + cookbook/resolve-target-entity-listener diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index f9c774d5723..2175a909917 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -55,6 +55,14 @@ + + + + + + + + @@ -152,8 +160,14 @@ + + + + + + @@ -445,6 +459,7 @@ + @@ -462,6 +477,7 @@ + @@ -477,6 +493,7 @@ + @@ -495,6 +512,7 @@ + diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 07b6b9ea95f..2b021b28588 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -23,8 +23,10 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\DBAL\Types\Type; +use Doctrine\ORM\Cache\QueryCacheKey; use Doctrine\DBAL\Cache\QueryCacheProfile; +use Doctrine\ORM\Cache; use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\ORMInvalidArgumentException; @@ -120,6 +122,37 @@ abstract class AbstractQuery */ protected $_hydrationCacheProfile; + /** + * Whether to use second level cache, if available. + * + * @var boolean + */ + protected $cacheable; + + /** + * Second level cache region name. + * + * @var string + */ + protected $cacheRegion; + + /** + * Second level query cache mode. + * + * @var integer + */ + protected $cacheMode; + + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + protected $cacheLogger; + + /** + * @var integer + */ + protected $lifetime = 0; + /** * Initializes a new instance of a class derived from AbstractQuery. * @@ -127,8 +160,102 @@ abstract class AbstractQuery */ public function __construct(EntityManager $em) { - $this->_em = $em; - $this->parameters = new ArrayCollection(); + $this->_em = $em; + $this->parameters = new ArrayCollection(); + $this->cacheLogger = $em->getConfiguration()->getSecondLevelCacheLogger(); + } + + /** + * + * Enable/disable second level query (result) caching for this query. + * + * @param boolean $cacheable + * + * @return \Doctrine\ORM\AbstractQuery This query instance. + */ + public function setCacheable($cacheable) + { + $this->cacheable = (boolean) $cacheable; + + return $this; + } + + /** + * @return boolean TRUE if the query results are enable for second level cache, FALSE otherwise. + */ + public function isCacheable() + { + return $this->cacheable; + } + + /** + * @param string $cacheRegion + * + * @return \Doctrine\ORM\AbstractQuery This query instance. + */ + public function setCacheRegion($cacheRegion) + { + $this->cacheRegion = $cacheRegion; + + return $this; + } + + /** + * Obtain the name of the second level query cache region in which query results will be stored + * + * @return The cache region name; NULL indicates the default region. + */ + public function getCacheRegion() + { + return $this->cacheRegion; + } + + /** + * @return boolean TRUE if the query cache and second level cache are anabled, FALSE otherwise. + */ + protected function isCacheEnabled() + { + return $this->cacheable && $this->_em->getConfiguration()->isSecondLevelCacheEnabled(); + } + + /** + * @return integer + */ + public function getLifetime() + { + return $this->lifetime; + } + + /** + * Sets the life-time for this query into second level cache. + * + * @param integer $lifetime + * @return \Doctrine\ORM\AbstractQuery This query instance. + */ + public function setLifetime($lifetime) + { + $this->lifetime = $lifetime; + + return $this; + } + + /** + * @return integer + */ + public function getCacheMode() + { + return $this->cacheMode; + } + + /** + * @param integer $cacheMode + * @return \Doctrine\ORM\AbstractQuery This query instance. + */ + public function setCacheMode($cacheMode) + { + $this->cacheMode = $cacheMode; + + return $this; } /** @@ -306,6 +433,16 @@ public function setResultSetMapping(Query\ResultSetMapping $rsm) return $this; } + /** + * Gets the ResultSetMapping used for hydration. + * + * @return \Doctrine\ORM\Query\ResultSetMapping + */ + public function getResultSetMapping() + { + return $this->_resultSetMapping; + } + /** * Allows to translate entity namespaces to full qualified names. * @@ -742,11 +879,10 @@ public function iterate($parameters = null, $hydrationMode = null) $this->setParameters($parameters); } + $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); $stmt = $this->_doExecute(); - return $this->_em->newHydrator($this->_hydrationMode)->iterate( - $stmt, $this->_resultSetMapping, $this->_hints - ); + return $this->_em->newHydrator($this->_hydrationMode)->iterate($stmt, $rsm, $this->_hints); } /** @@ -758,6 +894,23 @@ public function iterate($parameters = null, $hydrationMode = null) * @return mixed */ public function execute($parameters = null, $hydrationMode = null) + { + if ($this->cacheable && $this->isCacheEnabled()) { + return $this->executeUsingQueryCache($parameters, $hydrationMode); + } + + return $this->executeIgnoreQueryCache($parameters, $hydrationMode); + } + + /** + * Execute query ignoring second level cache. + * + * @param ArrayCollection|array|null $parameters + * @param integer|null $hydrationMode + * + * @return mixed + */ + private function executeIgnoreQueryCache($parameters = null, $hydrationMode = null) { if ($hydrationMode !== null) { $this->setHydrationMode($hydrationMode); @@ -799,15 +952,52 @@ public function execute($parameters = null, $hydrationMode = null) return $stmt; } - $data = $this->_em->newHydrator($this->_hydrationMode)->hydrateAll( - $stmt, $this->_resultSetMapping, $this->_hints - ); + $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $data = $this->_em->newHydrator($this->_hydrationMode)->hydrateAll($stmt, $rsm, $this->_hints); $setCacheEntry($data); return $data; } + /** + * Load from second level cache or executes the query and put into cache. + * + * @param ArrayCollection|array|null $parameters + * @param integer|null $hydrationMode + * + * @return mixed + */ + private function executeUsingQueryCache($parameters = null, $hydrationMode = null) + { + $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $querykey = new QueryCacheKey($this->getHash(), $this->lifetime, $this->cacheMode ?: Cache::MODE_NORMAL); + $queryCache = $this->_em->getCache()->getQueryCache($this->cacheRegion); + $result = $queryCache->get($querykey, $rsm); + + if ($result !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->queryCacheHit($queryCache->getRegion()->getName(), $querykey); + } + + return $result; + } + + $result = $this->executeIgnoreQueryCache($parameters, $hydrationMode); + $cached = $queryCache->put($querykey, $rsm, $result); + + if ($this->cacheLogger) { + $this->cacheLogger->queryCacheMiss($queryCache->getRegion()->getName(), $querykey); + } + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $querykey); + } + + return $result; + } + /** * Get the result cache id to use to store the result set cache entry. * Will return the configured id if it exists otherwise a hash will be @@ -881,4 +1071,26 @@ public function __clone() $this->_hints = array(); } + + /** + * Generates a string of currently query to use for the cache second level cache. + * + * @return string + */ + protected function getHash() + { + $hints = $this->getHints(); + $query = $this->getSQL(); + $params = array(); + + foreach ($this->parameters as $parameter) { + $value = $parameter->getValue(); + + $params[$parameter->getName()] = is_scalar($value) ? $value : $this->processParameterValue($value); + } + + ksort($hints); + + return sha1($query . '-' . serialize($params) . '-' . serialize($hints)); + } } diff --git a/lib/Doctrine/ORM/Cache.php b/lib/Doctrine/ORM/Cache.php new file mode 100644 index 00000000000..fe0df9702df --- /dev/null +++ b/lib/Doctrine/ORM/Cache.php @@ -0,0 +1,185 @@ +. + */ + +namespace Doctrine\ORM; + +use Doctrine\ORM\EntityManagerInterface; + +/** + * Provides an API for querying/managing the second level cache regions. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface Cache +{ + const DEFAULT_QUERY_REGION_NAME = 'query.cache.region'; + + /** + * May read items from the cache, but will not add items. + */ + const MODE_GET = 1; + + /** + * Will never read items from the cache, + * but will add items to the cache as it reads them from the database. + */ + const MODE_PUT = 2; + + /** + * May read items from the cache, and add items to the cache. + */ + const MODE_NORMAL = 3; + + /** + * The session will never read items from the cache, + * but will refresh items to the cache as it reads them from the database. + */ + const MODE_REFRESH = 4; + + /** + * Construct + * + * @param \Doctrine\ORMEntityManagerInterface $em + */ + public function __construct(EntityManagerInterface $em); + + /** + * @param string $className The entity class. + * + * @return \Doctrine\ORM\Cache\Region|null + */ + public function getEntityCacheRegion($className); + + /** + * @param string $className The entity class. + * @param string $association The field name that represents the association. + * + * @return \Doctrine\ORM\Cache\Region|null + */ + public function getCollectionCacheRegion($className, $association); + + /** + * Determine whether the cache contains data for the given entity "instance". + * + * @param string $className The entity class. + * @param mixed $identifier The entity identifier + * + * @return boolean true if the underlying cache contains corresponding data; false otherwise. + */ + public function containsEntity($className, $identifier); + + /** + * Evicts the entity data for a particular entity "instance". + * + * @param string $className The entity class. + * @param mixed $identifier The entity identifier. + * + * @return void + */ + public function evictEntity($className, $identifier); + + /** + * Evicts all entity data from the given region. + * + * @param string $className The entity metadata. + * + * @return void + */ + public function evictEntityRegion($className); + + /** + * Evict data from all entity regions. + * + * @return void + */ + public function evictEntityRegions(); + + /** + * Determine whether the cache contains data for the given collection. + * + * @param string $className The entity class. + * @param string $association The field name that represents the association. + * @param mixed $ownerIdentifier The identifier of the owning entity. + * + * @return boolean true if the underlying cache contains corresponding data; false otherwise. + */ + public function containsCollection($className, $association, $ownerIdentifier); + + /** + * Evicts the cache data for the given identified collection instance. + * + * @param string $className The entity class. + * @param string $association The field name that represents the association. + * @param mixed $ownerIdentifier The identifier of the owning entity. + * + * @return void + */ + public function evictCollection($className, $association, $ownerIdentifier); + + /** + * Evicts all entity data from the given region. + * + * @param string $className The entity class. + * @param string $association The field name that represents the association. + * + * @return void + */ + public function evictCollectionRegion($className, $association); + + /** + * Evict data from all collection regions. + * + * @return void + */ + public function evictCollectionRegions(); + + /** + * Determine whether the cache contains data for the given query. + * + * @param string $regionName The cache name given to the query. + * + * @return boolean true if the underlying cache contains corresponding data; false otherwise. + */ + public function containsQuery($regionName); + + /** + * Evicts all cached query results under the given name, or default query cache if the region name is NULL. + * + * @param string $regionName The cache name associated to the queries being cached. + */ + public function evictQueryRegion($regionName = null); + + /** + * Evict data from all query regions. + * + * @return void + */ + public function evictQueryRegions(); + + /** + * Get query cache by region name or create a new one if none exist. + * + * @param regionName Query cache region name, or default query cache if the region name is NULL. + * + * @return \Doctrine\ORM\Cache\QueryCache The Query Cache associated with the region name. + */ + public function getQueryCache($regionName = null); +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Cache/CacheEntry.php b/lib/Doctrine/ORM/Cache/CacheEntry.php new file mode 100644 index 00000000000..cdcb6c0fb70 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheEntry.php @@ -0,0 +1,32 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Cache entry interface + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface CacheEntry +{ + +} diff --git a/lib/Doctrine/ORM/Cache/CacheException.php b/lib/Doctrine/ORM/Cache/CacheException.php new file mode 100644 index 00000000000..5b548fe5ec7 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheException.php @@ -0,0 +1,72 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\ORMException; + +/** + * Exception for cache. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CacheException extends ORMException +{ + /** + * @param string $sourceEntity + * @param string $fieldName + * + * @return \Doctrine\ORM\Cache\CacheException + */ + public static function updateReadOnlyCollection($sourceEntity, $fieldName) + { + return new self(sprintf('Cannot update a readonly collection "%s#%s"', $sourceEntity, $fieldName)); + } + + /** + * @param string $entityName + * + * @return \Doctrine\ORM\Cache\CacheException + */ + public static function updateReadOnlyEntity($entityName) + { + return new self(sprintf('Cannot update a readonly entity "%s"', $entityName)); + } + + /** + * @param string $entityName + * + * @return \Doctrine\ORM\Cache\CacheException + */ + public static function nonCacheableEntity($entityName) + { + return new self(sprintf('Entity "%s" not configured as part of the second-level cache.', $entityName)); + } + + /** + * @param string $entityName + * + * @return \Doctrine\ORM\Cache\CacheException + */ + public static function nonCacheableEntityAssociation($entityName, $field) + { + return new self(sprintf('Entity association field "%s#%s" not configured as part of the second-level cache.', $entityName, $field)); + } +} diff --git a/lib/Doctrine/ORM/Cache/CacheFactory.php b/lib/Doctrine/ORM/Cache/CacheFactory.php new file mode 100644 index 00000000000..89d54d43f3f --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheFactory.php @@ -0,0 +1,95 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; + +use Doctrine\ORM\Persisters\CollectionPersister; +use Doctrine\ORM\Persisters\EntityPersister; + +/** + * @since 2.5 + * @author Fabio B. Silva + */ +interface CacheFactory +{ + /** + * Build an entity persister for the given entity metadata. + * + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param \Doctrine\ORM\Persisters\EntityPersister $persister The entity persister that will be cached. + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * + * @return \Doctrine\ORM\Cache\Persister\CachedEntityPersister + */ + public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata); + + /** + * Build a collection persister for the given relation mapping. + * + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached. + * @param array $mapping The association mapping. + * + * @return \Doctrine\ORM\Cache\Persister\CachedCollectionPersister + */ + public function buildCachedCollectionPersister(EntityManagerInterface $em, CollectionPersister $persister, array $mapping); + + /** + * Build a query cache based on the given region name + * + * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. + * @param string $regionName The region name. + * + * @return \Doctrine\ORM\Cache\QueryCache The built query cache. + */ + public function buildQueryCache(EntityManagerInterface $em, $regionName = null); + + /** + * Build an entity hidrator + * + * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * + * @return \Doctrine\ORM\Cache\EntityHydrator The built entity hidrator. + */ + public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata); + + /** + * Build a collection hidrator + * + * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. + * @param array $mapping The association mapping. + * + * @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hidrator. + */ + public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping); + + /** + * Build a cache region + * + * @param array $cache The cache configuration. + * + * @return \Doctrine\ORM\Cache\Region The cache region. + */ + public function getRegion(array $cache); +} diff --git a/lib/Doctrine/ORM/Cache/CacheKey.php b/lib/Doctrine/ORM/Cache/CacheKey.php new file mode 100644 index 00000000000..ede2030913c --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheKey.php @@ -0,0 +1,36 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Defines entity / collection key to be stored in the cache region. + * Allows multiple roles to be stored in the same cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +abstract class CacheKey +{ + /** + * @var string Unique identifier + */ + public $hash; +} diff --git a/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php b/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php new file mode 100644 index 00000000000..4bb329a7e62 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php @@ -0,0 +1,51 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Collection cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CollectionCacheEntry implements CacheEntry +{ + /** + * @var array + */ + public $identifiers; + + /** + * @param array $identifiers + */ + public function __construct(array $identifiers) + { + $this->identifiers = $identifiers; + } + + /** + * @param array $values + */ + public static function __set_state(array $values) + { + return new self($values['identifiers']); + } +} diff --git a/lib/Doctrine/ORM/Cache/CollectionCacheKey.php b/lib/Doctrine/ORM/Cache/CollectionCacheKey.php new file mode 100644 index 00000000000..184fa9bf2a5 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CollectionCacheKey.php @@ -0,0 +1,60 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Defines entity collection roles to be stored in the cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CollectionCacheKey extends CacheKey +{ + /** + * @var array + */ + public $ownerIdentifier; + + /** + * @var string + */ + public $entityClass; + + /** + * @var string + */ + public $association; + + /** + * @param string $entityClass The entity class. + * @param string $association The field name that represents the association. + * @param array $ownerIdentifier The identifier of the owning entity. + */ + public function __construct($entityClass, $association, array $ownerIdentifier) + { + ksort($ownerIdentifier); + + $this->entityClass = $entityClass; + $this->association = $association; + $this->ownerIdentifier = $ownerIdentifier; + $this->hash = str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association; + } +} diff --git a/lib/Doctrine/ORM/Cache/CollectionHydrator.php b/lib/Doctrine/ORM/Cache/CollectionHydrator.php new file mode 100644 index 00000000000..9b3141dd518 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CollectionHydrator.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\CollectionCacheEntry; + +/** + * Hidrator cache entry for collections + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface CollectionHydrator +{ + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key The cached collection key. + * @param array|\Doctrine\Common\Collections\Collection $collection The collection. + * + * @return \Doctrine\ORM\Cache\CollectionCacheEntry + */ + public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, $collection); + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The owning entity metadata. + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key The cached collection key. + * @param \Doctrine\ORM\Cache\CollectionCacheEntry $entry The cached collection entry. + * @param Doctrine\ORM\PersistentCollection $collection The collection to load the cache into. + * + * @return array + */ + public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection); +} diff --git a/lib/Doctrine/ORM/Cache/ConcurrentRegion.php b/lib/Doctrine/ORM/Cache/ConcurrentRegion.php new file mode 100644 index 00000000000..7bb50086ba5 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/ConcurrentRegion.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache\Lock; + +/** + * Defines contract for concurrently managed data region. + * It should be able to lock an specific cache entry in an atomic operation. + * + * When a entry is locked another process should not be able to read or write the entry. + * All evict operation should not consider locks, even though an entry is locked evict should be able to delete the entry and its lock. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface ConcurrentRegion extends Region +{ + /** + * Attempts to read lock the mapping for the given key. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to lock. + * + * @return \Doctrine\ORM\Cache\Lock A lock instance or NULL if the lock already exists. + * + * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. + */ + public function lock(CacheKey $key); + + /** + * Attempts to read unlock the mapping for the given key. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to unlock. + * @param \Doctrine\ORM\Cache\Lock $lock The lock previously obtained from {@link readLock} + * + * @return void + * + * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. + */ + public function unlock(CacheKey $key, Lock $lock); +} diff --git a/lib/Doctrine/ORM/Cache/DefaultCache.php b/lib/Doctrine/ORM/Cache/DefaultCache.php new file mode 100644 index 00000000000..92f0c639619 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultCache.php @@ -0,0 +1,346 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache; +use Doctrine\Common\Util\ClassUtils; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\Persister\CachedPersister; +use Doctrine\ORM\ORMInvalidArgumentException; + +/** + * Provides an API for querying/managing the second level cache regions. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultCache implements Cache +{ + /** + * @var \Doctrine\ORM\EntityManagerInterface + */ + private $em; + + /** + * @var \Doctrine\ORM\UnitOfWork + */ + private $uow; + + /** + * @var \Doctrine\ORM\Cache\CacheFactory + */ + private $cacheFactory; + + /** + * @var array<\Doctrine\ORM\Cache\QueryCache> + */ + private $queryCaches = array(); + + /** + * @var \Doctrine\ORM\Cache\QueryCache + */ + private $defaultQueryCache; + + /** + * {@inheritdoc} + */ + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + $this->uow = $em->getUnitOfWork(); + $this->cacheFactory = $em->getConfiguration()->getSecondLevelCacheFactory(); + } + + /** + * {@inheritdoc} + */ + public function getEntityCacheRegion($className) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if ( ! ($persister instanceof CachedPersister)) { + return null; + } + + return $persister->getCacheRegion(); + } + + /** + * {@inheritdoc} + */ + public function getCollectionCacheRegion($className, $association) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if ( ! ($persister instanceof CachedPersister)) { + return null; + } + + return $persister->getCacheRegion(); + } + + /** + * {@inheritdoc} + */ + public function containsEntity($className, $identifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + $key = $this->buildEntityCacheKey($metadata, $identifier); + + if ( ! ($persister instanceof CachedPersister)) { + return false; + } + + return $persister->getCacheRegion()->contains($key); + } + + /** + * {@inheritdoc} + */ + public function evictEntity($className, $identifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + $key = $this->buildEntityCacheKey($metadata, $identifier); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evict($key); + } + + /** + * {@inheritdoc} + */ + public function evictEntityRegion($className) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evictAll(); + } + + /** + * {@inheritdoc} + */ + public function evictEntityRegions() + { + $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); + + foreach ($metadatas as $metadata) { + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if ( ! ($persister instanceof CachedPersister)) { + continue; + } + + $persister->getCacheRegion()->evictAll(); + } + } + + /** + * {@inheritdoc} + */ + public function containsCollection($className, $association, $ownerIdentifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + $key = $this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier); + + if ( ! ($persister instanceof CachedPersister)) { + return false; + } + + return $persister->getCacheRegion()->contains($key); + } + + /** + * {@inheritdoc} + */ + public function evictCollection($className, $association, $ownerIdentifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + $key = $this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evict($key); + } + + /** + * {@inheritdoc} + */ + public function evictCollectionRegion($className, $association) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evictAll(); + } + + /** + * {@inheritdoc} + */ + public function evictCollectionRegions() + { + $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); + + foreach ($metadatas as $metadata) { + + foreach ($metadata->associationMappings as $association) { + + if ( ! $association['type'] & ClassMetadata::TO_MANY) { + continue; + } + + $persister = $this->uow->getCollectionPersister($association); + + if ( ! ($persister instanceof CachedPersister)) { + continue; + } + + $persister->getCacheRegion()->evictAll(); + } + } + } + + /** + * {@inheritdoc} + */ + public function containsQuery($regionName) + { + return isset($this->queryCaches[$regionName]); + } + + /** + * {@inheritdoc} + */ + public function evictQueryRegion($regionName = null) + { + if ($regionName === null && $this->defaultQueryCache !== null) { + $this->defaultQueryCache->clear(); + + return; + } + + if (isset($this->queryCaches[$regionName])) { + $this->queryCaches[$regionName]->clear(); + } + } + + /** + * {@inheritdoc} + */ + public function evictQueryRegions() + { + $this->getQueryCache()->clear(); + + foreach ($this->queryCaches as $queryCache) { + $queryCache->clear(); + } + } + + /** + * {@inheritdoc} + */ + public function getQueryCache($regionName = null) + { + if ($regionName === null) { + return $this->defaultQueryCache ?: + $this->defaultQueryCache = $this->cacheFactory->buildQueryCache($this->em); + } + + if ( ! isset($this->queryCaches[$regionName])) { + $this->queryCaches[$regionName] = $this->cacheFactory->buildQueryCache($this->em, $regionName); + } + + return $this->queryCaches[$regionName]; + } + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param mixed $identifier The entity identifier. + * + * @return \Doctrine\ORM\Cache\EntityCacheKey + */ + private function buildEntityCacheKey(ClassMetadata $metadata, $identifier) + { + if ( ! is_array($identifier)) { + $identifier = $this->toIdentifierArray($metadata, $identifier); + } + + return new EntityCacheKey($metadata->rootEntityName, $identifier); + } + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param string $association The field name that represents the association. + * @param mixed $ownerIdentifier The identifier of the owning entity. + * + * @return \Doctrine\ORM\Cache\CollectionCacheKey + */ + private function buildCollectionCacheKey(ClassMetadata $metadata, $association, $ownerIdentifier) + { + if ( ! is_array($ownerIdentifier)) { + $ownerIdentifier = $this->toIdentifierArray($metadata, $ownerIdentifier);; + } + + return new CollectionCacheKey($metadata->rootEntityName, $association, $ownerIdentifier); + } + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param mixed $identifier The entity identifier. + * + * @return array + */ + private function toIdentifierArray(ClassMetadata $metadata, $identifier) + { + if (is_object($identifier) && $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($identifier))) { + $identifier = $this->uow->getSingleIdentifierValue($identifier); + + if ($identifier === null) { + throw ORMInvalidArgumentException::invalidIdentifierBindingEntity(); + } + } + + return array($metadata->identifier[0] => $identifier); + } + +} diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php new file mode 100644 index 00000000000..bb6a90901c8 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -0,0 +1,193 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache; +use Doctrine\ORM\Configuration; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Cache\Region\DefaultRegion; +use Doctrine\ORM\Cache\Region\FileLockRegion; +use Doctrine\Common\Cache\Cache as CacheDriver; + +use Doctrine\ORM\Persisters\EntityPersister; +use Doctrine\ORM\Persisters\CollectionPersister; +use Doctrine\ORM\Cache\Persister\ReadOnlyCachedEntityPersister; +use Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister; +use Doctrine\ORM\Cache\Persister\ReadOnlyCachedCollectionPersister; +use Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister; +use Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedEntityPersister; +use Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedCollectionPersister; + +/** + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultCacheFactory implements CacheFactory +{ + /** + * @var \Doctrine\Common\Cache\Cache + */ + private $cache; + + /** + * @var \Doctrine\ORM\Configuration + */ + private $configuration; + + /** + * @var array + */ + private $regions; + + /** + * @var string + */ + private $fileLockRegionDirectory; + + /** + * @param \Doctrine\ORM\Configuration $configuration + * @param \Doctrine\Common\Cache\Cache $cache + */ + public function __construct(Configuration $configuration, CacheDriver $cache) + { + $this->cache = $cache; + $this->configuration = $configuration; + } + + /** + * @param string $fileLockRegionDirectory + */ + public function setFileLockRegionDirectory($fileLockRegionDirectory) + { + $this->fileLockRegionDirectory = $fileLockRegionDirectory; + } + + /** + * @return string + */ + public function getFileLockRegionDirectory() + { + return $this->fileLockRegionDirectory; + } + + /** + * {@inheritdoc} + */ + public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata) + { + $region = $this->getRegion($metadata->cache); + $usage = $metadata->cache['usage']; + + if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) { + return new ReadOnlyCachedEntityPersister($persister, $region, $em, $metadata); + } + + if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) { + return new NonStrictReadWriteCachedEntityPersister($persister, $region, $em, $metadata); + } + + if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) { + return new ReadWriteCachedEntityPersister($persister, $region, $em, $metadata); + } + + throw new \InvalidArgumentException(sprintf("Unrecognized access strategy type [%s]", $usage)); + } + + /** + * {@inheritdoc} + */ + public function buildCachedCollectionPersister(EntityManagerInterface $em, CollectionPersister $persister, array $mapping) + { + $usage = $mapping['cache']['usage']; + $region = $this->getRegion($mapping['cache']); + + if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) { + return new ReadOnlyCachedCollectionPersister($persister, $region, $em, $mapping); + } + + if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) { + return new NonStrictReadWriteCachedCollectionPersister($persister, $region, $em, $mapping); + } + + if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) { + return new ReadWriteCachedCollectionPersister($persister, $region, $em, $mapping); + } + + throw new \InvalidArgumentException(sprintf("Unrecognized access strategy type [%s]", $usage)); + } + + /** + * {@inheritdoc} + */ + public function buildQueryCache(EntityManagerInterface $em, $regionName = null) + { + return new DefaultQueryCache($em, $this->getRegion(array( + 'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME, + 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE + ))); + } + + /** + * {@inheritdoc} + */ + public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping) + { + return new DefaultCollectionHydrator($em); + } + + /** + * {@inheritdoc} + */ + public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata) + { + return new DefaultEntityHydrator($em); + } + + /** + * {@inheritdoc} + */ + public function getRegion(array $cache) + { + if (isset($this->regions[$cache['region']])) { + return $this->regions[$cache['region']]; + } + + $region = new DefaultRegion($cache['region'], clone $this->cache, array( + 'lifetime' => $this->configuration->getSecondLevelCacheRegionLifetime($cache['region']) + )); + + if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { + + if ( ! $this->fileLockRegionDirectory) { + throw new \RuntimeException( + 'To use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' . + 'The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory, DefaultCacheFactory#setFileLockRegionDirectory(). ' + ); + } + + $directory = $this->fileLockRegionDirectory . DIRECTORY_SEPARATOR . $cache['region']; + $region = new FileLockRegion($region, $directory, $this->configuration->getSecondLevelCacheLockLifetime()); + } + + return $this->regions[$cache['region']] = $region; + } +} diff --git a/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php b/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php new file mode 100644 index 00000000000..fb4a45d2f66 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php @@ -0,0 +1,103 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Query; +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\CollectionCacheEntry; + +/** + * Default hidrator cache for collections + * + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultCollectionHydrator implements CollectionHydrator +{ + /** + * @var \Doctrine\ORM\EntityManagerInterface + */ + private $em; + + /** + * @var \Doctrine\ORM\UnitOfWork + */ + private $uow; + + /** + * @var array + */ + private static $hints = array(Query::HINT_CACHE_ENABLED => true); + + /** + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + */ + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + $this->uow = $em->getUnitOfWork(); + } + + /** + * {@inheritdoc} + */ + public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, $collection) + { + $data = array(); + + foreach ($collection as $index => $entity) { + $data[$index] = $this->uow->getEntityIdentifier($entity); + } + + return new CollectionCacheEntry($data); + } + + /** + * {@inheritdoc} + */ + public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection) + { + $assoc = $metadata->associationMappings[$key->association]; + $targetPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + $targetRegion = $targetPersister->getCacheRegion(); + $list = array(); + + foreach ($entry->identifiers as $index => $identifier) { + + $entityEntry = $targetRegion->get(new EntityCacheKey($assoc['targetEntity'], $identifier)); + + if ($entityEntry === null) { + return null; + } + + $list[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->data, self::$hints); + } + + array_walk($list, function($entity, $index) use ($collection) { + $collection->hydrateSet($index, $entity); + }); + + return $list; + } +} diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php new file mode 100644 index 00000000000..fede42d517f --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -0,0 +1,151 @@ +. + */ + +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; +use Doctrine\ORM\Cache\EntityCacheEntry; + +/** + * Default hidrator cache for entities + * + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultEntityHydrator implements EntityHydrator +{ + /** + * @var \Doctrine\ORM\EntityManager + */ + private $em; + + /** + * @var \Doctrine\ORM\UnitOfWork + */ + private $uow; + + /** + * @var array + */ + private static $hints = array(Query::HINT_CACHE_ENABLED => true); + + /** + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + */ + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + $this->uow = $em->getUnitOfWork(); + } + + /** + * {@inheritdoc} + */ + public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $entity) + { + $data = $this->uow->getOriginalEntityData($entity); + $data = array_merge($data, $key->identifier); // why update has no identifier values ? + + foreach ($metadata->associationMappings as $name => $assoc) { + + if ( ! isset($data[$name])) { + continue; + } + + if ( ! isset($assoc['cache']) || + ($assoc['type'] & ClassMetadata::TO_ONE) === 0 || + ($data[$name] instanceof Proxy && ! $data[$name]->__isInitialized__)) { + + unset($data[$name]); + + continue; + } + + if ( ! isset($assoc['id'])) { + $data[$name] = $this->uow->getEntityIdentifier($data[$name]); + + continue; + } + + // handle association identifier + $targetId = is_object($data[$name]) && $this->em->getMetadataFactory()->hasMetadataFor(get_class($data[$name])) + ? $this->uow->getEntityIdentifier($data[$name]) + : $data[$name]; + + // @TODO - fix it ! + // hande UnitOfWork#createEntity hash generation + if ( ! is_array($targetId)) { + + $data[reset($assoc['joinColumnFieldNames'])] = $targetId; + + $targetEntity = $this->em->getClassMetadata($assoc['targetEntity']); + $targetId = array($targetEntity->identifier[0] => $targetId); + } + + $data[$name] = $targetId; + } + + return new EntityCacheEntry($metadata->name, $data); + } + + /** + * {@inheritdoc} + */ + public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, $entity = null) + { + $data = $entry->data; + $hints = self::$hints; + + if ($entity !== null) { + $hints[Query::HINT_REFRESH] = true; + $hints[Query::HINT_REFRESH_ENTITY] = $entity; + } + + foreach ($metadata->associationMappings as $name => $assoc) { + + if ( ! isset($assoc['cache']) || ! isset($data[$name])) { + continue; + } + + $assocId = $data[$name]; + $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + $assocRegion = $assocPersister->getCacheRegion(); + $assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assocId)); + + if ($assocEntry === null) { + return null; + } + + $data[$name] = $assoc['fetch'] === ClassMetadata::FETCH_EAGER + ? $this->uow->createEntity($assocEntry->class, $assocEntry->data, $hints) + : $this->em->getReference($assocEntry->class, $assocId); + } + + if ($entity !== null) { + $this->uow->registerManaged($entity, $key->identifier, $data); + } + + return $this->uow->createEntity($entry->class, $data, $hints); + } +} diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php new file mode 100644 index 00000000000..7fe70728754 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -0,0 +1,294 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Cache\Persister\CachedPersister; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Query\ResultSetMapping; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Cache\QueryCacheEntry; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Cache\CacheException; +use Doctrine\Common\Proxy\Proxy; +use Doctrine\ORM\Cache; +use Doctrine\ORM\Query; + +/** + * Default query cache implementation. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultQueryCache implements QueryCache +{ + /** + * @var \Doctrine\ORM\EntityManagerInterface + */ + private $em; + + /** + * @var \Doctrine\ORM\UnitOfWork + */ + private $uow; + + /** + * @var \Doctrine\ORM\Cache\Region + */ + private $region; + + /** + * @var \Doctrine\ORM\Cache\QueryCacheValidator + */ + private $validator; + + /** + * @var array + */ + private static $hints = array(Query::HINT_CACHE_ENABLED => true); + + /** + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param \Doctrine\ORM\Cache\Region $region The query region. + */ + public function __construct(EntityManagerInterface $em, Region $region) + { + $this->em = $em; + $this->region = $region; + $this->uow = $em->getUnitOfWork(); + $this->validator = $em->getConfiguration()->getSecondLevelCacheQueryValidator(); + } + + /** + * {@inheritdoc} + * + * @TODO - does not work recursively yet + */ + public function get(QueryCacheKey $key, ResultSetMapping $rsm) + { + if ( ! ($key->cacheMode & Cache::MODE_GET)) { + return null; + } + + $entry = $this->region->get($key); + + if ( ! $entry instanceof QueryCacheEntry) { + return null; + } + + if ( ! $this->validator->isValid($key, $entry)) { + $this->region->evict($key); + + return null; + } + + $result = array(); + $entityName = reset($rsm->aliasMap); //@TODO find root entity + $hasRelation = ( ! empty($rsm->relationMap)); + $persister = $this->uow->getEntityPersister($entityName); + $region = $persister->getCacheRegion(); + + // @TODO - move to cache hydration componente + foreach ($entry->result as $index => $entry) { + + if (($entityEntry = $region->get(new EntityCacheKey($entityName, $entry['identifier']))) === null) { + return null; + } + + if ( ! $hasRelation) { + $result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->data, self::$hints); + + continue; + } + + $data = $entityEntry->data; + + foreach ($entry['associations'] as $name => $assoc) { + + $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + $assocRegion = $assocPersister->getCacheRegion(); + + if ($assoc['type'] & ClassMetadata::TO_ONE) { + + if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) { + return null; + } + + $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints); + + continue; + } + + if ( ! isset($assoc['list']) || empty($assoc['list'])) { + continue; + } + + $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); + $collection = new PersistentCollection($this->em, $targetClass, new ArrayCollection()); + + foreach ($assoc['list'] as $assocIndex => $assocId) { + + if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) { + return null; + } + + $element = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints); + + $collection->hydrateSet($assocIndex, $element); + } + + $data[$name] = $collection; + + $collection->setInitialized(true); + } + + $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints); + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @TODO - does not work recursively yet + */ + public function put(QueryCacheKey $key, ResultSetMapping $rsm, array $result) + { + if ($rsm->scalarMappings) { + throw new CacheException("Second level cache does not suport scalar results."); + } + + if ( ! ($key->cacheMode & Cache::MODE_PUT)) { + return false; + } + + $data = array(); + $entityName = reset($rsm->aliasMap); //@TODO find root entity + $hasRelation = ( ! empty($rsm->relationMap)); + $metadata = $this->em->getClassMetadata($entityName); + $persister = $this->uow->getEntityPersister($entityName); + + if ( ! ($persister instanceof CachedPersister)) { + throw CacheException::nonCacheableEntity($entityName); + } + + $region = $persister->getCacheRegion(); + + foreach ($result as $index => $entity) { + $identifier = $this->uow->getEntityIdentifier($entity); + $data[$index]['identifier'] = $identifier; + $data[$index]['associations'] = array(); + + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey = new EntityCacheKey($entityName, $identifier))) { + // Cancel put result if entity put fail + if ( ! $persister->storeEntityCache($entity, $entityKey)) { + return false; + } + } + + if ( ! $hasRelation) { + continue; + } + + // @TODO - move to cache hydration componente + foreach ($rsm->relationMap as $name) { + $assoc = $metadata->associationMappings[$name]; + + if (($assocValue = $metadata->getFieldValue($entity, $name)) === null || $assocValue instanceof Proxy) { + continue; + } + + if ( ! isset($assoc['cache'])) { + throw CacheException::nonCacheableEntityAssociation($entityName, $name); + } + + $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + $assocRegion = $assocPersister->getCacheRegion(); + $assocMetadata = $assocPersister->getClassMetadata(); + + // Handle *-to-one associations + if ($assoc['type'] & ClassMetadata::TO_ONE) { + + $assocIdentifier = $this->uow->getEntityIdentifier($assocValue); + + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier))) { + + // Cancel put result if association entity put fail + if ( ! $assocPersister->storeEntityCache($assocValue, $entityKey)) { + return false; + } + } + + $data[$index]['associations'][$name] = array( + 'targetEntity' => $assocMetadata->rootEntityName, + 'identifier' => $assocIdentifier, + 'type' => $assoc['type'] + ); + + continue; + } + + // Handle *-to-many associations + $list = array(); + + foreach ($assocValue as $assocItemIndex => $assocItem) { + $assocIdentifier = $this->uow->getEntityIdentifier($assocItem); + + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier))) { + + // Cancel put result if entity put fail + if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) { + return false; + } + } + + $list[$assocItemIndex] = $assocIdentifier; + } + + $data[$index]['associations'][$name] = array( + 'targetEntity' => $assocMetadata->rootEntityName, + 'type' => $assoc['type'], + 'list' => $list, + ); + } + } + + return $this->region->put($key, new QueryCacheEntry($data)); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->region->evictAll(); + } + + /** + * {@inheritdoc} + */ + public function getRegion() + { + return $this->region; + } +} diff --git a/lib/Doctrine/ORM/Cache/EntityCacheEntry.php b/lib/Doctrine/ORM/Cache/EntityCacheEntry.php new file mode 100644 index 00000000000..73ce222b3c9 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/EntityCacheEntry.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Entity cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class EntityCacheEntry implements CacheEntry +{ + /** + * @var array + */ + public $data; + + /** + * @var string + */ + public $class; + + /** + * @param string $class The entity class. + * @param array $data The entity data. + */ + public function __construct($class, array $data) + { + $this->class = $class; + $this->data = $data; + } + + /** + * @param array $values + */ + public static function __set_state(array $values) + { + return new self($values['class'], $values['data']); + } +} diff --git a/lib/Doctrine/ORM/Cache/EntityCacheKey.php b/lib/Doctrine/ORM/Cache/EntityCacheKey.php new file mode 100644 index 00000000000..7729691a6b0 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/EntityCacheKey.php @@ -0,0 +1,53 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Defines entity classes roles to be stored in the cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class EntityCacheKey extends CacheKey +{ + /** + * @var array + */ + public $identifier; + + /** + * @var string + */ + public $entityClass; + + /** + * @param string $entityClass The entity class name. In a inheritance hierarchy it should always be the root entity class. + * @param array $identifier The entity identifier + */ + public function __construct($entityClass, array $identifier) + { + ksort($identifier); + + $this->identifier = $identifier; + $this->entityClass = $entityClass; + $this->hash = str_replace('\\', '.', strtolower($entityClass) . '_' . implode(' ', $identifier)); + } +} diff --git a/lib/Doctrine/ORM/Cache/EntityHydrator.php b/lib/Doctrine/ORM/Cache/EntityHydrator.php new file mode 100644 index 00000000000..3e382cd75ca --- /dev/null +++ b/lib/Doctrine/ORM/Cache/EntityHydrator.php @@ -0,0 +1,51 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Cache\EntityCacheEntry; + +/** + * Hidrator cache entry for entities + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface EntityHydrator +{ + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param \Doctrine\ORM\Cache\EntityCacheKey $key The entity cache key. + * @param object $entity The entity. + * + * @return \Doctrine\ORM\Cache\EntityCacheEntry + */ + public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $entity); + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param \Doctrine\ORM\Cache\EntityCacheKey $key The entity cache key. + * @param \Doctrine\ORM\Cache\EntityCacheEntry $entry The entity cache entry. + * @param object $entity The entity to load the cache into. If not specified, a new entity is created. + */ + public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, $entity = null); +} diff --git a/lib/Doctrine/ORM/Cache/Lock.php b/lib/Doctrine/ORM/Cache/Lock.php new file mode 100644 index 00000000000..5346aa32371 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Lock.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Cache Lock + * + * @since 2.5 + * @author Fabio B. Silva + */ +class Lock +{ + /** + * @var string + */ + public $value; + + /** + * @var integer + */ + public $time; + + /** + * @param string $value + * @param integer $time + */ + public function __construct($value, $time = null) + { + $this->value = $value; + $this->time = $time ? : time(); + } + + /** + * @return \Doctrine\ORM\Cache\Lock + */ + public static function createLockRead() + { + return new self(uniqid(time())); + } +} diff --git a/lib/Doctrine/ORM/Cache/LockException.php b/lib/Doctrine/ORM/Cache/LockException.php new file mode 100644 index 00000000000..d4c76240aa9 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/LockException.php @@ -0,0 +1,32 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Lock exception for cache. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class LockException extends CacheException +{ + +} diff --git a/lib/Doctrine/ORM/Cache/Logging/CacheLogger.php b/lib/Doctrine/ORM/Cache/Logging/CacheLogger.php new file mode 100644 index 00000000000..bdae4eea8c5 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Logging/CacheLogger.php @@ -0,0 +1,106 @@ +. + */ + +namespace Doctrine\ORM\Cache\Logging; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\QueryCacheKey; + +/** + * Interface for logging. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface CacheLogger +{ + /** + * Log an entity put into second level cache. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\EntityCacheKey $key The cache key of the entity. + */ + public function entityCachePut($regionName, EntityCacheKey $key); + + /** + * Log an entity get from second level cache resulted in a hit. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\EntityCacheKey $key The cache key of the entity. + */ + public function entityCacheHit($regionName, EntityCacheKey $key); + + /** + * Log an entity get from second level cache resulted in a miss. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\EntityCacheKey $key The cache key of the entity. + */ + public function entityCacheMiss($regionName, EntityCacheKey $key); + + /** + * Log an entity put into second level cache. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key The cache key of the collection. + */ + public function collectionCachePut($regionName, CollectionCacheKey $key); + + /** + * Log an entity get from second level cache resulted in a hit. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key The cache key of the collection. + */ + public function collectionCacheHit($regionName, CollectionCacheKey $key); + + /** + * Log an entity get from second level cache resulted in a miss. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key The cache key of the collection. + */ + public function collectionCacheMiss($regionName, CollectionCacheKey $key); + + /** + * Log a query put into the query cache. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\QueryCacheKey $key The cache key of the query. + */ + public function queryCachePut($regionName, QueryCacheKey $key); + + /** + * Log a query get from the query cache resulted in a hit. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\QueryCacheKey $key The cache key of the query. + */ + public function queryCacheHit($regionName, QueryCacheKey $key); + + /** + * Log a query get from the query cache resulted in a miss. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\QueryCacheKey $key The cache key of the query. + */ + public function queryCacheMiss($regionName, QueryCacheKey $key); +} diff --git a/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php b/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php new file mode 100644 index 00000000000..283bd451247 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php @@ -0,0 +1,227 @@ +. + */ + +namespace Doctrine\ORM\Cache\Logging; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\QueryCacheKey; + +/** + * Provide basic second level cache statistics. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class StatisticsCacheLogger implements CacheLogger +{ + /** + * @var array + */ + private $cacheMissCountMap = array(); + + /** + * @var array + */ + private $cacheHitCountMap = array(); + + /** + * @var array + */ + private $cachePutCountMap = array(); + + /** + * {@inheritdoc} + */ + public function collectionCacheMiss($regionName, CollectionCacheKey $key) + { + $this->cacheMissCountMap[$regionName] = isset($this->cacheMissCountMap[$regionName]) + ? $this->cacheMissCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function collectionCacheHit($regionName, CollectionCacheKey $key) + { + $this->cacheHitCountMap[$regionName] = isset($this->cacheHitCountMap[$regionName]) + ? $this->cacheHitCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function collectionCachePut($regionName, CollectionCacheKey $key) + { + $this->cachePutCountMap[$regionName] = isset($this->cachePutCountMap[$regionName]) + ? $this->cachePutCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function entityCacheMiss($regionName, EntityCacheKey $key) + { + $this->cacheMissCountMap[$regionName] = isset($this->cacheMissCountMap[$regionName]) + ? $this->cacheMissCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function entityCacheHit($regionName, EntityCacheKey $key) + { + $this->cacheHitCountMap[$regionName] = isset($this->cacheHitCountMap[$regionName]) + ? $this->cacheHitCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function entityCachePut($regionName, EntityCacheKey $key) + { + $this->cachePutCountMap[$regionName] = isset($this->cachePutCountMap[$regionName]) + ? $this->cachePutCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function queryCacheHit($regionName, QueryCacheKey $key) + { + $this->cacheHitCountMap[$regionName] = isset($this->cacheHitCountMap[$regionName]) + ? $this->cacheHitCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function queryCacheMiss($regionName, QueryCacheKey $key) + { + $this->cacheMissCountMap[$regionName] = isset($this->cacheMissCountMap[$regionName]) + ? $this->cacheMissCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function queryCachePut($regionName, QueryCacheKey $key) + { + $this->cachePutCountMap[$regionName] = isset($this->cachePutCountMap[$regionName]) + ? $this->cachePutCountMap[$regionName] + 1 + : 1; + } + + /** + * Get the number of entries successfully retrieved from cache. + * + * @param string $regionName The name of the cache region. + * + * @return integer + */ + public function getRegionHitCount($regionName) + { + return isset($this->cacheHitCountMap[$regionName]) ? $this->cacheHitCountMap[$regionName] : 0; + } + + /** + * Get the number of cached entries *not* found in cache. + * + * @param string $regionName The name of the cache region. + * + * @return integer + */ + public function getRegionMissCount($regionName) + { + return isset($this->cacheMissCountMap[$regionName]) ? $this->cacheMissCountMap[$regionName] : 0; + } + + /** + * Get the number of cacheable entries put in cache. + * + * @param string $regionName The name of the cache region. + * + * @return integer + */ + public function getRegionPutCount($regionName) + { + return isset($this->cachePutCountMap[$regionName]) ? $this->cachePutCountMap[$regionName] : 0; + } + + /** + * Clear region statistics + * + * @param string $regionName The name of the cache region. + */ + public function clearRegionStats($regionName) + { + $this->cachePutCountMap[$regionName] = 0; + $this->cacheHitCountMap[$regionName] = 0; + $this->cacheMissCountMap[$regionName] = 0; + } + + /** + * Clear all statistics + */ + public function clearStats() + { + $this->cachePutCountMap = array(); + $this->cacheHitCountMap = array(); + $this->cacheMissCountMap = array(); + } + + /** + * Get the total number of put in cache. + * + * @return integer + */ + public function getPutCount() + { + return array_sum($this->cachePutCountMap); + } + + /** + * Get the total number of entries successfully retrieved from cache. + * + * @return integer + */ + public function getHitCount() + { + return array_sum($this->cacheHitCountMap); + } + + /** + * Get the total number of cached entries *not* found in cache. + * + * @return integer + */ + public function getMissCount() + { + return array_sum($this->cacheMissCountMap); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php new file mode 100644 index 00000000000..23a24994035 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php @@ -0,0 +1,275 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Persisters\CollectionPersister; +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Cache\Region; +use Doctrine\Common\Util\ClassUtils; + +/** + * @author Fabio B. Silva + * @since 2.5 + */ +abstract class AbstractCollectionPersister implements CachedCollectionPersister +{ + /** + * @var \Doctrine\ORM\UnitOfWork + */ + protected $uow; + + /** + * @var \Doctrine\ORM\Mapping\ClassMetadataFactory + */ + protected $metadataFactory; + + /** + * @var \Doctrine\ORM\Persisters\CollectionPersister + */ + protected $persister; + + /** + * @var \Doctrine\ORM\Mapping\ClassMetadata + */ + protected $sourceEntity; + + /** + * @var \Doctrine\ORM\Mapping\ClassMetadata + */ + protected $targetEntity; + + /** + * @var array + */ + protected $association; + + /** + * @var array + */ + protected $queuedCache = array(); + + /** + * @var \Doctrine\ORM\Cache\Region + */ + protected $region; + + /** + * @var string + */ + protected $regionName; + + /** + * @var \Doctrine\ORM\Cache\CollectionHydrator + */ + protected $hidrator; + + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + protected $cacheLogger; + + /** + * @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached. + * @param \Doctrine\ORM\Cache\Region $region The collection region. + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param array $mapping The association mapping. + */ + public function __construct(CollectionPersister $persister, Region $region, EntityManagerInterface $em, array $association) + { + $configuration = $em->getConfiguration(); + $cacheFactory = $configuration->getSecondLevelCacheFactory(); + + $this->region = $region; + $this->persister = $persister; + $this->association = $association; + $this->regionName = $region->getName(); + $this->uow = $em->getUnitOfWork(); + $this->metadataFactory = $em->getMetadataFactory(); + $this->cacheLogger = $configuration->getSecondLevelCacheLogger(); + $this->hidrator = $cacheFactory->buildCollectionHydrator($em, $association); + $this->sourceEntity = $em->getClassMetadata($association['sourceEntity']); + $this->targetEntity = $em->getClassMetadata($association['targetEntity']); + } + + /** + * {@inheritdoc} + */ + public function getCacheRegion() + { + return $this->region; + } + + /** + * {@inheritdoc} + */ + public function getSourceEntityMetadata() + { + return $this->sourceEntity; + } + + /** + * {@inheritdoc} + */ + public function getTargetEntityMetadata() + { + return $this->targetEntity; + } + + /** + * @param \Doctrine\ORM\PersistentCollection $collection + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key + * + * @return \Doctrine\ORM\PersistentCollection|null + */ + public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key) + { + + if (($cache = $this->region->get($key)) === null) { + return null; + } + + if (($cache = $this->hidrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection)) === null) { + return null; + } + + return $cache; + } + + /** + * {@inheritdoc} + */ + public function storeCollectionCache(CollectionCacheKey $key, $elements) + { + $targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName); + $targetRegion = $targetPersister->getCacheRegion(); + $targetHidrator = $targetPersister->getEntityHydrator(); + $entry = $this->hidrator->buildCacheEntry($this->targetEntity, $key, $elements); + + foreach ($entry->identifiers as $index => $identifier) { + $entityKey = new EntityCacheKey($this->targetEntity->rootEntityName, $identifier); + + if ($targetRegion->contains($entityKey)) { + continue; + } + + $class = $this->targetEntity; + $className = ClassUtils::getClass($elements[$index]); + + if ($className !== $this->targetEntity->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $entity = $elements[$index]; + $entityEntry = $targetHidrator->buildCacheEntry($class, $entityKey, $entity); + + $targetRegion->put($entityKey, $entityEntry); + } + + $cached = $this->region->put($key, $entry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->collectionCachePut($this->regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function contains(PersistentCollection $collection, $element) + { + return $this->persister->contains($collection, $element); + } + + /** + * {@inheritdoc} + */ + public function containsKey(PersistentCollection $collection, $key) + { + return $this->persister->containsKey($collection, $key); + } + + /** + * {@inheritdoc} + */ + public function count(PersistentCollection $collection) + { + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $entry = $this->region->get($key); + + if ($entry !== null) { + return count($entry->identifiers); + } + + return $this->persister->count($collection); + } + + /** + * {@inheritdoc} + */ + public function deleteRows(PersistentCollection $collection) + { + $this->persister->deleteRows($collection); + } + + /** + * {@inheritdoc} + */ + public function insertRows(PersistentCollection $collection) + { + $this->persister->insertRows($collection); + } + + /** + * {@inheritdoc} + */ + public function get(PersistentCollection $collection, $index) + { + return $this->persister->get($collection, $index); + } + + /** + * {@inheritdoc} + */ + public function removeElement(PersistentCollection $collection, $element) + { + return $this->persister->removeElement($collection, $element); + } + + /** + * {@inheritdoc} + */ + public function removeKey(PersistentCollection $collection, $key) + { + return $this->persister->removeKey($collection, $key); + } + + /** + * {@inheritdoc} + */ + public function slice(PersistentCollection $collection, $offset, $length = null) + { + return $this->persister->slice($collection, $offset, $length); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php new file mode 100644 index 00000000000..25e4bd55901 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -0,0 +1,515 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache; +use Doctrine\ORM\Cache\Region; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\QueryCacheKey; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Persisters\EntityPersister; + +use Doctrine\Common\Util\ClassUtils; +use Doctrine\Common\Collections\Criteria; + +/** + * @author Fabio B. Silva + * @since 2.5 + */ +abstract class AbstractEntityPersister implements CachedEntityPersister +{ + /** + * @var \Doctrine\ORM\UnitOfWork + */ + protected $uow; + + /** + * @var \Doctrine\ORM\Mapping\ClassMetadataFactory + */ + protected $metadataFactory; + + /** + * @var \Doctrine\ORM\Persisters\EntityPersister + */ + protected $persister; + + /** + * @var \Doctrine\ORM\Mapping\ClassMetadata + */ + protected $class; + + /** + * @var array + */ + protected $queuedCache = array(); + + /** + * @var \Doctrine\ORM\Cache\Region + */ + protected $region; + + /** + * @var \Doctrine\ORM\Cache\EntityHydrator + */ + protected $hidrator; + + /** + * @var \Doctrine\ORM\Cache + */ + protected $cache; + + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + protected $cacheLogger; + + /** + * @var string + */ + protected $regionName; + + /** + * @param \Doctrine\ORM\Persisters\EntityPersister $persister The entity persister to cache. + * @param \Doctrine\ORM\Cache\Region $region The entity cache region. + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param \Doctrine\ORM\Mapping\ClassMetadata $class The entity metadata. + */ + public function __construct(EntityPersister $persister, Region $region, EntityManagerInterface $em, ClassMetadata $class) + { + $config = $em->getConfiguration(); + $factory = $config->getSecondLevelCacheFactory(); + + $this->class = $class; + $this->region = $region; + $this->persister = $persister; + $this->cache = $em->getCache(); + $this->regionName = $region->getName(); + $this->uow = $em->getUnitOfWork(); + $this->metadataFactory = $em->getMetadataFactory(); + $this->cacheLogger = $config->getSecondLevelCacheLogger(); + $this->hidrator = $factory->buildEntityHydrator($em, $class); + } + + /** + * {@inheritdoc} + */ + public function addInsert($entity) + { + $this->persister->addInsert($entity); + } + + /** + * {@inheritdoc} + */ + public function getInserts() + { + return $this->persister->getInserts(); + } + + /** + * {@inheritdoc} + */ + public function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) + { + return $this->persister->getSelectSQL($criteria, $assoc, $lockMode, $limit, $offset, $orderBy); + } + + /** + * {@inheritdoc} + */ + public function getInsertSQL() + { + return $this->persister->getInsertSQL(); + } + + /** + * {@inheritdoc} + */ + public function getResultSetMapping() + { + return $this->persister->getResultSetMapping(); + } + + /** + * {@inheritdoc} + */ + public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null) + { + return $this->persister->getSelectConditionStatementSQL($field, $value, $assoc, $comparison); + } + + /** + * {@inheritdoc} + */ + public function exists($entity, array $extraConditions = array()) + { + if (empty($extraConditions)) { + + $key = new EntityCacheKey($this->class->rootEntityName, $this->class->getIdentifierValues($entity)); + + if ($this->region->contains($key)) { + return true; + } + } + + return $this->persister->exists($entity, $extraConditions); + } + + /** + * {@inheritdoc} + */ + public function getCacheRegion() + { + return $this->region; + } + + /** + * @return \Doctrine\ORM\Cache\EntityHydrator + */ + public function getEntityHydrator() + { + return $this->hidrator; + } + + /** + * {@inheritdoc} + */ + public function storeEntityCache($entity, EntityCacheKey $key) + { + $class = $this->class; + $className = ClassUtils::getClass($entity); + + if ($className !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->entityCachePut($this->regionName, $key); + } + + return $cached; + } + + /** + * Generates a string of currently query + * + * @return string + */ + protected function getHash($query, $criteria, array $orderBy = null, $limit = null, $offset = null) + { + list($params) = $this->expandParameters($criteria); + + return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset); + } + + /** + * {@inheritdoc} + */ + public function expandParameters($criteria) + { + return $this->persister->expandParameters($criteria); + } + + /** + * {@inheritdoc} + */ + public function getClassMetadata() + { + return $this->persister->getClassMetadata(); + } + + /** + * {@inheritdoc} + */ + public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) + { + return $this->persister->getManyToManyCollection($assoc, $sourceEntity, $offset, $limit); + } + + /** + * {@inheritdoc} + */ + public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) + { + return $this->persister->getOneToManyCollection($assoc, $sourceEntity, $offset, $limit); + } + + /** + * {@inheritdoc} + */ + public function getOwningTable($fieldName) + { + return $this->persister->getOwningTable($fieldName); + } + + /** + * {@inheritdoc} + */ + public function executeInserts() + { + $this->queuedCache['insert'] = $this->persister->getInserts(); + + return $this->persister->executeInserts(); + } + + /** + * {@inheritdoc} + */ + public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null, array $orderBy = null) + { + //@TODO - Should throw exception ? + if ($entity !== null || $assoc !== null || ! empty($hints) || $lockMode !== 0) { + return $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy); + } + + //handle only EntityRepository#findOneBy + $query = $this->persister->getSelectSQL($criteria, null, 0, $limit, 0, $orderBy); + $hash = $this->getHash($query, $criteria); + $rsm = $this->getResultSetMapping(); + $querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL); + $queryCache = $this->cache->getQueryCache($this->regionName); + $result = $queryCache->get($querykey, $rsm); + + if ($result !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->queryCacheHit($this->regionName, $querykey); + } + + return $result[0]; + } + + if (($result = $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy)) === null) { + return null; + } + + $cached = $queryCache->put($querykey, $rsm, array($result)); + + if ($this->cacheLogger && $result) { + $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); + } + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->queryCachePut($this->regionName, $querykey); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null) + { + $query = $this->persister->getSelectSQL($criteria, null, 0, $limit, $offset, $orderBy); + $hash = $this->getHash($query, $criteria); + $rsm = $this->getResultSetMapping(); + $querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL); + $queryCache = $this->cache->getQueryCache($this->regionName); + $result = $queryCache->get($querykey, $rsm); + + if ($result !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->queryCacheHit($this->regionName, $querykey); + } + + return $result; + } + + $result = $this->persister->loadAll($criteria, $orderBy, $limit, $offset); + $cached = $queryCache->put($querykey, $rsm, $result); + + if ($this->cacheLogger && $result) { + $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); + } + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->queryCachePut($this->regionName, $querykey); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function loadById(array $identifier, $entity = null) + { + $cacheKey = new EntityCacheKey($this->class->rootEntityName, $identifier); + $cacheEntry = $this->region->get($cacheKey); + $class = $this->class; + + if ($cacheEntry !== null) { + + if ($cacheEntry->class !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($cacheEntry->class); + } + + if (($entity = $this->hidrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity)) !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->entityCacheHit($this->regionName, $cacheKey); + } + + return $entity; + } + } + + $entity = $this->persister->loadById($identifier, $entity); + + if ($entity === null) { + return null; + } + + $class = $this->class; + $className = ClassUtils::getClass($entity); + + if ($className !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $cacheEntry = $this->hidrator->buildCacheEntry($class, $cacheKey, $entity); + $cached = $this->region->put($cacheKey, $cacheEntry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->entityCachePut($this->regionName, $cacheKey); + } + + if ($this->cacheLogger) { + $this->cacheLogger->entityCacheMiss($this->regionName, $cacheKey); + } + + return $entity; + } + + /** + * {@inheritdoc} + */ + public function loadCriteria(Criteria $criteria) + { + return $this->persister->loadCriteria($criteria); + } + + /** + * {@inheritdoc} + */ + public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) + { + $persister = $this->uow->getCollectionPersister($assoc); + $hasCache = ($persister instanceof CachedPersister); + $key = null; + + if ($hasCache) { + $ownerId = $this->uow->getEntityIdentifier($coll->getOwner()); + $key = new CollectionCacheKey($assoc['sourceEntity'], $assoc['fieldName'], $ownerId); + $list = $persister->loadCollectionCache($coll, $key); + + if ($list !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key); + } + + return $list; + } + } + + $list = $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $coll); + + if ($hasCache && ! empty($list)) { + $persister->storeCollectionCache($key, $list); + + if ($this->cacheLogger) { + $this->cacheLogger->collectionCacheMiss($persister->getCacheRegion()->getName(), $key); + } + } + + return $list; + } + + /** + * {@inheritdoc} + */ + public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) + { + $persister = $this->uow->getCollectionPersister($assoc); + $hasCache = ($persister instanceof CachedPersister); + + if ($hasCache) { + $ownerId = $this->uow->getEntityIdentifier($coll->getOwner()); + $key = new CollectionCacheKey($assoc['sourceEntity'], $assoc['fieldName'], $ownerId); + $list = $persister->loadCollectionCache($coll, $key); + + if ($list !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key); + } + + return $list; + } + } + + $list = $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $coll); + + if ($hasCache && ! empty($list)) { + $persister->storeCollectionCache($key, $list); + + if ($this->cacheLogger) { + $this->cacheLogger->collectionCacheMiss($persister->getCacheRegion()->getName(), $key); + } + } + + return $list; + } + + /** + * {@inheritdoc} + */ + public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = array()) + { + return $this->persister->loadOneToOneEntity($assoc, $sourceEntity, $identifier); + } + + /** + * {@inheritdoc} + */ + public function lock(array $criteria, $lockMode) + { + $this->persister->lock($criteria, $lockMode); + } + + /** + * {@inheritdoc} + */ + public function refresh(array $id, $entity, $lockMode = 0) + { + $this->persister->refresh($id, $entity, $lockMode); + } + +} diff --git a/lib/Doctrine/ORM/Cache/Persister/CachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/CachedCollectionPersister.php new file mode 100644 index 00000000000..2318c590693 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/CachedCollectionPersister.php @@ -0,0 +1,64 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Persisters\CollectionPersister; +use Doctrine\ORM\PersistentCollection; + +/** + * Interface for second level cache collection persisters. + * + * @author Fabio B. Silva + * @since 2.5 + */ +interface CachedCollectionPersister extends CachedPersister, CollectionPersister +{ + /** + * @return \Doctrine\ORM\Mapping\ClassMetadata + */ + public function getSourceEntityMetadata(); + + /** + * @return \Doctrine\ORM\Mapping\ClassMetadata + */ + public function getTargetEntityMetadata(); + + /** + * Loads a collection from cache + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key + * + * @return \Doctrine\ORM\PersistentCollection|null + */ + public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key); + + /** + * Stores a collection into cache + * + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key + * @param array|\Doctrine\Common\Collections\Collection $elements + * + * @return void + */ + public function storeCollectionCache(CollectionCacheKey $key, $elements); +} diff --git a/lib/Doctrine/ORM/Cache/Persister/CachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/CachedEntityPersister.php new file mode 100644 index 00000000000..adf5fa0c6ee --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/CachedEntityPersister.php @@ -0,0 +1,45 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Persisters\EntityPersister; + +/** + * Interface for second level cache entity persisters. + * + * @author Fabio B. Silva + * @since 2.5 + */ +interface CachedEntityPersister extends CachedPersister, EntityPersister +{ + /** + * @return \Doctrine\ORM\Cache\EntityHydrator + */ + public function getEntityHydrator(); + + /** + * @param object $entity + * @param \Doctrine\ORM\Cache\EntityCacheKey $key + * @return boolean + */ + public function storeEntityCache($entity, EntityCacheKey $key); +} diff --git a/lib/Doctrine/ORM/Cache/Persister/CachedPersister.php b/lib/Doctrine/ORM/Cache/Persister/CachedPersister.php new file mode 100644 index 00000000000..89afd32095f --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/CachedPersister.php @@ -0,0 +1,46 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +/** + * Interface for persister that support second level cache. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface CachedPersister +{ + /** + * Perform whatever processing is encapsulated here after completion of the transaction. + */ + public function afterTransactionComplete(); + + /** + * Perform whatever processing is encapsulated here after completion of the rolled-back. + */ + public function afterTransactionRolledBack(); + + /** + * Gets the The region access. + * + * @return \Doctrine\ORM\Cache\Region + */ + public function getCacheRegion(); +} diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php new file mode 100644 index 00000000000..619a520b3c0 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php @@ -0,0 +1,105 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\PersistentCollection; + +/** + * @author Fabio B. Silva + * @since 2.5 + */ +class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPersister +{ + /** + * {@inheritdoc} + */ + public function afterTransactionComplete() + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->storeCollectionCache($item['key'], $item['list']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $key) { + $this->region->evict($key); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionRolledBack() + { + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function delete(PersistentCollection $collection) + { + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + + $this->persister->delete($collection); + + $this->queuedCache['delete'][spl_object_hash($collection)] = $key; + } + + /** + * {@inheritdoc} + */ + public function update(PersistentCollection $collection) + { + $isInitialized = $collection->isInitialized(); + $isDirty = $collection->isDirty(); + + if ( ! $isInitialized && ! $isDirty) { + return; + } + + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + + // Invalidate non initialized collections OR odered collection + if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) { + + $this->persister->update($collection); + + $this->queuedCache['delete'][spl_object_hash($collection)] = $key; + + return; + } + + $this->persister->update($collection); + + $this->queuedCache['update'][spl_object_hash($collection)] = array( + 'key' => $key, + 'list' => $collection + ); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php new file mode 100644 index 00000000000..281f7056206 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php @@ -0,0 +1,116 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\EntityCacheKey; + +use Doctrine\Common\Util\ClassUtils; + +/** + * Specific non-strict read/write cached entity persister + * + * @author Fabio B. Silva + * @since 2.5 + */ +class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister +{ + /** + * {@inheritdoc} + */ + public function afterTransactionComplete() + { + if (isset($this->queuedCache['insert'])) { + foreach ($this->queuedCache['insert'] as $entity) { + + $class = $this->class; + $className = ClassUtils::getClass($entity); + + if ($className !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->entityCachePut($this->regionName, $key); + } + } + } + + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $entity) { + + $class = $this->class; + $className = ClassUtils::getClass($entity); + + if ($className !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->entityCachePut($this->regionName, $key); + } + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $key) { + $this->region->evict($key); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionRolledBack() + { + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function delete($entity) + { + $this->persister->delete($entity); + + $this->queuedCache['delete'][] = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + } + + /** + * {@inheritdoc} + */ + public function update($entity) + { + $this->persister->update($entity); + + $this->queuedCache['update'][] = $entity; + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedCollectionPersister.php new file mode 100644 index 00000000000..20890e87d8c --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedCollectionPersister.php @@ -0,0 +1,44 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Cache\CacheException; +use Doctrine\Common\Util\ClassUtils; + +/** + * @author Fabio B. Silva + * @since 2.5 + */ +class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollectionPersister +{ + /** + * {@inheritdoc} + */ + public function update(PersistentCollection $collection) + { + if ($collection->isDirty() && count($collection->getSnapshot()) > 0) { + throw CacheException::updateReadOnlyCollection(ClassUtils::getClass($collection->getOwner()), $this->association['fieldName']); + } + + parent::update($collection); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedEntityPersister.php new file mode 100644 index 00000000000..69db37f60e1 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedEntityPersister.php @@ -0,0 +1,41 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\CacheException; +use Doctrine\Common\Util\ClassUtils; + +/** + * Specific read-only region entity persister + * + * @author Fabio B. Silva + * @since 2.5 + */ +class ReadOnlyCachedEntityPersister extends NonStrictReadWriteCachedEntityPersister +{ + /** + * {@inheritdoc} + */ + public function update($entity) + { + throw CacheException::updateReadOnlyEntity(ClassUtils::getClass($entity)); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php new file mode 100644 index 00000000000..573257bdd59 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php @@ -0,0 +1,140 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Persisters\CollectionPersister; +use Doctrine\ORM\EntityManagerInterface; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\ConcurrentRegion; +use Doctrine\ORM\PersistentCollection; + +/** + * @author Fabio B. Silva + * @since 2.5 + */ +class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister +{ + /** + * @var \Doctrine\ORM\Cache\ConcurrentRegion + */ + protected $region; + + /** + * @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached. + * @param \Doctrine\ORM\Cache\ConcurrentRegion $region The collection region. + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param array $mapping The association mapping. + */ + public function __construct(CollectionPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, array $association) + { + parent::__construct($persister, $region, $em, $association); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionComplete() + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionRolledBack() + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function delete(PersistentCollection $collection) + { + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $lock = $this->region->lock($key); + + $this->persister->delete($collection); + + if ($lock === null) { + return; + } + + $this->queuedCache['delete'][spl_object_hash($collection)] = array( + 'key' => $key, + 'lock' => $lock + ); + } + + /** + * {@inheritdoc} + */ + public function update(PersistentCollection $collection) + { + $isInitialized = $collection->isInitialized(); + $isDirty = $collection->isDirty(); + + if ( ! $isInitialized && ! $isDirty) { + return; + } + + $this->persister->update($collection); + + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $lock = $this->region->lock($key); + + if ($lock === null) { + return; + } + + $this->queuedCache['update'][spl_object_hash($collection)] = array( + 'key' => $key, + 'lock' => $lock + ); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php new file mode 100644 index 00000000000..e49c225411a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php @@ -0,0 +1,133 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Persisters\EntityPersister; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; + +use Doctrine\ORM\Cache\ConcurrentRegion; +use Doctrine\ORM\Cache\EntityCacheKey; + +/** + * Specific read-write entity persister + * + * @author Fabio B. Silva + * @since 2.5 + */ +class ReadWriteCachedEntityPersister extends AbstractEntityPersister +{ + /** + * @var \Doctrine\ORM\Cache\ConcurrentRegion + */ + protected $region; + + /** + * @param \Doctrine\ORM\Persister\EntityPersister $persister The entity persister to cache. + * @param \Doctrine\ORM\Cache\ConcurrentRegion $region The entity cache region. + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param \Doctrine\ORM\Mapping\ClassMetadata $class The entity metadata. + */ + public function __construct(EntityPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, ClassMetadata $class) + { + parent::__construct($persister, $region, $em, $class); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionComplete() + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionRolledBack() + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function delete($entity) + { + $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $lock = $this->region->lock($key); + + $this->persister->delete($entity); + + if ($lock === null) { + return; + } + + $this->queuedCache['delete'][] = array( + 'lock' => $lock, + 'key' => $key + ); + } + + /** + * {@inheritdoc} + */ + public function update($entity) + { + $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $lock = $this->region->lock($key); + + $this->persister->update($entity); + + if ($lock === null) { + return; + } + + $this->queuedCache['update'][] = array( + 'lock' => $lock, + 'key' => $key + ); + } +} diff --git a/lib/Doctrine/ORM/Cache/QueryCache.php b/lib/Doctrine/ORM/Cache/QueryCache.php new file mode 100644 index 00000000000..7bdc0d33789 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCache.php @@ -0,0 +1,60 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Query\ResultSetMapping; + +/** + * Defines the contract for caches capable of storing query results. + * These caches should only concern themselves with storing the matching result ids. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface QueryCache +{ + /** + * @return boolean + */ + public function clear(); + + /** + * @param \Doctrine\ORM\Cache\QueryCacheKey $key + * @param \Doctrine\ORM\Query\ResultSetMapping $rsm + * @param array $result + * + * @return boolean + */ + public function put(QueryCacheKey $key, ResultSetMapping $rsm, array $result); + + /** + * @param \Doctrine\ORM\Cache\QueryCacheKey $key + * @param \Doctrine\ORM\Query\ResultSetMapping $rsm + * + * @return void + */ + public function get(QueryCacheKey $key, ResultSetMapping $rsm); + + /** + * @return \Doctrine\ORM\Cache\Region + */ + public function getRegion(); +} diff --git a/lib/Doctrine/ORM/Cache/QueryCacheEntry.php b/lib/Doctrine/ORM/Cache/QueryCacheEntry.php new file mode 100644 index 00000000000..46e0d603f0b --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCacheEntry.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Query cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class QueryCacheEntry implements CacheEntry +{ + /** + * @var array + */ + public $result; + + /** + * @var integer + */ + public $time; + + /** + * @param array $result + * @param integer $time + */ + public function __construct($result, $time = null) + { + $this->result = $result; + $this->time = $time ?: time(); + } + + /** + * @param array $values + */ + public static function __set_state(array $values) + { + return new self($values['result'], $values['time']); + } +} diff --git a/lib/Doctrine/ORM/Cache/QueryCacheKey.php b/lib/Doctrine/ORM/Cache/QueryCacheKey.php new file mode 100644 index 00000000000..2355416827e --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCacheKey.php @@ -0,0 +1,53 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * A key that identifies a particular query with bound parameter values. + * This is the object Hibernate uses as its key into its query cache. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class QueryCacheKey extends CacheKey +{ + /** + * @var integer + */ + public $lifetime; + + /** + * @var integer + */ + public $cacheMode; + + /** + * @param string $hash Result cache id + * @param integer $lifetime Query lifetime + * @param integer $cacheMode Query cache mode + */ + public function __construct($hash, $lifetime, $cacheMode = 3) + { + $this->hash = $hash; + $this->lifetime = $lifetime; + $this->cacheMode = $cacheMode; + } +} diff --git a/lib/Doctrine/ORM/Cache/QueryCacheValidator.php b/lib/Doctrine/ORM/Cache/QueryCacheValidator.php new file mode 100644 index 00000000000..b4dfa79aa5a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCacheValidator.php @@ -0,0 +1,42 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache\QueryCacheEntry; + +/** + * Cache query validator interface. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface QueryCacheValidator +{ + /** + * Checks if the query entry is valid + * + * @param \Doctrine\ORM\Cache\QueryCacheEntry $key + * @param \Doctrine\ORM\Cache\QueryCacheEntry $entry + * + * @return boolean + */ + public function isValid(QueryCacheKey $key, QueryCacheEntry $entry); +} diff --git a/lib/Doctrine/ORM/Cache/Region.php b/lib/Doctrine/ORM/Cache/Region.php new file mode 100644 index 00000000000..16609400fc3 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Region.php @@ -0,0 +1,86 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache\Lock; + +/** + * Defines a contract for accessing a particular named region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface Region +{ + /** + * Retrieve the name of this region. + * + * @return string The region name + */ + public function getName(); + + /** + * Determine whether this region contains data for the given key. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The cache key + * + * @return boolean TRUE if the underlying cache contains corresponding data; FALSE otherwise. + */ + public function contains(CacheKey $key); + + /** + * Get an item from the cache. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to be retrieved. + * + * @return \Doctrine\ORM\Cache\CacheEntry The cached entry or NULL + * + * @throws \Doctrine\ORM\Cache\CacheException Indicates a problem accessing the item or region. + */ + public function get(CacheKey $key); + + /** + * Put an item into the cache. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key under which to cache the item. + * @param \Doctrine\ORM\Cache\CacheEntry $entry The entry to cache. + * @param \Doctrine\ORM\Cache\Lock $lock The lock previously obtained. + * + * @throws \Doctrine\ORM\Cache\CacheException Indicates a problem accessing the region. + */ + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null); + + /** + * Remove an item from the cache. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key under which to cache the item. + * + * @throws \Doctrine\ORM\Cache\CacheException Indicates a problem accessing the region. + */ + public function evict(CacheKey $key); + + /** + * Remove all contents of this particular cache region. + * + * @throws \Doctrine\ORM\Cache\CacheException Indicates problem accessing the region. + */ + public function evictAll(); +} diff --git a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php new file mode 100644 index 00000000000..0b62c5b8124 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -0,0 +1,126 @@ +. + */ + +namespace Doctrine\ORM\Cache\Region; + +use Doctrine\ORM\Cache\Lock; +use Doctrine\ORM\Cache\Region; +use Doctrine\ORM\Cache\CacheKey; +use Doctrine\ORM\Cache\CacheEntry; +use Doctrine\Common\Cache\CacheProvider; + +/** + * The simplest cache region compatible with all doctrine-cache drivers. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultRegion implements Region +{ + const ENTRY_KEY = '_entry_'; + + /** + * @var \Doctrine\Common\Cache\CacheProvider + */ + private $cache; + + /** + * @var string + */ + private $name; + + /** + * @var integer + */ + private $lifetime = 0; + + /** + * @param string $name + * @param \Doctrine\Common\Cache\CacheProvider $cache + * @param array $configuration + */ + public function __construct($name, CacheProvider $cache, array $configuration = array()) + { + $this->name = $name; + $this->cache = $cache; + + $this->cache->setNamespace($this->name); + + if (isset($configuration['lifetime']) && $configuration['lifetime'] > 0) { + $this->lifetime = (integer) $configuration['lifetime']; + } + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + /** + * @return \Doctrine\Common\Cache\Cache + */ + public function getCache() + { + return $this->cache; + } + + /** + * {@inheritdoc} + */ + public function contains(CacheKey $key) + { + return $this->cache->contains($this->name . self::ENTRY_KEY . $key->hash); + } + + /** + * {@inheritdoc} + */ + public function get(CacheKey $key) + { + return $this->cache->fetch($this->name . self::ENTRY_KEY . $key->hash) ?: null; + } + + /** + * {@inheritdoc} + */ + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) + { + return $this->cache->save($this->name . self::ENTRY_KEY . $key->hash, $entry, $this->lifetime); + } + + /** + * {@inheritdoc} + */ + public function evict(CacheKey $key) + { + return $this->cache->delete($this->name . self::ENTRY_KEY . $key->hash); + } + + /** + * {@inheritdoc} + */ + public function evictAll() + { + return $this->cache->deleteAll(); + } +} diff --git a/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php new file mode 100644 index 00000000000..680f1940be6 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php @@ -0,0 +1,245 @@ +. + */ + +namespace Doctrine\ORM\Cache\Region; + +use Doctrine\ORM\Cache\Lock; +use Doctrine\ORM\Cache\Region; +use Doctrine\ORM\Cache\CacheKey; +use Doctrine\ORM\Cache\CacheEntry; +use Doctrine\ORM\Cache\ConcurrentRegion; + +/** + * Very naive concurrent region, based on file locks. + * + * since 2.5 + * author Fabio B. Silva + */ +class FileLockRegion implements ConcurrentRegion +{ + const LOCK_EXTENSION = 'lock'; + + /** + * var \Doctrine\ORM\Cache\Region + */ + private $region; + + /** + * var string + */ + private $directory; + + /** + * var integer + */ + private $lockLifetime; + + /** + * @param \Doctrine\ORM\Cache\Region $region + * @param string $directory + * @param string $lockLifetime + * + * @throws \InvalidArgumentException + */ + public function __construct(Region $region, $directory, $lockLifetime) + { + if ( ! is_dir($directory) && ! @mkdir($directory, 0777, true)) { + throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $directory)); + } + + if ( ! is_writable($directory)) { + throw new \InvalidArgumentException(sprintf('The directory "%s" is not writable.', $directory)); + } + + $this->region = $region; + $this->directory = $directory; + $this->lockLifetime = $lockLifetime; + } + + /** + * param \Doctrine\ORM\Cache\CacheKey $key + * param \Doctrine\ORM\Cache\Lock $lock + * + * return boolean + */ + private function isLoked(CacheKey $key, Lock $lock = null) + { + $filename = $this->getLockFileName($key); + + if ( ! is_file($filename)) { + return false; + } + + $time = $this->getLockTime($filename); + $content = $this->getLockContent($filename); + + if ( ! $content || ! $time) { + @unlink($filename); + + return false; + } + + if ($lock && $content === $lock->value) { + return false; + } + + // outdated lock + if (($time + $this->lockLifetime) <= time()) { + @unlink($filename); + + return false; + } + + return true; + } + + /** + * @param \Doctrine\ORM\Cache\CacheKey $key + * + * return string + */ + private function getLockFileName(CacheKey $key) + { + return $this->directory . DIRECTORY_SEPARATOR . $key->hash . '.' . self::LOCK_EXTENSION; + } + + /** + * @param string $filename + * + * return string + */ + private function getLockContent($filename) + { + return @file_get_contents($filename); + } + + /** + * @param string $filename + * + * return integer + */ + private function getLockTime($filename) + { + return @fileatime($filename); + } + + /** + * {inheritdoc} + */ + public function getName() + { + return $this->region->getName(); + } + + /** + * {inheritdoc} + */ + public function contains(CacheKey $key) + { + if ($this->isLoked($key)) { + return false; + } + + return $this->region->contains($key); + } + + /** + * {inheritdoc} + */ + public function get(CacheKey $key) + { + if ($this->isLoked($key)) { + return null; + } + + return $this->region->get($key); + } + + /** + * {inheritdoc} + */ + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) + { + if ($this->isLoked($key, $lock)) { + return false; + } + + return $this->region->put($key, $entry); + } + + /** + * {inheritdoc} + */ + public function evict(CacheKey $key) + { + if ($this->isLoked($key)) { + @unlink($this->getLockFileName($key)); + } + + return $this->region->evict($key); + } + + /** + * {inheritdoc} + */ + public function evictAll() + { + foreach (glob(sprintf("%s/*.%s" , $this->directory, self::LOCK_EXTENSION)) as $filename) { + @unlink($filename); + } + + return $this->region->evictAll(); + } + + /** + * {inheritdoc} + */ + public function lock(CacheKey $key) + { + if ($this->isLoked($key)) { + return null; + } + + $lock = Lock::createLockRead(); + $filename = $this->getLockFileName($key); + + if ( ! @file_put_contents($filename, $lock->value, LOCK_EX)) { + return null; + } + + return $lock; + } + + /** + * {inheritdoc} + */ + public function unlock(CacheKey $key, Lock $lock) + { + if ($this->isLoked($key, $lock)) { + return false; + } + + if ( ! @unlink($this->getLockFileName($key))) { + return false; + } + + return true; + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampQueryCacheValidator.php b/lib/Doctrine/ORM/Cache/TimestampQueryCacheValidator.php new file mode 100644 index 00000000000..c213beefef8 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampQueryCacheValidator.php @@ -0,0 +1,43 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache\QueryCacheEntry; +use Doctrine\ORM\Cache\QueryCacheKey; + +/** + * @since 2.5 + * @author Fabio B. Silva + */ +class TimestampQueryCacheValidator implements QueryCacheValidator +{ + /** + * {@inheritdoc} + */ + public function isValid(QueryCacheKey $key, QueryCacheEntry $entry) + { + if ($key->lifetime == 0) { + return true; + } + + return ($entry->time + $key->lifetime) > time(); + } +} diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index 7ab147f9847..6496957a3b6 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -35,6 +35,11 @@ use Doctrine\ORM\Mapping\QuoteStrategy; use Doctrine\ORM\Repository\DefaultRepositoryFactory; use Doctrine\ORM\Repository\RepositoryFactory; +use Doctrine\ORM\Cache\CacheFactory; +use Doctrine\ORM\Cache\Logging\CacheLogger; +use Doctrine\ORM\Cache\QueryCacheValidator; +use Doctrine\ORM\Cache\TimestampQueryCacheValidator; +use Doctrine\ORM\Cache\Persisters\CachedPersisterFactory; /** * Configuration container for all configuration options of Doctrine. @@ -233,6 +238,169 @@ public function getMetadataDriverImpl() : null; } + /** + * @return boolean + */ + public function isSecondLevelCacheEnabled() + { + return isset($this->_attributes['isSecondLevelCacheEnabled']) + ? $this->_attributes['isSecondLevelCacheEnabled'] + : false; + } + + /** + * @param boolean $flag + * + * @return void + */ + public function setSecondLevelCacheEnabled($flag = true) + { + $this->_attributes['isSecondLevelCacheEnabled'] = (boolean) $flag; + } + + /** + * @return \Doctrine\ORM\Cache\CacheFactory|null + */ + public function getSecondLevelCacheFactory() + { + return isset($this->_attributes['secondLevelCacheFactory']) + ? $this->_attributes['secondLevelCacheFactory'] + : null; + } + + /** + * @param \Doctrine\ORM\Cache\CacheFactory $factory + * + * @return void + */ + public function setSecondLevelCacheFactory(CacheFactory $factory) + { + $this->_attributes['secondLevelCacheFactory'] = $factory; + } + + /** + * @param string $name + * + * @return integer + */ + public function getSecondLevelCacheRegionLifetime($name) + { + if (isset($this->_attributes['secondLevelCacheRegionLifetime'][$name])) { + return $this->_attributes['secondLevelCacheRegionLifetime'][$name]; + } + + return $this->getSecondLevelCacheDefaultRegionLifetime(); + } + + /** + * @param string $name + * @param integer $lifetime + */ + public function setSecondLevelCacheRegionLifetime($name, $lifetime) + { + $this->_attributes['secondLevelCacheRegionLifetime'][$name] = (integer) $lifetime; + } + + /** + * @return integer + */ + public function getSecondLevelCacheDefaultRegionLifetime() + { + return isset($this->_attributes['secondLevelCacheDefaultRegionLifetime']) + ? $this->_attributes['secondLevelCacheDefaultRegionLifetime'] + : 0; + } + + /** + * @param integer $lifetime + */ + public function setSecondLevelCacheDefaultRegionLifetime($lifetime) + { + $this->_attributes['secondLevelCacheDefaultRegionLifetime'] = (integer) $lifetime; + } + + /** + * @param integer $lifetime + */ + public function setSecondLevelCacheLockLifetime($lifetime) + { + $this->_attributes['secondLevelCacheLockLifetime'] = (integer) $lifetime; + } + + /** + * @return integer + */ + public function getSecondLevelCacheLockLifetime() + { + return isset($this->_attributes['secondLevelCacheLockLifetime']) + ? $this->_attributes['secondLevelCacheLockLifetime'] + : 60; + } + + /** + * @return \Doctrine\ORM\Cache\Logging\CacheLogger|null + */ + public function getSecondLevelCacheLogger() + { + return isset($this->_attributes['secondLevelCacheLogger']) + ? $this->_attributes['secondLevelCacheLogger'] + : null; + } + + /** + * @param \Doctrine\ORM\Cache\Logging\CacheLogger $logger + */ + public function setSecondLevelCacheLogger(CacheLogger $logger) + { + $this->_attributes['secondLevelCacheLogger'] = $logger; + } + + /** + * @return \Doctrine\ORM\Cache\QueryCacheValidator + */ + public function getSecondLevelCacheQueryValidator() + { + return isset($this->_attributes['secondLevelCacheQueryValidator']) + ? $this->_attributes['secondLevelCacheQueryValidator'] + : $this->_attributes['secondLevelCacheQueryValidator'] = new TimestampQueryCacheValidator(); + } + + /** + * @param \Doctrine\ORM\Cache\QueryCacheValidator $validator + */ + public function setSecondLevelQueryValidator(QueryCacheValidator $validator) + { + $this->_attributes['secondLevelCacheQueryValidator'] = $validator; + } + + /** + * Set the persister factory. + * + * @since 2.5 + * + * @param \Doctrine\ORM\Persister\PersisterFactory $persisterFactory + */ + public function setSecondLevelCachePersisterFactory($persisterFactory) + { + $this->_attributes['persisterFactory'] = $persisterFactory; + } + + /** + * Get the persister factory. + * + * @since 2.5 + * + * @return \Doctrine\ORM\Persister\PersisterFactory + */ + public function getSecondLevelCachePersisterFactory() + { + if ( ! isset($this->_attributes['cachedPersisterFactory'])){ + $this->_attributes['cachedPersisterFactory'] = new CachedPersisterFactory(); + } + + return $this->_attributes['cachedPersisterFactory']; + } + /** * Gets the cache driver implementation that is used for the query cache (SQL cache). * @@ -696,6 +864,38 @@ public function getDefaultRepositoryClassName() : 'Doctrine\ORM\EntityRepository'; } + /** + * @since 2.5 + * + * @param string $className + * + * @return void + * + * @throws ORMException If not is a \Doctrine\ORM\Cache + */ + public function setSecondLevelCacheClassName($className) + { + $reflectionClass = new \ReflectionClass($className); + + if ( ! $reflectionClass->implementsInterface('Doctrine\ORM\Cache')) { + throw ORMException::invalidSecondLevelCache($className); + } + + $this->_attributes['secondLevelCacheClassName'] = $className; + } + + /** + * @since 2.5 + * + * @return string A \Doctrine\ORM\Cache implementation + */ + public function getSecondLevelCacheClassName() + { + return isset($this->_attributes['secondLevelCacheClassName']) + ? $this->_attributes['secondLevelCacheClassName'] + : 'Doctrine\ORM\Cache\DefaultCache'; + } + /** * Sets naming strategy. * diff --git a/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php b/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php index dc123118f52..1a768f95330 100644 --- a/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php +++ b/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php @@ -268,4 +268,12 @@ public function hasFilters() { return $this->wrapped->hasFilters(); } + + /** + * {@inheritdoc} + */ + public function getCache() + { + return $this->wrapped->getCache(); + } } diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index d21d3fb86d3..d759f70b2f5 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -134,6 +134,11 @@ */ private $filterCollection; + /** + * @var \Doctrine\ORM\Cache The second level cache regions API. + */ + private $cache; + /** * Creates a new EntityManager that operates on the given database connection * and uses the given Configuration and EventManager implementations. @@ -162,6 +167,11 @@ protected function __construct(Connection $conn, Configuration $config, EventMan $config->getProxyNamespace(), $config->getAutoGenerateProxyClasses() ); + + if ($config->isSecondLevelCacheEnabled()) { + $cacheClass = $config->getSecondLevelCacheClassName(); + $this->cache = new $cacheClass($this); + } } /** @@ -217,6 +227,14 @@ public function beginTransaction() $this->conn->beginTransaction(); } + /** + * @return \Doctrine\ORM\Cache + */ + public function getCache() + { + return $this->cache; + } + /** * Executes a function in a transaction. * @@ -457,7 +475,7 @@ public function find($entityName, $id, $lockMode = LockMode::NONE, $lockVersion switch ($lockMode) { case LockMode::NONE: - return $persister->load($sortedId); + return $persister->loadById($sortedId); case LockMode::OPTIMISTIC: if ( ! $class->isVersioned) { diff --git a/lib/Doctrine/ORM/EntityManagerInterface.php b/lib/Doctrine/ORM/EntityManagerInterface.php index d72f7cd0cf8..9599f1c5b36 100644 --- a/lib/Doctrine/ORM/EntityManagerInterface.php +++ b/lib/Doctrine/ORM/EntityManagerInterface.php @@ -31,6 +31,7 @@ */ interface EntityManagerInterface extends ObjectManager { + public function getCache(); public function getConnection(); public function getExpressionBuilder(); public function beginTransaction(); diff --git a/lib/Doctrine/ORM/Mapping/Cache.php b/lib/Doctrine/ORM/Mapping/Cache.php new file mode 100644 index 00000000000..560a9c4eb34 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/Cache.php @@ -0,0 +1,44 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +/** + * Caching to an entity or a collection. + * + * @author Fabio B. Silva + * @since 2.5 + * + * @Annotation + * @Target("CLASS") + */ +final class Cache implements Annotation +{ + /** + * @Enum({"READ_ONLY", "NONSTRICT_READ_WRITE", "READ_WRITE"}) + * + * @var string The concurrency strategy. + */ + public $usage = 'READ_ONLY'; + + /** + * @var string Cache region name. + */ + public $region; +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 9320d1b6d50..62634a1fd59 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -144,6 +144,10 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS $class->setPrimaryTable($parent->table); } + if ($parent && $parent->cache) { + $class->cache = $parent->cache; + } + if ($parent && $parent->containsForeignIdentifier) { $class->containsForeignIdentifier = true; } diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 9b3b294925d..d0be5f2ed9e 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -26,7 +26,6 @@ use ReflectionClass; use Doctrine\Common\Persistence\Mapping\ClassMetadata; use Doctrine\Common\ClassLoader; -use Doctrine\Common\EventArgs; /** * A ClassMetadata instance holds all the object-relational mapping metadata @@ -189,6 +188,21 @@ class ClassMetadataInfo implements ClassMetadata */ const TO_MANY = 12; + /** + * ReadOnly cache can do reads, inserts and deletes, cannot perform updates or employ any locks, + */ + const CACHE_USAGE_READ_ONLY = 1; + + /** + * Nonstrict Read Write Cache doesn’t employ any locks but can do inserts, update and deletes. + */ + const CACHE_USAGE_NONSTRICT_READ_WRITE = 2; + + /** + * Read Write Attempts to lock the entity before update/delete. + */ + const CACHE_USAGE_READ_WRITE = 3; + /** * READ-ONLY: The name of the entity class. * @@ -577,6 +591,11 @@ class ClassMetadataInfo implements ClassMetadata */ public $versionField; + /** + * @var array + */ + public $cache; + /** * The ReflectionClass instance of the mapped class. * @@ -855,6 +874,10 @@ public function __sleep() $serialized[] = "customGeneratorDefinition"; } + if ($this->cache) { + $serialized[] = "cache"; + } + return $serialized; } @@ -979,6 +1002,44 @@ public function getReflectionClass() return $this->reflClass; } + /** + * @param array $cache + * + * @return void + */ + public function enableCache(array $cache) + { + if ( ! isset($cache['usage'])) { + $cache['usage'] = self::CACHE_USAGE_READ_ONLY; + } + + if ( ! isset($cache['region'])) { + $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)); + } + + $this->cache = $cache; + } + + /** + * @param array $cache + * + * @return void + */ + public function enableAssociationCache($fieldName, array $cache) + { + if ( ! isset($cache['usage'])) { + $cache['usage'] = isset($this->cache['usage']) + ? $this->cache['usage'] + : self::CACHE_USAGE_READ_ONLY; + } + + if ( ! isset($cache['region'])) { + $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)) . '__' . $fieldName; + } + + $this->associationMappings[$fieldName]['cache'] = $cache; + } + /** * Sets the change tracking policy used by this class. * diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index f9aaddba709..359f2632272 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -128,6 +128,16 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) $metadata->setPrimaryTable($primaryTable); } + // Evaluate @Cache annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\Cache'])) { + $cacheAnnot = $classAnnotations['Doctrine\ORM\Mapping\Cache']; + + $metadata->enableCache(array( + 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage), + 'region' => $cacheAnnot->region, + )); + } + // Evaluate NamedNativeQueries annotation if (isset($classAnnotations['Doctrine\ORM\Mapping\NamedNativeQueries'])) { $namedNativeQueriesAnnot = $classAnnotations['Doctrine\ORM\Mapping\NamedNativeQueries']; @@ -365,6 +375,14 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) $metadata->mapManyToMany($mapping); } + + // Evaluate @Cache annotation + if (($cacheAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Cache')) !== null) { + $metadata->enableAssociationCache($mapping['fieldName'], array( + 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage), + 'region' => $cacheAnnot->region, + )); + } } // Evaluate AssociationOverrides annotation diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index 14abadb9e4d..611087a1c09 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -64,4 +64,5 @@ require_once __DIR__.'/../AssociationOverrides.php'; require_once __DIR__.'/../AttributeOverride.php'; require_once __DIR__.'/../AttributeOverrides.php'; -require_once __DIR__.'/../EntityListeners.php'; \ No newline at end of file +require_once __DIR__.'/../EntityListeners.php'; +require_once __DIR__.'/../Cache.php'; diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index ed4386ac4fa..dbdaf6e6487 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -81,6 +81,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) $metadata->setPrimaryTable($table); + // Evaluate second level cache + if (isset($xmlRoot->{'cache'})) { + $metadata->enableCache($this->cacheToArray($xmlRoot->{'cache'})); + } + // Evaluate named queries if (isset($xmlRoot->{'named-queries'})) { foreach ($xmlRoot->{'named-queries'}->{'named-query'} as $namedQueryElement) { @@ -349,6 +354,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) } $metadata->mapOneToOne($mapping); + + // Evaluate second level cache + if (isset($oneToOneElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement->{'cache'})); + } } } @@ -388,6 +398,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) } $metadata->mapOneToMany($mapping); + + // Evaluate second level cache + if (isset($oneToManyElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement->{'cache'})); + } } } @@ -428,6 +443,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) } $metadata->mapManyToOne($mapping); + + // Evaluate second level cache + if (isset($manyToOneElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement->{'cache'})); + } } } @@ -493,6 +513,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) } $metadata->mapManyToMany($mapping); + + // Evaluate second level cache + if (isset($manyToManyElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement->{'cache'})); + } } } @@ -701,6 +726,22 @@ private function columnToArray(SimpleXMLElement $fieldMapping) return $mapping; } + /** + * @param SimpleXMLElement $cacheMapping + * + * @return array + */ + private function cacheToArray(SimpleXMLElement $cacheMapping) + { + $region = isset($cacheMapping['region']) ? (string)$cacheMapping['region'] : null; + $usage = isset($cacheMapping['usage']) ? constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . strtoupper($cacheMapping['usage'])) : null; + + return array( + 'usage' => $usage, + 'region' => $region, + ); + } + /** * Gathers a list of cascade options found in the given cascade element. * diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 1e7aa33560d..80b2d6a2abf 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -72,9 +72,16 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) // Evaluate root level properties $table = array(); + if (isset($element['table'])) { $table['name'] = $element['table']; } + + // Evaluate second level cache + if (isset($element['cache'])) { + $metadata->enableCache($this->cacheToArray($element['cache'])); + } + $metadata->setPrimaryTable($table); // Evaluate named queries @@ -361,6 +368,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) } $metadata->mapOneToOne($mapping); + + // Evaluate second level cache + if (isset($oneToOneElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement['cache'])); + } } } @@ -394,6 +406,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) } $metadata->mapOneToMany($mapping); + + // Evaluate second level cache + if (isset($oneToManyElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement['cache'])); + } } } @@ -438,6 +455,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) } $metadata->mapManyToOne($mapping); + + // Evaluate second level cache + if (isset($manyToOneElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement['cache'])); + } } } @@ -506,6 +528,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) } $metadata->mapManyToMany($mapping); + + // Evaluate second level cache + if (isset($manyToManyElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement['cache'])); + } } } @@ -704,6 +731,22 @@ private function columnToArray($fieldName, $column) return $mapping; } + /** + * @param array $cacheMapping + * + * @return array + */ + private function cacheToArray($cacheMapping) + { + $region = isset($cacheMapping['region']) ? (string)$cacheMapping['region'] : null; + $usage = isset($cacheMapping['usage']) ? constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . strtoupper($cacheMapping['usage'])) : null; + + return array( + 'usage' => $usage, + 'region' => $region, + ); + } + /** * {@inheritDoc} */ diff --git a/lib/Doctrine/ORM/ORMException.php b/lib/Doctrine/ORM/ORMException.php index 99333f034dc..8349c4f6da1 100644 --- a/lib/Doctrine/ORM/ORMException.php +++ b/lib/Doctrine/ORM/ORMException.php @@ -100,6 +100,20 @@ public static function unrecognizedField($field) return new self("Unrecognized field: $field"); } + /** + * + * @param string $class + * @param string $association + * @param string $given + * @param string $expected + * + * @return \Doctrine\ORM\ORMInvalidArgumentException + */ + static public function unexpectedAssociationValue($class, $association, $given, $expected) + { + return new self(sprintf('Found entity of type %s on association %s#%s, but expecting %s', $given, $class, $association, $expected)); + } + /** * @param string $className * @param string $field @@ -248,6 +262,16 @@ public static function invalidEntityRepository($className) return new self("Invalid repository class '".$className."'. It must be a Doctrine\Common\Persistence\ObjectRepository."); } + /** + * @param string $className + * + * @return ORMException + */ + public static function invalidSecondLevelCache($className) + { + return new self(sprintf('Invalid cache class "%s". It must be a Doctrine\ORM\Cache.', $className)); + } + /** * @param string $className * @param string $fieldName diff --git a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php index fdc54aee125..61222318f76 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php @@ -28,7 +28,7 @@ * @since 2.0 * @author Roman Borschel */ -abstract class AbstractCollectionPersister +abstract class AbstractCollectionPersister implements CollectionPersister { /** * @var EntityManager @@ -74,11 +74,7 @@ public function __construct(EntityManager $em) } /** - * Deletes the persistent state represented by the given collection. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return void + * {@inheritdoc} */ public function delete(PersistentCollection $coll) { @@ -88,9 +84,7 @@ public function delete(PersistentCollection $coll) return; // ignore inverse side } - $sql = $this->getDeleteSQL($coll); - - $this->conn->executeUpdate($sql, $this->getDeleteSQLParameters($coll)); + $this->conn->executeUpdate($this->getDeleteSQL($coll), $this->getDeleteSQLParameters($coll)); } /** @@ -113,12 +107,7 @@ abstract protected function getDeleteSQL(PersistentCollection $coll); abstract protected function getDeleteSQLParameters(PersistentCollection $coll); /** - * Updates the given collection, synchronizing its state with the database - * by inserting, updating and deleting individual elements. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return void + * {@inheritdoc} */ public function update(PersistentCollection $coll) { @@ -133,11 +122,7 @@ public function update(PersistentCollection $coll) } /** - * Deletes rows. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return void + * {@inheritdoc} */ public function deleteRows(PersistentCollection $coll) { @@ -150,11 +135,7 @@ public function deleteRows(PersistentCollection $coll) } /** - * Inserts rows. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return void + * {@inheritdoc} */ public function insertRows(PersistentCollection $coll) { @@ -167,13 +148,7 @@ public function insertRows(PersistentCollection $coll) } /** - * Counts the size of this persistent collection. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return integer - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function count(PersistentCollection $coll) { @@ -181,15 +156,7 @@ public function count(PersistentCollection $coll) } /** - * Slices elements. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param integer $offset - * @param integer $length - * - * @return array - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function slice(PersistentCollection $coll, $offset, $length = null) { @@ -197,14 +164,7 @@ public function slice(PersistentCollection $coll, $offset, $length = null) } /** - * Checks for existence of an element. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return boolean - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function contains(PersistentCollection $coll, $element) { @@ -212,14 +172,7 @@ public function contains(PersistentCollection $coll, $element) } /** - * Checks for existence of a key. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param mixed $key - * - * @return boolean - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function containsKey(PersistentCollection $coll, $key) { @@ -227,14 +180,7 @@ public function containsKey(PersistentCollection $coll, $key) } /** - * Removes an element. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return mixed - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function removeElement(PersistentCollection $coll, $element) { @@ -242,14 +188,7 @@ public function removeElement(PersistentCollection $coll, $element) } /** - * Removes an element by key. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param mixed $key - * - * @return void - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function removeKey(PersistentCollection $coll, $key) { @@ -257,14 +196,7 @@ public function removeKey(PersistentCollection $coll, $key) } /** - * Gets an element by key. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param mixed $index - * - * @return mixed - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function get(PersistentCollection $coll, $index) { diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index c594f3bbf64..9a85db6a753 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -78,7 +78,7 @@ * @author Fabio B. Silva * @since 2.0 */ -class BasicEntityPersister +class BasicEntityPersister implements EntityPersister { /** * @var array @@ -223,7 +223,7 @@ public function __construct(EntityManager $em, ClassMetadata $class) } /** - * @return \Doctrine\ORM\Mapping\ClassMetadata + * {@inheritdoc} */ public function getClassMetadata() { @@ -231,12 +231,15 @@ public function getClassMetadata() } /** - * Adds an entity to the queued insertions. - * The entity remains queued until {@link executeInserts} is invoked. - * - * @param object $entity The entity to queue for insertion. - * - * @return void + * {@inheritdoc} + */ + public function getResultSetMapping() + { + return $this->rsm; + } + + /** + * {@inheritdoc} */ public function addInsert($entity) { @@ -244,13 +247,15 @@ public function addInsert($entity) } /** - * Executes all queued entity insertions and returns any generated post-insert - * identifiers that were created as a result of the insertions. - * - * If no inserts are queued, invoking this method is a NOOP. - * - * @return array An array of any generated post-insert IDs. This will be an empty array - * if the entity class does not use the IDENTITY generation strategy. + * {@inheritdoc} + */ + public function getInserts() + { + return $this->queuedInserts; + } + + /** + * {@inheritdoc} */ public function executeInserts() { @@ -339,20 +344,7 @@ protected function fetchVersionValue($versionedClass, $id) } /** - * Updates a managed entity. The entity is updated according to its current changeset - * in the running UnitOfWork. If there is no changeset, nothing is updated. - * - * The data to update is retrieved through {@link prepareUpdateData}. - * Subclasses that override this method are supposed to obtain the update data - * in the same way, through {@link prepareUpdateData}. - * - * Subclasses are also supposed to take care of versioning when overriding this method, - * if necessary. The {@link updateTable} method can be used to apply the data retrieved - * from {@prepareUpdateData} on the target tables, thereby optionally applying versioning. - * - * @param object $entity The entity to update. - * - * @return void + * {@inheritdoc} */ public function update($entity) { @@ -549,16 +541,7 @@ protected function deleteJoinTableRecords($identifier) } /** - * Deletes a managed entity. - * - * The entity to delete must be managed and have a persistent identifier. - * The deletion happens instantaneously. - * - * Subclasses may override this method to customize the semantics of entity deletion. - * - * @param object $entity The entity to delete. - * - * @return void + * {@inheritdoc} */ public function delete($entity) { @@ -713,15 +696,7 @@ protected function prepareInsertData($entity) } /** - * Gets the name of the table that owns the column the given field is mapped to. - * - * The default implementation in BasicEntityPersister always returns the name - * of the table the entity type of this persister is mapped to, since an entity - * is always persisted to a single table with a BasicEntityPersister. - * - * @param string $fieldName The field name. - * - * @return string The table name. + * {@inheritdoc} */ public function getOwningTable($fieldName) { @@ -729,19 +704,7 @@ public function getOwningTable($fieldName) } /** - * Loads an entity by a list of field criteria. - * - * @param array $criteria The criteria by which to load the entity. - * @param object|null $entity The entity to load the data into. If not specified, a new entity is created. - * @param array|null $assoc The association that connects the entity to load to another entity, if any. - * @param array $hints Hints for entity creation. - * @param int $lockMode - * @param int|null $limit Limit number of results. - * @param array|null $orderBy Criteria to order by. - * - * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. - * - * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? + * {@inheritdoc} */ public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null, array $orderBy = null) { @@ -761,18 +724,15 @@ public function load(array $criteria, $entity = null, $assoc = null, array $hint } /** - * Loads an entity of this persister's mapped class as part of a single-valued - * association from another entity. - * - * @param array $assoc The association to load. - * @param object $sourceEntity The entity that owns the association (not necessarily the "owning side"). - * @param array $identifier The identifier of the entity to load. Must be provided if - * the association to load represents the owning side, otherwise - * the identifier is derived from the $sourceEntity. - * - * @return object The loaded and managed entity instance or NULL if the entity can not be found. - * - * @throws \Doctrine\ORM\Mapping\MappingException + * {@inheritdoc} + */ + public function loadById(array $identifier, $entity = null) + { + return $this->load($identifier, $entity); + } + + /** + * {@inheritdoc} */ public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = array()) { @@ -838,14 +798,7 @@ public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifie } /** - * Refreshes a managed entity. - * - * @param array $id The identifier of the entity as an associative array from - * column or field names to values. - * @param object $entity The entity to refresh. - * @param int $lockMode - * - * @return void + * {@inheritdoc} */ public function refresh(array $id, $entity, $lockMode = 0) { @@ -858,11 +811,7 @@ public function refresh(array $id, $entity, $lockMode = 0) } /** - * Loads Entities matching the given Criteria object. - * - * @param \Doctrine\Common\Collections\Criteria $criteria - * - * @return array + * {@inheritdoc} */ public function loadCriteria(Criteria $criteria) { @@ -916,14 +865,7 @@ private function expandCriteriaParameters(Criteria $criteria) } /** - * Loads a list of entities by a list of field criteria. - * - * @param array $criteria - * @param array|null $orderBy - * @param int|null $limit - * @param int|null $offset - * - * @return array + * {@inheritdoc} */ public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null) { @@ -937,14 +879,7 @@ public function loadAll(array $criteria = array(), array $orderBy = null, $limit } /** - * Gets (sliced or full) elements of the given collection. - * - * @param array $assoc - * @param object $sourceEntity - * @param int|null $offset - * @param int|null $limit - * - * @return array + * {@inheritdoc} */ public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) { @@ -1000,13 +935,7 @@ private function loadCollectionFromStatement($assoc, $stmt, $coll) } /** - * Loads a collection of entities of a many-to-many association. - * - * @param array $assoc The association mapping of the association being loaded. - * @param object $sourceEntity The entity that owns the collection. - * @param PersistentCollection $coll The collection to fill. - * - * @return array + * {@inheritdoc} */ public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) { @@ -1083,18 +1012,9 @@ private function getManyToManyStatement(array $assoc, $sourceEntity, $offset = n } /** - * Gets the SELECT SQL to select one or more entities by a set of field criteria. - * - * @param array|\Doctrine\Common\Collections\Criteria $criteria - * @param array|null $assoc - * @param int $lockMode - * @param int|null $limit - * @param int|null $offset - * @param array|null $orderBy - * - * @return string + * {@inheritdoc} */ - protected function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) + public function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) { $lockSql = ''; $joinSql = ''; @@ -1391,11 +1311,9 @@ protected function getSelectManyToManyJoinSQL(array $manyToMany) } /** - * Gets the INSERT SQL used by the persister to persist a new entity. - * - * @return string + * {@inheritdoc} */ - protected function getInsertSQL() + public function getInsertSQL() { if ($this->insertSql !== null) { return $this->insertSql; @@ -1529,12 +1447,7 @@ protected function getSQLTableAlias($className, $assocName = '') } /** - * Locks all rows of this entity matching the given criteria with the specified pessimistic lock mode. - * - * @param array $criteria - * @param int $lockMode - * - * @return void + * {@inheritdoc} */ public function lock(array $criteria, $lockMode) { @@ -1597,14 +1510,7 @@ protected function getSelectConditionCriteriaSQL(Criteria $criteria) } /** - * Gets the SQL WHERE condition for matching a field with a given value. - * - * @param string $field - * @param mixed $value - * @param array|null $assoc - * @param string|null $comparison - * - * @return string + * {@inheritdoc} */ public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null) { @@ -1707,14 +1613,7 @@ protected function getSelectConditionSQL(array $criteria, $assoc = null) } /** - * Returns an array with (sliced or full list) of elements in the specified collection. - * - * @param array $assoc - * @param object $sourceEntity - * @param int|null $offset - * @param int|null $limit - * - * @return array + * {@inheritdoc} */ public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) { @@ -1724,13 +1623,7 @@ public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = nu } /** - * Loads a collection of entities in a one-to-many association. - * - * @param array $assoc - * @param object $sourceEntity - * @param PersistentCollection $coll The collection to load/fill. - * - * @return array + * {@inheritdoc} */ public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) { @@ -1782,13 +1675,9 @@ private function getOneToManyStatement(array $assoc, $sourceEntity, $offset = nu } /** - * Expands the parameters from the given criteria and use the correct binding types if found. - * - * @param array $criteria - * - * @return array + * {@inheritdoc} */ - private function expandParameters($criteria) + public function expandParameters($criteria) { $params = array(); $types = array(); @@ -1890,12 +1779,7 @@ private function getIndividualValue($value) } /** - * Checks whether the given managed entity exists in the database. - * - * @param object $entity - * @param array $extraConditions - * - * @return boolean TRUE if the entity exists in the database, FALSE otherwise. + * {@inheritdoc} */ public function exists($entity, array $extraConditions = array()) { @@ -1944,11 +1828,7 @@ protected function getJoinSQLForJoinColumns($joinColumns) } /** - * Gets an SQL column alias for a column name. - * - * @param string $columnName - * - * @return string + * {@inheritdoc} */ public function getSQLColumnAlias($columnName) { @@ -1976,4 +1856,4 @@ protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targ $sql = implode(' AND ', $filterClauses); return $sql ? "(" . $sql . ")" : ""; // Wrap again to avoid "X or Y and FilterConditionSQL" } -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/Persisters/CollectionPersister.php b/lib/Doctrine/ORM/Persisters/CollectionPersister.php new file mode 100644 index 00000000000..f8d1944420b --- /dev/null +++ b/lib/Doctrine/ORM/Persisters/CollectionPersister.php @@ -0,0 +1,141 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\ORM\PersistentCollection; + +/** + * Collection persister interface + * Define the behavior that should be implemented by all collection persisters. + * + * @author Fabio B. Silva + * @since 2.5 + */ +interface CollectionPersister +{ + /** + * Deletes the persistent state represented by the given collection. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * + * @return void + */ + public function delete(PersistentCollection $collection); + + /** + * Updates the given collection, synchronizing its state with the database + * by inserting, updating and deleting individual elements. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * + * @return void + */ + public function update(PersistentCollection $collection); + + /** + * Deletes rows. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * + * @return void + */ + public function deleteRows(PersistentCollection $collection); + + /** + * Inserts rows. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * + * @return void + */ + public function insertRows(PersistentCollection $collection); + + /** + * Counts the size of this persistent collection. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * + * @return integer + * + * @throws \BadMethodCallException + */ + public function count(PersistentCollection $collection); + + /** + * Slices elements. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param integer $offset + * @param integer $length + * + * @return array + */ + public function slice(PersistentCollection $collection, $offset, $length = null); + + /** + * Checks for existence of an element. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param object $element + * + * @return boolean + */ + public function contains(PersistentCollection $collection, $element); + + /** + * Checks for existence of a key. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param mixed $key + * + * @return boolean + */ + public function containsKey(PersistentCollection $collection, $key); + + /** + * Removes an element. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param object $element + * + * @return mixed + */ + public function removeElement(PersistentCollection $collection, $element); + + /** + * Removes an element by key. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param mixed $key + * + * @return void + */ + public function removeKey(PersistentCollection $collection, $key); + + /** + * Gets an element by key. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param mixed $index + * + * @return mixed + */ + public function get(PersistentCollection $collection, $index); +} diff --git a/lib/Doctrine/ORM/Persisters/EntityPersister.php b/lib/Doctrine/ORM/Persisters/EntityPersister.php new file mode 100644 index 00000000000..a27a59d1452 --- /dev/null +++ b/lib/Doctrine/ORM/Persisters/EntityPersister.php @@ -0,0 +1,300 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\ORM\PersistentCollection; +use Doctrine\Common\Collections\Criteria; + + +/** + * Entity persister interface + * Define the behavior that should be implemented by all entity persisters. + * + * @author Fabio B. Silva + * @since 2.5 + */ +interface EntityPersister +{ + /** + * @return \Doctrine\ORM\Mapping\ClassMetadata + */ + public function getClassMetadata(); + + /** + * Gets the ResultSetMapping used for hydration. + * + * @return \Doctrine\ORM\Query\ResultSetMapping + */ + public function getResultSetMapping(); + + /** + * Get all queued inserts. + * + * @return array + */ + public function getInserts(); + + /** + * @TODO - It should not be here. + * But its necessary since JoinedSubclassPersister#executeInserts invoke the root persister. + * + * Gets the INSERT SQL used by the persister to persist a new entity. + * + * @return string + */ + public function getInsertSQL(); + + /** + * Gets the SELECT SQL to select one or more entities by a set of field criteria. + * + * @param array|\Doctrine\Common\Collections\Criteria $criteria + * @param array|null $assoc + * @param int $lockMode + * @param int|null $limit + * @param int|null $offset + * @param array|null $orderBy + * + * @return string + */ + public function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null); + + /** + * Expands the parameters from the given criteria and use the correct binding types if found. + * + * @param $criteria + * + * @return array + */ + public function expandParameters($criteria); + + /** + * Gets the SQL WHERE condition for matching a field with a given value. + * + * @param string $field + * @param mixed $value + * @param array|null $assoc + * @param string|null $comparison + * + * @return string + */ + public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null); + + /** + * Adds an entity to the queued insertions. + * The entity remains queued until {@link executeInserts} is invoked. + * + * @param object $entity The entity to queue for insertion. + * + * @return void + */ + public function addInsert($entity); + + /** + * Executes all queued entity insertions and returns any generated post-insert + * identifiers that were created as a result of the insertions. + * + * If no inserts are queued, invoking this method is a NOOP. + * + * @return array An array of any generated post-insert IDs. This will be an empty array + * if the entity class does not use the IDENTITY generation strategy. + */ + public function executeInserts(); + + /** + * Updates a managed entity. The entity is updated according to its current changeset + * in the running UnitOfWork. If there is no changeset, nothing is updated. + * + * @param object $entity The entity to update. + * + * @return void + */ + public function update($entity); + + /** + * Deletes a managed entity. + * + * The entity to delete must be managed and have a persistent identifier. + * The deletion happens instantaneously. + * + * Subclasses may override this method to customize the semantics of entity deletion. + * + * @param object $entity The entity to delete. + * + * @return void + */ + public function delete($entity); + + /** + * Gets the name of the table that owns the column the given field is mapped to. + * + * The default implementation in BasicEntityPersister always returns the name + * of the table the entity type of this persister is mapped to, since an entity + * is always persisted to a single table with a BasicEntityPersister. + * + * @param string $fieldName The field name. + * + * @return string The table name. + */ + public function getOwningTable($fieldName); + + /** + * Loads an entity by a list of field criteria. + * + * @param array $criteria The criteria by which to load the entity. + * @param object|null $entity The entity to load the data into. If not specified, a new entity is created. + * @param array|null $assoc The association that connects the entity to load to another entity, if any. + * @param array $hints Hints for entity creation. + * @param int $lockMode + * @param int|null $limit Limit number of results. + * @param array|null $orderBy Criteria to order by. + * + * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. + * + * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? + */ + public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null, array $orderBy = null); + + /** + * Loads an entity by identifier. + * + * @param array $identifier The entity identifier. + * @param object|null $entity The entity to load the data into. If not specified, a new entity is created. + * + * @return object The loaded and managed entity instance or NULL if the entity can not be found. + * + * @todo Check parameters + */ + public function loadById(array $identifier, $entity = null); + + /** + * Loads an entity of this persister's mapped class as part of a single-valued + * association from another entity. + * + * @param array $assoc The association to load. + * @param object $sourceEntity The entity that owns the association (not necessarily the "owning side"). + * @param array $identifier The identifier of the entity to load. Must be provided if + * the association to load represents the owning side, otherwise + * the identifier is derived from the $sourceEntity. + * + * @return object The loaded and managed entity instance or NULL if the entity can not be found. + * + * @throws \Doctrine\ORM\Mapping\MappingException + */ + public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = array()); + + /** + * Refreshes a managed entity. + * + * @param array $id The identifier of the entity as an associative array from + * column or field names to values. + * @param object $entity The entity to refresh. + * @param int $lockMode + * + * @return void + */ + public function refresh(array $id, $entity, $lockMode = 0); + + /** + * Loads Entities matching the given Criteria object. + * + * @param \Doctrine\Common\Collections\Criteria $criteria + * + * @return array + */ + public function loadCriteria(Criteria $criteria); + + /** + * Loads a list of entities by a list of field criteria. + * + * @param array $criteria + * @param array|null $orderBy + * @param int|null $limit + * @param int|null $offset + * + * @return array + */ + public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null); + + /** + * Gets (sliced or full) elements of the given collection. + * + * @param array $assoc + * @param object $sourceEntity + * @param int|null $offset + * @param int|null $limit + * + * @return array + */ + public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null); + + /** + * Loads a collection of entities of a many-to-many association. + * + * @param array $assoc The association mapping of the association being loaded. + * @param object $sourceEntity The entity that owns the collection. + * @param PersistentCollection $collection The collection to fill. + * + * @return array + */ + public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection); + + /** + * Loads a collection of entities in a one-to-many association. + * + * @param array $assoc + * @param object $sourceEntity + * @param PersistentCollection $collection The collection to load/fill. + * + * @return array + */ + public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection); + + /** + * Locks all rows of this entity matching the given criteria with the specified pessimistic lock mode. + * + * @param array $criteria + * @param int $lockMode + * + * @return void + */ + public function lock(array $criteria, $lockMode); + + /** + * Returns an array with (sliced or full list) of elements in the specified collection. + * + * @param array $assoc + * @param object $sourceEntity + * @param int|null $offset + * @param int|null $limit + * + * @return array + */ + public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null); + + /** + * Checks whether the given managed entity exists in the database. + * + * @param object $entity + * @param array $extraConditions + * + * @return boolean TRUE if the entity exists in the database, FALSE otherwise. + */ + public function exists($entity, array $extraConditions = array()); +} diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 684d30571cc..9e2ce474362 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -296,7 +296,7 @@ public function delete($entity) /** * {@inheritdoc} */ - protected function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) + public function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) { $joinSql = ''; $identifierColumn = $this->class->getIdentifierColumnNames(); diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index 1ec6e5e7c1f..14664d3ce32 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -257,11 +257,7 @@ public function count(PersistentCollection $coll) } /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param int $offset - * @param int|null $length - * - * @return array + * {@inheritdoc} */ public function slice(PersistentCollection $coll, $offset, $length = null) { @@ -271,10 +267,7 @@ public function slice(PersistentCollection $coll, $offset, $length = null) } /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return boolean + * {@inheritdoc} */ public function contains(PersistentCollection $coll, $element) { @@ -300,10 +293,7 @@ public function contains(PersistentCollection $coll, $element) } /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return boolean + * {@inheritdoc} */ public function removeElement(PersistentCollection $coll, $element) { diff --git a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php index 2915fd2ed89..6c0c1e78e3a 100644 --- a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php @@ -34,8 +34,6 @@ class OneToManyPersister extends AbstractCollectionPersister { /** * {@inheritdoc} - * - * @override */ public function get(PersistentCollection $coll, $index) { @@ -166,11 +164,7 @@ public function count(PersistentCollection $coll) } /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param int $offset - * @param int|null $length - * - * @return \Doctrine\Common\Collections\ArrayCollection + * {@inheritdoc} */ public function slice(PersistentCollection $coll, $offset, $length = null) { @@ -181,11 +175,8 @@ public function slice(PersistentCollection $coll, $offset, $length = null) return $persister->getOneToManyCollection($mapping, $coll->getOwner(), $offset, $length); } - /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return boolean + /** + * {@inheritdoc} */ public function contains(PersistentCollection $coll, $element) { @@ -215,10 +206,7 @@ public function contains(PersistentCollection $coll, $element) } /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return boolean + * {@inheritdoc} */ public function removeElement(PersistentCollection $coll, $element) { diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index dfac9a3e791..d33a590f076 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -26,7 +26,7 @@ use Doctrine\Common\Proxy\Proxy as BaseProxy; use Doctrine\Common\Proxy\ProxyGenerator; use Doctrine\ORM\ORMInvalidArgumentException; -use Doctrine\ORM\Persisters\BasicEntityPersister; +use Doctrine\ORM\Persisters\EntityPersister; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityNotFoundException; @@ -107,13 +107,13 @@ protected function createProxyDefinition($className) * Creates a closure capable of initializing a proxy * * @param \Doctrine\Common\Persistence\Mapping\ClassMetadata $classMetadata - * @param \Doctrine\ORM\Persisters\BasicEntityPersister $entityPersister + * @param \Doctrine\ORM\Persisters\EntityPersister $entityPersister * * @return \Closure * * @throws \Doctrine\ORM\EntityNotFoundException */ - private function createInitializer(ClassMetadata $classMetadata, BasicEntityPersister $entityPersister) + private function createInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister) { if ($classMetadata->getReflectionClass()->hasMethod('__wakeup')) { return function (BaseProxy $proxy) use ($entityPersister, $classMetadata) { @@ -130,7 +130,7 @@ private function createInitializer(ClassMetadata $classMetadata, BasicEntityPers $properties = $proxy->__getLazyProperties(); foreach ($properties as $propertyName => $property) { - if (!isset($proxy->$propertyName)) { + if ( ! isset($proxy->$propertyName)) { $proxy->$propertyName = $properties[$propertyName]; } } @@ -138,7 +138,7 @@ private function createInitializer(ClassMetadata $classMetadata, BasicEntityPers $proxy->__setInitialized(true); $proxy->__wakeup(); - if (null === $entityPersister->load($classMetadata->getIdentifierValues($proxy), $proxy)) { + if (null === $entityPersister->loadById($classMetadata->getIdentifierValues($proxy), $proxy)) { $proxy->__setInitializer($initializer); $proxy->__setCloner($cloner); $proxy->__setInitialized(false); @@ -169,7 +169,7 @@ private function createInitializer(ClassMetadata $classMetadata, BasicEntityPers $proxy->__setInitialized(true); - if (null === $entityPersister->load($classMetadata->getIdentifierValues($proxy), $proxy)) { + if (null === $entityPersister->loadById($classMetadata->getIdentifierValues($proxy), $proxy)) { $proxy->__setInitializer($initializer); $proxy->__setCloner($cloner); $proxy->__setInitialized(false); @@ -183,13 +183,13 @@ private function createInitializer(ClassMetadata $classMetadata, BasicEntityPers * Creates a closure capable of finalizing state a cloned proxy * * @param \Doctrine\Common\Persistence\Mapping\ClassMetadata $classMetadata - * @param \Doctrine\ORM\Persisters\BasicEntityPersister $entityPersister + * @param \Doctrine\ORM\Persisters\EntityPersister $entityPersister * * @return \Closure * * @throws \Doctrine\ORM\EntityNotFoundException */ - private function createCloner(ClassMetadata $classMetadata, BasicEntityPersister $entityPersister) + private function createCloner(ClassMetadata $classMetadata, EntityPersister $entityPersister) { return function (BaseProxy $proxy) use ($entityPersister, $classMetadata) { if ($proxy->__isInitialized()) { @@ -198,20 +198,21 @@ private function createCloner(ClassMetadata $classMetadata, BasicEntityPersister $proxy->__setInitialized(true); $proxy->__setInitializer(null); - $class = $entityPersister->getClassMetadata(); - $original = $entityPersister->load($classMetadata->getIdentifierValues($proxy)); + + $class = $entityPersister->getClassMetadata(); + $original = $entityPersister->loadById($classMetadata->getIdentifierValues($proxy)); if (null === $original) { throw new EntityNotFoundException(); } - foreach ($class->getReflectionClass()->getProperties() as $reflectionProperty) { - $propertyName = $reflectionProperty->getName(); - - if ($class->hasField($propertyName) || $class->hasAssociation($propertyName)) { - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($proxy, $reflectionProperty->getValue($original)); + foreach ($class->getReflectionClass()->getProperties() as $property) { + if ( ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) { + continue; } + + $property->setAccessible(true); + $property->setValue($proxy, $property->getValue($original)); } }; } diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 71f5f5550ce..de8411df0de 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -19,15 +19,13 @@ namespace Doctrine\ORM; -use Doctrine\Common\Collections\ArrayCollection; - use Doctrine\DBAL\LockMode; - use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\ParserResult; use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\ParameterTypeInferer; +use Doctrine\Common\Collections\ArrayCollection; /** * A Query object represents a DQL query. @@ -61,6 +59,11 @@ final class Query extends AbstractQuery */ const HINT_REFRESH = 'doctrine.refresh'; + /** + * @var string + */ + const HINT_CACHE_ENABLED = 'doctrine.cache.enabled'; + /** * Internal hint: is set to the proxy entity that is currently triggered for loading * @@ -178,16 +181,6 @@ final class Query extends AbstractQuery */ private $_useQueryCache = true; - /** - * Initializes a new Query instance. - * - * @param \Doctrine\ORM\EntityManager $entityManager - */ - /*public function __construct(EntityManager $entityManager) - { - parent::__construct($entityManager); - }*/ - /** * Gets the SQL query/queries that correspond to this DQL query. * @@ -214,6 +207,19 @@ public function getAST() return $parser->getAST(); } + /** + * {@inheritdoc} + */ + public function getResultSetMapping() + { + // parse query or load from cache + if ($this->_resultSetMapping === null) { + $this->_resultSetMapping = $this->_parse()->getResultSetMapping(); + } + + return $this->_resultSetMapping; + } + /** * Parses the DQL query, if necessary, and stores the parser result. * @@ -303,13 +309,14 @@ private function processParameterMappings($paramMappings) foreach ($this->parameters as $parameter) { $key = $parameter->getName(); $value = $parameter->getValue(); + $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); if ( ! isset($paramMappings[$key])) { throw QueryException::unknownParameter($key); } - if (isset($this->_resultSetMapping->metadataParameterMapping[$key]) && $value instanceof ClassMetadata) { - $value = $value->getMetadataValue($this->_resultSetMapping->metadataParameterMapping[$key]); + if (isset($rsm->metadataParameterMapping[$key]) && $value instanceof ClassMetadata) { + $value = $value->getMetadataValue($rsm->metadataParameterMapping[$key]); } $value = $this->processParameterValue($value); @@ -655,6 +662,14 @@ protected function _getQueryCacheId() ); } + /** + * {@inheritdoc} + */ + protected function getHash() + { + return sha1(parent::getHash(). '-'. $this->_firstResult . '-' . $this->_maxResults); + } + /** * Cleanup Query resource when clone is called. * diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index fb61779198f..0a2db86cfe3 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -401,12 +401,13 @@ private function _generateOrderedCollectionOrderByItems() foreach ($this->selectedClasses as $selectedClass) { $dqlAlias = $selectedClass['dqlAlias']; $qComp = $this->queryComponents[$dqlAlias]; - $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name); if ( ! isset($qComp['relation']['orderBy'])) { continue; } + $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name); + foreach ($qComp['relation']['orderBy'] as $fieldName => $orientation) { $columnName = $this->quoteStrategy->getColumnName($fieldName, $qComp['metadata'], $this->platform); $tableName = ($qComp['metadata']->isInheritanceTypeJoined()) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 5de769f6381..3cbe8f05358 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -38,6 +38,13 @@ use Doctrine\ORM\Event\PostFlushEventArgs; use Doctrine\ORM\Event\ListenersInvoker; +use Doctrine\ORM\Cache\Persister\CachedPersister; +use Doctrine\ORM\Persisters\BasicEntityPersister; +use Doctrine\ORM\Persisters\SingleTablePersister; +use Doctrine\ORM\Persisters\JoinedSubclassPersister; +use Doctrine\ORM\Persisters\OneToManyPersister; +use Doctrine\ORM\Persisters\ManyToManyPersister; + /** * The UnitOfWork is responsible for tracking changes to objects during an * "object-level" transaction and for writing out changes to the database @@ -254,6 +261,11 @@ class UnitOfWork implements PropertyChangedListener */ private $eagerLoadingEntities = array(); + /** + * @var boolean + */ + protected $hasCache = false; + /** * Initializes a new UnitOfWork instance, bound to the given EntityManager. * @@ -264,6 +276,7 @@ public function __construct(EntityManager $em) $this->em = $em; $this->evm = $em->getEventManager(); $this->listenersInvoker = new ListenersInvoker($em); + $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled(); } /** @@ -351,6 +364,7 @@ public function commit($entity = null) foreach ($this->collectionDeletions as $collectionToDelete) { $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); } + // Collection updates (deleteRows, updateRows, insertRows) foreach ($this->collectionUpdates as $collectionToUpdate) { $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate); @@ -368,9 +382,13 @@ public function commit($entity = null) $this->em->close(); $conn->rollback(); + $this->afterTransactionRolledBack(); + throw $e; } + $this->afterTransactionComplete(); + // Take new snapshots from visited collections foreach ($this->visitedCollections as $coll) { $coll->takeSnapshot(); @@ -675,17 +693,22 @@ public function computeChangeSet(ClassMetadata $class, $entity) // Look for changes in associations of the entity foreach ($class->associationMappings as $field => $assoc) { - if (($val = $class->reflFields[$field]->getValue($entity)) !== null) { - $this->computeAssociationChanges($assoc, $val); - if (!isset($this->entityChangeSets[$oid]) && - $assoc['isOwningSide'] && - $assoc['type'] == ClassMetadata::MANY_TO_MANY && - $val instanceof PersistentCollection && - $val->isDirty()) { - $this->entityChangeSets[$oid] = array(); - $this->originalEntityData[$oid] = $actualData; - $this->entityUpdates[$oid] = $entity; - } + + if (($val = $class->reflFields[$field]->getValue($entity)) === null) { + continue; + } + + $this->computeAssociationChanges($assoc, $val); + + if ( ! isset($this->entityChangeSets[$oid]) && + $assoc['isOwningSide'] && + $assoc['type'] == ClassMetadata::MANY_TO_MANY && + $val instanceof PersistentCollection && + $val->isDirty()) { + + $this->entityChangeSets[$oid] = array(); + $this->originalEntityData[$oid] = $actualData; + $this->entityUpdates[$oid] = $entity; } } } @@ -746,8 +769,8 @@ public function computeChangeSets() /** * Computes the changes of an association. * - * @param array $assoc - * @param mixed $value The value of the association. + * @param array $assoc The association mapping. + * @param mixed $value The value of the association. * * @throws ORMInvalidArgumentException * @throws ORMException @@ -780,15 +803,7 @@ private function computeAssociationChanges($assoc, $value) $state = $this->getEntityState($entry, self::STATE_NEW); if ( ! ($entry instanceof $assoc['targetEntity'])) { - throw new ORMException( - sprintf( - 'Found entity of type %s on association %s#%s, but expecting %s', - get_class($entry), - $assoc['sourceEntity'], - $assoc['fieldName'], - $targetClass->name - ) - ); + throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']); } switch ($state) { @@ -936,6 +951,7 @@ private function executeInserts($class) $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); foreach ($this->entityInsertions as $oid => $entity) { + if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { continue; } @@ -987,6 +1003,7 @@ private function executeUpdates($class) $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate); foreach ($this->entityUpdates as $oid => $entity) { + if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { continue; } @@ -2589,6 +2606,14 @@ public function createEntity($className, array $data, &$hints = array()) continue 2; } + // use the entity association + if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) { + $class->reflFields[$field]->setValue($entity, $data[$field]); + $this->originalEntityData[$oid][$field] = $data[$field]; + + continue; + } + $associatedId = array(); // TODO: Is this even computed right in all cases of composite keys? @@ -2695,6 +2720,22 @@ public function createEntity($className, array $data, &$hints = array()) break; default: + // Ignore if its a cached collection + if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) { + break; + } + + // use the given collection + if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) { + + $data[$field]->setOwner($entity, $assoc); + + $class->reflFields[$field]->setValue($entity, $data[$field]); + $this->originalEntityData[$oid][$field] = $data[$field]; + + break; + } + // Inject collection $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection); $pColl->setOwner($entity, $assoc); @@ -2940,7 +2981,7 @@ public function size() * * @param string $entityName The name of the Entity. * - * @return \Doctrine\ORM\Persisters\BasicEntityPersister + * @return \Doctrine\ORM\Persisters\EntityPersister */ public function getEntityPersister($entityName) { @@ -2952,21 +2993,27 @@ public function getEntityPersister($entityName) switch (true) { case ($class->isInheritanceTypeNone()): - $persister = new Persisters\BasicEntityPersister($this->em, $class); + $persister = new BasicEntityPersister($this->em, $class); break; case ($class->isInheritanceTypeSingleTable()): - $persister = new Persisters\SingleTablePersister($this->em, $class); + $persister = new SingleTablePersister($this->em, $class); break; case ($class->isInheritanceTypeJoined()): - $persister = new Persisters\JoinedSubclassPersister($this->em, $class); + $persister = new JoinedSubclassPersister($this->em, $class); break; default: throw new \RuntimeException('No persister found for entity.'); } + if ($this->hasCache && $class->cache !== null) { + $persister = $this->em->getConfiguration() + ->getSecondLevelCacheFactory() + ->buildCachedEntityPersister($this->em, $persister, $class); + } + $this->persisters[$entityName] = $persister; return $this->persisters[$entityName]; @@ -2977,29 +3024,31 @@ public function getEntityPersister($entityName) * * @param array $association * - * @return \Doctrine\ORM\Persisters\AbstractCollectionPersister + * @return \Doctrine\ORM\Persisters\CollectionPersister */ public function getCollectionPersister(array $association) { - $type = $association['type']; + $role = isset($association['cache']) + ? $association['sourceEntity'] . '::' . $association['fieldName'] + : $association['type']; - if (isset($this->collectionPersisters[$type])) { - return $this->collectionPersisters[$type]; + if (isset($this->collectionPersisters[$role])) { + return $this->collectionPersisters[$role]; } - switch ($type) { - case ClassMetadata::ONE_TO_MANY: - $persister = new Persisters\OneToManyPersister($this->em); - break; + $persister = ClassMetadata::ONE_TO_MANY === $association['type'] + ? new OneToManyPersister($this->em) + : new ManyToManyPersister($this->em); - case ClassMetadata::MANY_TO_MANY: - $persister = new Persisters\ManyToManyPersister($this->em); - break; + if ($this->hasCache && isset($association['cache'])) { + $persister = $this->em->getConfiguration() + ->getSecondLevelCacheFactory() + ->buildCachedCollectionPersister($this->em, $persister, $association); } - $this->collectionPersisters[$type] = $persister; + $this->collectionPersisters[$role] = $persister; - return $this->collectionPersisters[$type]; + return $this->collectionPersisters[$role]; } /** @@ -3192,6 +3241,50 @@ public function isReadOnly($object) return isset($this->readOnlyObjects[spl_object_hash($object)]); } + /** + * Perform whatever processing is encapsulated here after completion of the transaction. + */ + private function afterTransactionComplete() + { + if ( ! $this->hasCache) { + return; + } + + foreach ($this->persisters as $persister) { + if($persister instanceof CachedPersister) { + $persister->afterTransactionComplete(); + } + } + + foreach ($this->collectionPersisters as $persister) { + if($persister instanceof CachedPersister) { + $persister->afterTransactionComplete(); + } + } + } + + /** + * Perform whatever processing is encapsulated here after completion of the rolled-back. + */ + private function afterTransactionRolledBack() + { + if ( ! $this->hasCache) { + return; + } + + foreach ($this->persisters as $persister) { + if($persister instanceof CachedPersister) { + $persister->afterTransactionRolledBack(); + } + } + + foreach ($this->collectionPersisters as $persister) { + if($persister instanceof CachedPersister) { + $persister->afterTransactionRolledBack(); + } + } + } + private function dispatchOnFlushEvent() { if ($this->evm->hasListeners(Events::onFlush)) { diff --git a/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php new file mode 100644 index 00000000000..9f5f4cc3971 --- /dev/null +++ b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php @@ -0,0 +1,35 @@ +getClassMetadata(); + $cache = array( + 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE + ); + + /* @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */ + if (strstr($metadata->name, 'Doctrine\Tests\Models\Cache')) { + return; + } + + if ($metadata->isVersioned) { + return; + } + + $metadata->enableCache($cache); + + foreach ($metadata->associationMappings as $mapping) { + $metadata->enableAssociationCache($mapping['fieldName'], $cache); + } + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Mocks/CacheEntryMock.php b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php new file mode 100644 index 00000000000..702295cdada --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php @@ -0,0 +1,10 @@ +hash = $hash; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Mocks/CacheRegionMock.php b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php new file mode 100644 index 00000000000..06a40c2eba9 --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php @@ -0,0 +1,77 @@ +returns[$method][] = $value; + } + + public function getReturn($method, $datault) + { + if (isset($this->returns[$method]) && ! empty($this->returns[$method])) { + return array_shift($this->returns[$method]); + } + + return $datault; + } + + public function getName() + { + $this->calls[__FUNCTION__][] = array(); + + return $this->name; + } + + public function contains(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + return $this->getReturn(__FUNCTION__, false); + } + + public function evict(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + return $this->getReturn(__FUNCTION__, true); + } + + public function evictAll() + { + $this->calls[__FUNCTION__][] = array(); + + return $this->getReturn(__FUNCTION__, true); + } + + public function get(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + return $this->getReturn(__FUNCTION__, null); + } + + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) + { + $this->calls[__FUNCTION__][] = array('key' => $key, 'entry' => $entry); + + return $this->getReturn(__FUNCTION__, true); + } + + public function clear() + { + $this->calls = array(); + $this->returns = array(); + } +} diff --git a/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php new file mode 100644 index 00000000000..a84672bea7a --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php @@ -0,0 +1,150 @@ +region = $region; + } + + private function throwException($method) + { + if (isset($this->exceptions[$method]) && ! empty($this->exceptions[$method])) { + $exception = array_shift($this->exceptions[$method]); + + if($exception != null) { + throw $exception; + } + } + } + + public function addException($method, \Exception $e) + { + $this->exceptions[$method][] = $e; + } + + public function setLock(CacheKey $key, Lock $lock) + { + $this->locks[$key->hash] = $lock; + } + + public function contains(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + if (isset($this->locks[$key->hash])) { + return false; + } + + $this->throwException(__FUNCTION__); + + return $this->region->contains($key); + } + + public function evict(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + $this->throwException(__FUNCTION__); + + return $this->region->evict($key); + } + + public function evictAll() + { + $this->calls[__FUNCTION__][] = array(); + + $this->throwException(__FUNCTION__); + + return $this->region->evictAll(); + } + + public function get(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + $this->throwException(__FUNCTION__); + + if (isset($this->locks[$key->hash])) { + return null; + } + + return $this->region->get($key); + } + + public function getName() + { + $this->calls[__FUNCTION__][] = array(); + + $this->throwException(__FUNCTION__); + + return $this->region->getName(); + } + + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) + { + $this->calls[__FUNCTION__][] = array('key' => $key, 'entry' => $entry); + + $this->throwException(__FUNCTION__); + + if (isset($this->locks[$key->hash])) { + + if ($lock !== null && $this->locks[$key->hash]->value === $lock->value) { + return $this->region->put($key, $entry); + } + + return false; + } + + return $this->region->put($key, $entry); + } + + public function lock(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + $this->throwException(__FUNCTION__); + + if (isset($this->locks[$key->hash])) { + return null; + } + + return $this->locks[$key->hash] = Lock::createLockRead(); + } + + public function unlock(CacheKey $key, Lock $lock) + { + $this->calls[__FUNCTION__][] = array('key' => $key, 'lock' => $lock); + + $this->throwException(__FUNCTION__); + + if ( ! isset($this->locks[$key->hash])) { + return; + } + + if ($this->locks[$key->hash]->value !== $lock->value) { + throw LockException::unexpectedLockValue($lock); + } + + unset($this->locks[$key->hash]); + } +} diff --git a/tests/Doctrine/Tests/Models/Cache/Attraction.php b/tests/Doctrine/Tests/Models/Cache/Attraction.php new file mode 100644 index 00000000000..08e84487ef4 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Attraction.php @@ -0,0 +1,95 @@ +name = $name; + $this->city = $city; + $this->infos = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getCity() + { + return $this->city; + } + + public function setCity(City $city) + { + $this->city = $city; + } + + public function getInfos() + { + return $this->infos; + } + + public function addInfo(AttractionInfo $info) + { + if ( ! $this->infos->contains($info)) { + $this->infos->add($info); + } + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/AttractionContactInfo.php b/tests/Doctrine/Tests/Models/Cache/AttractionContactInfo.php new file mode 100644 index 00000000000..76ef305a41c --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/AttractionContactInfo.php @@ -0,0 +1,33 @@ +setAttraction($attraction); + $this->setFone($fone); + } + + public function getFone() + { + return $this->fone; + } + + public function setFone($fone) + { + $this->fone = $fone; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/AttractionInfo.php b/tests/Doctrine/Tests/Models/Cache/AttractionInfo.php new file mode 100644 index 00000000000..418ef49b06c --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/AttractionInfo.php @@ -0,0 +1,54 @@ +id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getAttraction() + { + return $this->attraction; + } + + public function setAttraction(Attraction $attraction) + { + $this->attraction = $attraction; + + $attraction->addInfo($this); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/AttractionLocationInfo.php b/tests/Doctrine/Tests/Models/Cache/AttractionLocationInfo.php new file mode 100644 index 00000000000..ebdb592aea9 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/AttractionLocationInfo.php @@ -0,0 +1,33 @@ +setAttraction($attraction); + $this->setAddress($address); + } + + public function getAddress() + { + return $this->address; + } + + public function setAddress($address) + { + $this->address = $address; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Bar.php b/tests/Doctrine/Tests/Models/Cache/Bar.php new file mode 100644 index 00000000000..f0d0931270d --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Bar.php @@ -0,0 +1,11 @@ +name = $name; + $this->state = $state; + $this->travels = new ArrayCollection(); + $this->attractions = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getState() + { + return $this->state; + } + + public function setState(State $state) + { + $this->state = $state; + } + + public function addTravel(Travel $travel) + { + $this->travels[] = $travel; + } + + public function getTravels() + { + return $this->travels; + } + + public function addAttraction(Attraction $attraction) + { + $this->attractions[] = $attraction; + } + + public function getAttractions() + { + return $this->attractions; + } + + public static function loadMetadata(\Doctrine\ORM\Mapping\ClassMetadataInfo $metadata) + { + include __DIR__ . '/../../ORM/Mapping/php/Doctrine.Tests.Models.Cache.City.php'; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Country.php b/tests/Doctrine/Tests/Models/Cache/Country.php new file mode 100644 index 00000000000..33a9cf40cfd --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Country.php @@ -0,0 +1,50 @@ +name = $name; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Flight.php b/tests/Doctrine/Tests/Models/Cache/Flight.php new file mode 100644 index 00000000000..a95caab241c --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Flight.php @@ -0,0 +1,65 @@ +goingTo = $goingTo; + $this->leavingFrom = $leavingFrom; + $this->departure = new \DateTime(); + } + + public function getLeavingFrom() + { + return $this->leavingFrom; + } + + public function getGoingTo() + { + return $this->goingTo; + } + + public function getDeparture() + { + return $this->departure; + } + + public function setDeparture($departure) + { + $this->departure = $departure; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Restaurant.php b/tests/Doctrine/Tests/Models/Cache/Restaurant.php new file mode 100644 index 00000000000..00d3ac06183 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Restaurant.php @@ -0,0 +1,11 @@ +name = $name; + $this->country = $country; + $this->cities = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getCountry() + { + return $this->country; + } + + public function setCountry(Country $country) + { + $this->country = $country; + } + + public function getCities() + { + return $this->cities; + } + + public function setCities(ArrayCollection $cities) + { + $this->cities = $cities; + } + + public function addCity(City $city) + { + $this->cities[] = $city; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Travel.php b/tests/Doctrine/Tests/Models/Cache/Travel.php new file mode 100644 index 00000000000..75e3275a489 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Travel.php @@ -0,0 +1,112 @@ +traveler = $traveler; + $this->createdAt = new \DateTime('now'); + $this->visitedCities = new ArrayCollection(); + } + + /** + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * @return \Doctrine\Tests\Models\Cache\Traveler + */ + public function getTraveler() + { + return $this->traveler; + } + + /** + * @param \Doctrine\Tests\Models\Cache\Traveler $traveler + */ + public function setTraveler(Traveler $traveler) + { + $this->traveler = $traveler; + } + + /** + * @return \Doctrine\Common\Collections\ArrayCollection + */ + public function getVisitedCities() + { + return $this->visitedCities; + } + + /** + * @param \Doctrine\Tests\Models\Cache\City $city + */ + public function addVisitedCity(City $city) + { + $this->visitedCities->add($city); + } + + /** + * @param \Doctrine\Tests\Models\Cache\City $city + */ + public function removeVisitedCity(City $city) + { + $this->visitedCities->removeElement($city); + } + + /** + * @return \DateTime + */ + public function getCreatedAt() + { + return $this->createdAt; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Traveler.php b/tests/Doctrine/Tests/Models/Cache/Traveler.php new file mode 100644 index 00000000000..ebc5b239cf1 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Traveler.php @@ -0,0 +1,91 @@ +name = $name; + $this->travels = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getTravels() + { + return $this->travels; + } + + /** + * @param \Doctrine\Tests\Models\Cache\Travel $item + */ + public function addTravel(Travel $item) + { + if ( ! $this->travels->contains($item)) { + $this->travels->add($item); + } + + if ($item->getTraveler() !== $this) { + $item->setTraveler($this); + } + } + + /** + * @param \Doctrine\Tests\Models\Cache\Travel $item + */ + public function removeTravel(Travel $item) + { + $this->travels->removeElement($item); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/AbstractRegionTest.php b/tests/Doctrine/Tests/ORM/Cache/AbstractRegionTest.php new file mode 100644 index 00000000000..ca01b069d46 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/AbstractRegionTest.php @@ -0,0 +1,85 @@ +cache = new ArrayCache(); + $this->region = $this->createRegion(); + } + + /** + * @return \Doctrine\ORM\Cache\Region + */ + protected abstract function createRegion(); + + static public function dataProviderCacheValues() + { + return array( + array(new CacheKeyMock('key.1'), new CacheEntryMock(array('id'=>1, 'name' => 'bar'))), + array(new CacheKeyMock('key.2'), new CacheEntryMock(array('id'=>2, 'name' => 'foo'))), + ); + } + + /** + * @dataProvider dataProviderCacheValues + */ + public function testPutGetContainsEvict($key, $value) + { + $this->assertFalse($this->region->contains($key)); + + $this->region->put($key, $value); + + $this->assertTrue($this->region->contains($key)); + + $actual = $this->region->get($key); + + $this->assertEquals($value, $actual); + + $this->region->evict($key); + + $this->assertFalse($this->region->contains($key)); + } + + public function testEvictAll() + { + $key1 = new CacheKeyMock('key.1'); + $key2 = new CacheKeyMock('key.2'); + + $this->assertFalse($this->region->contains($key1)); + $this->assertFalse($this->region->contains($key2)); + + $this->region->put($key1, new CacheEntryMock(array('value' => 'foo'))); + $this->region->put($key2, new CacheEntryMock(array('value' => 'bar'))); + + $this->assertTrue($this->region->contains($key1)); + $this->assertTrue($this->region->contains($key2)); + + $this->region->evictAll(); + + $this->assertFalse($this->region->contains($key1)); + $this->assertFalse($this->region->contains($key2)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/CacheKeyTest.php b/tests/Doctrine/Tests/ORM/Cache/CacheKeyTest.php new file mode 100644 index 00000000000..e0fd5d2a460 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/CacheKeyTest.php @@ -0,0 +1,68 @@ +1)); + $key2 = new EntityCacheKey('Bar', array('id'=>1)); + + $this->assertNotEquals($key1->hash, $key2->hash); + } + + public function testEntityCacheKeyIdentifierType() + { + $key1 = new EntityCacheKey('Foo', array('id'=>1)); + $key2 = new EntityCacheKey('Foo', array('id'=>'1')); + + $this->assertEquals($key1->hash, $key2->hash); + } + + public function testEntityCacheKeyIdentifierOrder() + { + $key1 = new EntityCacheKey('Foo', array('foo_bar'=>1, 'bar_foo'=> 2)); + $key2 = new EntityCacheKey('Foo', array('bar_foo'=>2, 'foo_bar'=> 1)); + + $this->assertEquals($key1->hash, $key2->hash); + } + + public function testCollectionCacheKeyIdentifierType() + { + $key1 = new CollectionCacheKey('Foo', 'assoc', array('id'=>1)); + $key2 = new CollectionCacheKey('Foo', 'assoc', array('id'=>'1')); + + $this->assertEquals($key1->hash, $key2->hash); + } + + public function testCollectionCacheKeyIdentifierOrder() + { + $key1 = new CollectionCacheKey('Foo', 'assoc', array('foo_bar'=>1, 'bar_foo'=> 2)); + $key2 = new CollectionCacheKey('Foo', 'assoc', array('bar_foo'=>2, 'foo_bar'=> 1)); + + $this->assertEquals($key1->hash, $key2->hash); + } + + public function testCollectionCacheKeyIdentifierCollision() + { + $key1 = new CollectionCacheKey('Foo', 'assoc', array('id'=>1)); + $key2 = new CollectionCacheKey('Bar', 'assoc', array('id'=>1)); + + $this->assertNotEquals($key1->hash, $key2->hash); + } + + public function testCollectionCacheKeyAssociationCollision() + { + $key1 = new CollectionCacheKey('Foo', 'assoc1', array('id'=>1)); + $key2 = new CollectionCacheKey('Foo', 'assoc2', array('id'=>1)); + + $this->assertNotEquals($key1->hash, $key2->hash); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php new file mode 100644 index 00000000000..d52196d4473 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -0,0 +1,262 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->em = $this->_getTestEntityManager(); + + + $arguments = array($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + $this->factory = $this->getMock('\Doctrine\ORM\Cache\DefaultCacheFactory', array( + 'getRegion' + ), $arguments); + } + + public function testInplementsCacheFactory() + { + $this->assertInstanceOf('Doctrine\ORM\Cache\CacheFactory', $this->factory); + } + + public function testBuildCachedEntityPersisterReadOnly() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = clone $em->getClassMetadata($entityName); + $persister = new BasicEntityPersister($em, $metadata); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $metadata->cache['usage'] = ClassMetadata::CACHE_USAGE_READ_ONLY; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($metadata->cache)) + ->will($this->returnValue($region)); + + + $cachedPersister = $this->factory->buildCachedEntityPersister($em, $persister, $metadata); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadOnlyCachedEntityPersister', $cachedPersister); + } + + public function testBuildCachedEntityPersisterReadWrite() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = clone $em->getClassMetadata($entityName); + $persister = new BasicEntityPersister($em, $metadata); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $metadata->cache['usage'] = ClassMetadata::CACHE_USAGE_READ_WRITE; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($metadata->cache)) + ->will($this->returnValue($region)); + + $cachedPersister = $this->factory->buildCachedEntityPersister($em, $persister, $metadata); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', $cachedPersister); + } + + public function testBuildCachedEntityPersisterNonStrictReadWrite() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = clone $em->getClassMetadata($entityName); + $persister = new BasicEntityPersister($em, $metadata); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $metadata->cache['usage'] = ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($metadata->cache)) + ->will($this->returnValue($region)); + + $cachedPersister = $this->factory->buildCachedEntityPersister($em, $persister, $metadata); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedEntityPersister', $cachedPersister); + } + + public function testBuildCachedCollectionPersisterReadOnly() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = $em->getClassMetadata($entityName); + $mapping = $metadata->associationMappings['cities']; + $persister = new OneToManyPersister($em); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $mapping['cache']['usage'] = ClassMetadata::CACHE_USAGE_READ_ONLY; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($mapping['cache'])) + ->will($this->returnValue($region)); + + + $cachedPersister = $this->factory->buildCachedCollectionPersister($em, $persister, $mapping); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedCollectionPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadOnlyCachedCollectionPersister', $cachedPersister); + } + + public function testBuildCachedCollectionPersisterReadWrite() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = $em->getClassMetadata($entityName); + $mapping = $metadata->associationMappings['cities']; + $persister = new OneToManyPersister($em); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $mapping['cache']['usage'] = ClassMetadata::CACHE_USAGE_READ_WRITE; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($mapping['cache'])) + ->will($this->returnValue($region)); + + $cachedPersister = $this->factory->buildCachedCollectionPersister($em, $persister, $mapping); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedCollectionPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', $cachedPersister); + } + + public function testBuildCachedCollectionPersisterNonStrictReadWrite() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = $em->getClassMetadata($entityName); + $mapping = $metadata->associationMappings['cities']; + $persister = new OneToManyPersister($em); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $mapping['cache']['usage'] = ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($mapping['cache'])) + ->will($this->returnValue($region)); + + $cachedPersister = $this->factory->buildCachedCollectionPersister($em, $persister, $mapping); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedCollectionPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedCollectionPersister', $cachedPersister); + } + + public function testInheritedEntityCacheRegion() + { + $em = $this->em; + $metadata1 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\AttractionContactInfo'); + $metadata2 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\AttractionLocationInfo'); + $persister1 = new BasicEntityPersister($em, $metadata1); + $persister2 = new BasicEntityPersister($em, $metadata2); + $factory = new DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + + $cachedPersister1 = $factory->buildCachedEntityPersister($em, $persister1, $metadata1); + $cachedPersister2 = $factory->buildCachedEntityPersister($em, $persister2, $metadata2); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister1); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister2); + + $this->assertNotSame($cachedPersister1, $cachedPersister2); + $this->assertSame($cachedPersister1->getCacheRegion(), $cachedPersister2->getCacheRegion()); + } + + public function testCreateNewCacheDriver() + { + $em = $this->em; + $metadata1 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\State'); + $metadata2 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\City'); + $persister1 = new BasicEntityPersister($em, $metadata1); + $persister2 = new BasicEntityPersister($em, $metadata2); + $factory = new DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + + $cachedPersister1 = $factory->buildCachedEntityPersister($em, $persister1, $metadata1); + $cachedPersister2 = $factory->buildCachedEntityPersister($em, $persister2, $metadata2); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister1); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister2); + + $this->assertNotSame($cachedPersister1, $cachedPersister2); + $this->assertNotSame($cachedPersister1->getCacheRegion(), $cachedPersister2->getCacheRegion()); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Unrecognized access strategy type [-1] + */ + public function testBuildCachedEntityPersisterNonStrictException() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = clone $em->getClassMetadata($entityName); + $persister = new BasicEntityPersister($em, $metadata); + + $metadata->cache['usage'] = -1; + + $this->factory->buildCachedEntityPersister($em, $persister, $metadata); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Unrecognized access strategy type [-1] + */ + public function testBuildCachedCollectionPersisterException() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = $em->getClassMetadata($entityName); + $mapping = $metadata->associationMappings['cities']; + $persister = new OneToManyPersister($em); + + $mapping['cache']['usage'] = -1; + + $this->factory->buildCachedCollectionPersister($em, $persister, $mapping); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessage To use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory + */ + public function testInvalidFileLockRegionDirectoryException() + { + $factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + + $factory->getRegion(array( + 'usage' => ClassMetadata::CACHE_USAGE_READ_WRITE, + 'region' => 'foo' + )); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php new file mode 100644 index 00000000000..eda38cfe7a1 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php @@ -0,0 +1,263 @@ +em = $this->_getTestEntityManager(); + $this->cache = new DefaultCache($this->em); + } + + /** + * @param string $className + * @param array $identifier + * @param array $data + */ + private function putEntityCacheEntry($className, array $identifier, array $data) + { + $metadata = $this->em->getClassMetadata($className); + $cacheKey = new EntityCacheKey($metadata->name, $identifier); + $cacheEntry = new EntityCacheEntry($metadata->name, $data); + $persister = $this->em->getUnitOfWork()->getEntityPersister($metadata->rootEntityName); + + $persister->getCacheRegion()->put($cacheKey, $cacheEntry); + } + + /** + * @param string $className + * @param string $association + * @param array $ownerIdentifier + * @param array $data + */ + private function putCollectionCacheEntry($className, $association, array $ownerIdentifier, array $data) + { + $metadata = $this->em->getClassMetadata($className); + $cacheKey = new CollectionCacheKey($metadata->name, $association, $ownerIdentifier); + $cacheEntry = new CollectionCacheEntry($data); + $persister = $this->em->getUnitOfWork()->getCollectionPersister($metadata->getAssociationMapping($association)); + + $persister->getCacheRegion()->put($cacheKey, $cacheEntry); + } + + public function testImplementsCache() + { + $this->assertInstanceOf('Doctrine\ORM\Cache', $this->cache); + } + + public function testGetEntityCacheRegionAccess() + { + $this->assertInstanceOf('Doctrine\ORM\Cache\Region', $this->cache->getEntityCacheRegion(State::CLASSNAME)); + $this->assertNull($this->cache->getEntityCacheRegion(self::NON_CACHEABLE_ENTITY)); + } + + public function testGetCollectionCacheRegionAccess() + { + $this->assertInstanceOf('Doctrine\ORM\Cache\Region', $this->cache->getCollectionCacheRegion(State::CLASSNAME, 'cities')); + $this->assertNull($this->cache->getCollectionCacheRegion(self::NON_CACHEABLE_ENTITY, 'phonenumbers')); + } + + public function testContainsEntity() + { + $identifier = array('id'=>1); + $className = Country::CLASSNAME; + $cacheEntry = array_merge($identifier, array('name' => 'Brazil')); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1)); + + $this->putEntityCacheEntry($className, $identifier, $cacheEntry); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1)); + $this->assertFalse($this->cache->containsEntity(self::NON_CACHEABLE_ENTITY, 1)); + } + + public function testEvictEntity() + { + $identifier = array('id'=>1); + $className = Country::CLASSNAME; + $cacheEntry = array_merge($identifier, array('name' => 'Brazil')); + + $this->putEntityCacheEntry($className, $identifier, $cacheEntry); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1)); + + $this->cache->evictEntity(Country::CLASSNAME, 1); + $this->cache->evictEntity(self::NON_CACHEABLE_ENTITY, 1); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1)); + } + + public function testEvictEntityRegion() + { + $identifier = array('id'=>1); + $className = Country::CLASSNAME; + $cacheEntry = array_merge($identifier, array('name' => 'Brazil')); + + $this->putEntityCacheEntry($className, $identifier, $cacheEntry); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1)); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + $this->cache->evictEntityRegion(self::NON_CACHEABLE_ENTITY); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1)); + } + + public function testEvictEntityRegions() + { + $identifier = array('id'=>1); + $className = Country::CLASSNAME; + $cacheEntry = array_merge($identifier, array('name' => 'Brazil')); + + $this->putEntityCacheEntry($className, $identifier, $cacheEntry); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1)); + + $this->cache->evictEntityRegions(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1)); + } + + public function testContainsCollection() + { + $ownerId = array('id'=>1); + $className = State::CLASSNAME; + $association = 'cities'; + $cacheEntry = array( + array('id' => 11), + array('id' => 12), + ); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + + $this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + $this->assertFalse($this->cache->containsCollection(self::NON_CACHEABLE_ENTITY, 'phonenumbers', 1)); + } + + public function testEvictCollection() + { + $ownerId = array('id'=>1); + $className = State::CLASSNAME; + $association = 'cities'; + $cacheEntry = array( + array('id' => 11), + array('id' => 12), + ); + + $this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + + $this->cache->evictCollection($className, $association, $ownerId); + $this->cache->evictCollection(self::NON_CACHEABLE_ENTITY, 'phonenumbers', 1); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + } + + public function testEvictCollectionRegion() + { + $ownerId = array('id'=>1); + $className = State::CLASSNAME; + $association = 'cities'; + $cacheEntry = array( + array('id' => 11), + array('id' => 12), + ); + + $this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + + $this->cache->evictCollectionRegion($className, $association); + $this->cache->evictCollectionRegion(self::NON_CACHEABLE_ENTITY, 'phonenumbers'); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + } + + public function testEvictCollectionRegions() + { + $ownerId = array('id'=>1); + $className = State::CLASSNAME; + $association = 'cities'; + $cacheEntry = array( + array('id' => 11), + array('id' => 12), + ); + + $this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + + $this->cache->evictCollectionRegions(); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + } + + public function testQueryCache() + { + $this->assertFalse($this->cache->containsQuery('foo')); + + $defaultQueryCache = $this->cache->getQueryCache(); + $fooQueryCache = $this->cache->getQueryCache('foo'); + + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCache', $defaultQueryCache); + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCache', $fooQueryCache); + $this->assertSame($defaultQueryCache, $this->cache->getQueryCache()); + $this->assertSame($fooQueryCache, $this->cache->getQueryCache('foo')); + + $this->cache->evictQueryRegion(); + $this->cache->evictQueryRegion('foo'); + $this->cache->evictQueryRegions(); + + $this->assertTrue($this->cache->containsQuery('foo')); + + $this->assertSame($defaultQueryCache, $this->cache->getQueryCache()); + $this->assertSame($fooQueryCache, $this->cache->getQueryCache('foo')); + } + + public function testToIdentifierArrayShoudLookupForEntityIdentifier() + { + $identifier = 123; + $entity = new Country('Foo'); + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + $method = new \ReflectionMethod($this->cache, 'toIdentifierArray'); + $property = new \ReflectionProperty($entity, 'id'); + + $property->setAccessible(true); + $method->setAccessible(true); + $property->setValue($entity, $identifier); + + $this->assertEquals(array('id'=>$identifier), $method->invoke($this->cache, $metadata, $identifier)); + } + +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCollectionHydratorTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCollectionHydratorTest.php new file mode 100644 index 00000000000..74082d8a7bb --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCollectionHydratorTest.php @@ -0,0 +1,77 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->structure = new DefaultCollectionHydrator($this->_em); + } + + public function testImplementsCollectionEntryStructure() + { + $this->assertInstanceOf('Doctrine\ORM\Cache\DefaultCollectionHydrator', $this->structure); + } + + public function testLoadCacheCollection() + { + $targetRegion = $this->_em->getCache()->getEntityCacheRegion(City::CLASSNAME); + $entry = new CollectionCacheEntry(array( + array('id'=>31), + array('id'=>32), + )); + + $targetRegion->put(new EntityCacheKey(City::CLASSNAME, array('id'=>31)), new EntityCacheEntry(City::CLASSNAME, array('id'=>31, 'name'=>'Foo'))); + $targetRegion->put(new EntityCacheKey(City::CLASSNAME, array('id'=>32)), new EntityCacheEntry(City::CLASSNAME, array('id'=>32, 'name'=>'Bar'))); + + $sourceClass = $this->_em->getClassMetadata(State::CLASSNAME); + $targetClass = $this->_em->getClassMetadata(City::CLASSNAME); + $key = new CollectionCacheKey($sourceClass->name, 'cities', array('id'=>21)); + $collection = new PersistentCollection($this->_em, $targetClass, new ArrayCollection()); + $list = $this->structure->loadCacheEntry($sourceClass, $key, $entry, $collection); + + $this->assertNotNull($list); + $this->assertCount(2, $list); + $this->assertCount(2, $collection); + + $this->assertInstanceOf($targetClass->name, $list[0]); + $this->assertInstanceOf($targetClass->name, $list[1]); + $this->assertInstanceOf($targetClass->name, $collection[0]); + $this->assertInstanceOf($targetClass->name, $collection[1]); + + $this->assertSame($list[0], $collection[0]); + $this->assertSame($list[1], $collection[1]); + + $this->assertEquals(31, $list[0]->getId()); + $this->assertEquals(32, $list[1]->getId()); + $this->assertEquals($list[0]->getId(), $collection[0]->getId()); + $this->assertEquals($list[1]->getId(), $collection[1]->getId()); + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($collection[0])); + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($collection[1])); + } + +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php new file mode 100644 index 00000000000..4c44430b064 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php @@ -0,0 +1,125 @@ +em = $this->_getTestEntityManager(); + $this->structure = new DefaultEntityHydrator($this->em); + } + + public function testImplementsEntityEntryStructure() + { + $this->assertInstanceOf('\Doctrine\ORM\Cache\EntityHydrator', $this->structure); + } + + public function testCreateEntity() + { + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + $key = new EntityCacheKey($metadata->name, array('id'=>1)); + $entry = new EntityCacheEntry($metadata->name, array('id'=>1, 'name'=>'Foo')); + $entity = $this->structure->loadCacheEntry($metadata, $key, $entry); + + $this->assertInstanceOf($metadata->name, $entity); + + $this->assertEquals(1, $entity->getId()); + $this->assertEquals('Foo', $entity->getName()); + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->em->getUnitOfWork()->getEntityState($entity)); + } + + public function testLoadProxy() + { + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + $key = new EntityCacheKey($metadata->name, array('id'=>1)); + $entry = new EntityCacheEntry($metadata->name, array('id'=>1, 'name'=>'Foo')); + $proxy = $this->em->getReference($metadata->name, $key->identifier); + $entity = $this->structure->loadCacheEntry($metadata, $key, $entry, $proxy); + + $this->assertInstanceOf($metadata->name, $entity); + $this->assertSame($proxy, $entity); + + $this->assertEquals(1, $entity->getId()); + $this->assertEquals('Foo', $entity->getName()); + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->em->getUnitOfWork()->getEntityState($proxy)); + } + + public function testBuildCacheEntry() + { + $entity = new Country('Foo'); + $uow = $this->em->getUnitOfWork(); + $data = array('id'=>1, 'name'=>'Foo'); + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + $key = new EntityCacheKey($metadata->name, array('id'=>1)); + + $entity->setId(1); + $uow->registerManaged($entity, $key->identifier, $data); + + $cache = $this->structure->buildCacheEntry($metadata, $key, $entity); + + $this->assertInstanceOf('Doctrine\ORM\Cache\CacheEntry', $cache); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $cache); + + $this->assertArrayHasKey('id', $cache->data); + $this->assertArrayHasKey('name', $cache->data); + $this->assertEquals(array( + 'id' => 1, + 'name' => 'Foo', + ), $cache->data); + } + + public function testBuildCacheEntryOwningSide() + { + $country = new Country('Foo'); + $state = new State('Bat', $country); + $uow = $this->em->getUnitOfWork(); + $countryData = array('id'=>11, 'name'=>'Foo'); + $stateData = array('id'=>12, 'name'=>'Bar', 'country' => $country); + $metadata = $this->em->getClassMetadata(State::CLASSNAME); + $key = new EntityCacheKey($metadata->name, array('id'=>11)); + + $country->setId(11); + $state->setId(12); + + $uow->registerManaged($country, array('id'=>11), $countryData); + $uow->registerManaged($state, array('id'=>12), $stateData); + + $cache = $this->structure->buildCacheEntry($metadata, $key, $state); + + $this->assertInstanceOf('Doctrine\ORM\Cache\CacheEntry', $cache); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $cache); + + $this->assertArrayHasKey('id', $cache->data); + $this->assertArrayHasKey('name', $cache->data); + $this->assertArrayHasKey('country', $cache->data); + $this->assertEquals(array( + 'id' => 11, + 'name' => 'Bar', + 'country' => array ('id' => 11), + ), $cache->data); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php new file mode 100644 index 00000000000..8cc1c54ef40 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -0,0 +1,526 @@ +enableSecondLevelCache(); + + $this->em = $this->_getTestEntityManager(); + $this->region = new CacheRegionMock(); + $this->queryCache = new DefaultQueryCache($this->em, $this->region); + $this->cacheFactory = new CacheFactoryDefaultQueryCacheTest($this->queryCache, $this->region); + + $this->em->getConfiguration()->setSecondLevelCacheFactory($this->cacheFactory); + } + + public function testImplementQueryCache() + { + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCache', $this->queryCache); + } + + public function testGetRegion() + { + $this->assertSame($this->region, $this->queryCache->getRegion()); + } + + public function testClearShouldEvictRegion() + { + $this->queryCache->clear(); + + $this->assertArrayHasKey('evictAll', $this->region->calls); + $this->assertCount(1, $this->region->calls['evictAll']); + } + + public function testPutBasicQueryResult() + { + $result = array(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + for ($i = 0; $i < 4; $i++) { + $name = "Country $i"; + $entity = new Country($name); + $result[] = $entity; + + $metadata->setFieldValue($entity, 'id', $i); + $this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('name' => $name)); + } + + $this->assertTrue($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(5, $this->region->calls['put']); + + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][0]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][1]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][2]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][3]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheKey', $this->region->calls['put'][4]['key']); + + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][0]['entry']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][1]['entry']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][2]['entry']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][3]['entry']); + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheEntry', $this->region->calls['put'][4]['entry']); + } + + public function testPutToOneAssociationQueryResult() + { + $result = array(); + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + $stateClass = $this->em->getClassMetadata(State::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c'); + $rsm->addJoinedEntityFromClassMetadata(State::CLASSNAME, 's', 'c', 'state', array('id'=>'state_id', 'name'=>'state_name')); + + for ($i = 0; $i < 4; $i++) { + $state = new State("State $i"); + $city = new City("City $i", $state); + $result[] = $city; + + $cityClass->setFieldValue($city, 'id', $i); + $stateClass->setFieldValue($state, 'id', $i*2); + + $uow->registerManaged($state, array('id' => $state->getId()), array('name' => $city->getName())); + $uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => $state)); + } + + $this->assertTrue($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(9, $this->region->calls['put']); + + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][0]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][1]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][2]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][3]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][4]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][5]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][6]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][7]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheKey', $this->region->calls['put'][8]['key']); + } + + public function testPutToOneAssociationNullQueryResult() + { + $result = array(); + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c'); + $rsm->addJoinedEntityFromClassMetadata(State::CLASSNAME, 's', 'c', 'state', array('id'=>'state_id', 'name'=>'state_name')); + + for ($i = 0; $i < 4; $i++) { + $city = new City("City $i", null); + $result[] = $city; + + $cityClass->setFieldValue($city, 'id', $i); + + $uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => null)); + } + + $this->assertTrue($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(5, $this->region->calls['put']); + + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][0]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][1]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][2]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][3]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheKey', $this->region->calls['put'][4]['key']); + } + + public function testPutToManyAssociationQueryResult() + { + $result = array(); + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + $stateClass = $this->em->getClassMetadata(State::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(State::CLASSNAME, 's'); + $rsm->addJoinedEntityFromClassMetadata(City::CLASSNAME, 'c', 's', 'cities', array('id'=>'c_id', 'name'=>'c_name')); + + for ($i = 0; $i < 4; $i++) { + $state = new State("State $i"); + $city1 = new City("City 1", $state); + $city2 = new City("City 2", $state); + $result[] = $state; + + $cityClass->setFieldValue($city1, 'id', $i + 11); + $cityClass->setFieldValue($city2, 'id', $i + 22); + $stateClass->setFieldValue($state, 'id', $i); + + $state->addCity($city1); + $state->addCity($city2); + + $uow->registerManaged($city1, array('id' => $city1->getId()), array('name' => $city1->getName(), 'state' => $state)); + $uow->registerManaged($city2, array('id' => $city2->getId()), array('name' => $city2->getName(), 'state' => $state)); + $uow->registerManaged($state, array('id' => $state->getId()), array('name' => $state->getName(), 'cities' => $state->getCities())); + } + + $this->assertTrue($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(13, $this->region->calls['put']); + } + + public function testgGetBasicQueryResult() + { + $rsm = new ResultSetMappingBuilder($this->em); + $key = new QueryCacheKey('query.key1', 0); + $entry = new QueryCacheEntry(array( + array('identifier' => array('id' => 1)), + array('identifier' => array('id' => 2)) + )); + + $data = array( + array('id'=>1, 'name' => 'Foo'), + array('id'=>2, 'name' => 'Bar') + ); + + $this->region->addReturn('get', $entry); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[0])); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[1])); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + $result = $this->queryCache->get($key, $rsm, $entry); + + $this->assertCount(2, $result); + $this->assertInstanceOf(Country::CLASSNAME, $result[0]); + $this->assertInstanceOf(Country::CLASSNAME, $result[1]); + $this->assertEquals(1, $result[0]->getId()); + $this->assertEquals(2, $result[1]->getId()); + $this->assertEquals('Foo', $result[0]->getName()); + $this->assertEquals('Bar', $result[1]->getName()); + } + + public function testCancelPutResultIfEntityPutFails() + { + $result = array(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + for ($i = 0; $i < 4; $i++) { + $name = "Country $i"; + $entity = new Country($name); + $result[] = $entity; + + $metadata->setFieldValue($entity, 'id', $i); + $this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('name' => $name)); + } + + $this->region->addReturn('put', false); + + $this->assertFalse($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(1, $this->region->calls['put']); + } + + public function testCancelPutResultIfAssociationEntityPutFails() + { + $result = array(); + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + $stateClass = $this->em->getClassMetadata(State::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c'); + $rsm->addJoinedEntityFromClassMetadata(State::CLASSNAME, 's', 'c', 'state', array('id'=>'state_id', 'name'=>'state_name')); + + $state = new State("State 1"); + $city = new City("City 2", $state); + $result[] = $city; + + $cityClass->setFieldValue($city, 'id', 1); + $stateClass->setFieldValue($state, 'id', 11); + + $uow->registerManaged($state, array('id' => $state->getId()), array('name' => $city->getName())); + $uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => $state)); + + $this->region->addReturn('put', true); // put root entity + $this->region->addReturn('put', false); // association fails + + $this->assertFalse($this->queryCache->put($key, $rsm, $result)); + } + + public function testCancelPutToManyAssociationQueryResult() + { + $result = array(); + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + $stateClass = $this->em->getClassMetadata(State::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(State::CLASSNAME, 's'); + $rsm->addJoinedEntityFromClassMetadata(City::CLASSNAME, 'c', 's', 'cities', array('id'=>'c_id', 'name'=>'c_name')); + + $state = new State("State"); + $city1 = new City("City 1", $state); + $city2 = new City("City 2", $state); + $result[] = $state; + + $stateClass->setFieldValue($state, 'id', 1); + $cityClass->setFieldValue($city1, 'id', 11); + $cityClass->setFieldValue($city2, 'id', 22); + + $state->addCity($city1); + $state->addCity($city2); + + $uow->registerManaged($city1, array('id' => $city1->getId()), array('name' => $city1->getName(), 'state' => $state)); + $uow->registerManaged($city2, array('id' => $city2->getId()), array('name' => $city2->getName(), 'state' => $state)); + $uow->registerManaged($state, array('id' => $state->getId()), array('name' => $state->getName(), 'cities' => $state->getCities())); + + $this->region->addReturn('put', true); // put root entity + $this->region->addReturn('put', false); // collection association fails + + $this->assertFalse($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(2, $this->region->calls['put']); + } + + public function testIgnoreCacheNonGetMode() + { + $rsm = new ResultSetMappingBuilder($this->em); + $key = new QueryCacheKey('query.key1', 0, Cache::MODE_PUT); + $entry = new QueryCacheEntry(array( + array('identifier' => array('id' => 1)), + array('identifier' => array('id' => 2)) + )); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + $this->region->addReturn('get', $entry); + + $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + } + + public function testIgnoreCacheNonPutMode() + { + $result = array(); + $rsm = new ResultSetMappingBuilder($this->em); + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + $key = new QueryCacheKey('query.key1', 0, Cache::MODE_GET); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + for ($i = 0; $i < 4; $i++) { + $name = "Country $i"; + $entity = new Country($name); + $result[] = $entity; + + $metadata->setFieldValue($entity, 'id', $i); + $this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('name' => $name)); + } + + $this->assertFalse($this->queryCache->put($key, $rsm, $result)); + } + + public function testGetShouldIgnoreOldQueryCacheEntryResult() + { + $rsm = new ResultSetMappingBuilder($this->em); + $key = new QueryCacheKey('query.key1', 50); + $entry = new QueryCacheEntry(array( + array('identifier' => array('id' => 1)), + array('identifier' => array('id' => 2)) + )); + $entities = array( + array('id'=>1, 'name' => 'Foo'), + array('id'=>2, 'name' => 'Bar') + ); + + $entry->time = time() - 100; + + $this->region->addReturn('get', $entry); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $entities[0])); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $entities[1])); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + } + + public function testGetShouldIgnoreNonQueryCacheEntryResult() + { + $rsm = new ResultSetMappingBuilder($this->em); + $key = new QueryCacheKey('query.key1', 0); + $entry = new \ArrayObject(array( + array('identifier' => array('id' => 1)), + array('identifier' => array('id' => 2)) + )); + + $data = array( + array('id'=>1, 'name' => 'Foo'), + array('id'=>2, 'name' => 'Bar') + ); + + $this->region->addReturn('get', $entry); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[0])); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[1])); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + } + + public function testGetShouldIgnoreMissingEntityQueryCacheEntry() + { + $rsm = new ResultSetMappingBuilder($this->em); + $key = new QueryCacheKey('query.key1', 0); + $entry = new QueryCacheEntry(array( + array('identifier' => array('id' => 1)), + array('identifier' => array('id' => 2)) + )); + + $this->region->addReturn('get', $entry); + $this->region->addReturn('get', null); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + } + + /** + * @expectedException Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Entity association field "Doctrine\Tests\Models\Cache\City#travels" not configured as part of the second-level cache. + */ + public function testQueryNotCacheableAssociationException() + { + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + $city = new City("City 1", null); + $result = array( + $city + ); + + $cityClass->setFieldValue($city, 'id', 1); + + $rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c'); + $rsm->addJoinedEntityFromClassMetadata(Travel::CLASSNAME, 't', 'c', 'travels', array('id' => 't_id')); + $uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => null)); + + $this->queryCache->put($key, $rsm, $result); + } + + /** + * @expectedException Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Second level cache does not suport scalar results. + */ + public function testScalarResultException() + { + $result = array(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + + $rsm->addScalarResult('id', 'u'); + + $this->queryCache->put($key, $rsm, $result); + } + + /** + * @expectedException Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Entity "Doctrine\Tests\Models\Generic\BooleanModel" not configured as part of the second-level cache. + */ + public function testNotCacheableEntityException() + { + $result = array(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $className = 'Doctrine\Tests\Models\Generic\BooleanModel'; + + $rsm->addRootEntityFromClassMetadata($className, 'c'); + + for ($i = 0; $i < 4; $i++) { + $entity = new BooleanModel(); + $boolean = ($i % 2 === 0); + + $entity->id = $i; + $entity->booleanField = $boolean; + $result[] = $entity; + + $this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('booleanField' => $boolean)); + } + + $this->assertFalse($this->queryCache->put($key, $rsm, $result)); + } + +} + +class CacheFactoryDefaultQueryCacheTest extends \Doctrine\ORM\Cache\DefaultCacheFactory +{ + private $queryCache; + private $region; + + public function __construct(DefaultQueryCache $queryCache, CacheRegionMock $region) + { + $this->queryCache = $queryCache; + $this->region = $region; + } + + public function buildQueryCache(EntityManagerInterface $em, $regionName = null) + { + return $this->queryCache; + } + + public function getRegion(array $cache) + { + return $this->region; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultRegionTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultRegionTest.php new file mode 100644 index 00000000000..e8f7764b59f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultRegionTest.php @@ -0,0 +1,50 @@ +cache); + } + + public function testGetters() + { + $this->assertEquals('default.region.test', $this->region->getName()); + $this->assertSame($this->cache, $this->region->getCache()); + } + + public function testSharedRegion() + { + if ( ! extension_loaded('apc') || false === @apc_cache_info()) { + $this->markTestSkipped('The ' . __CLASS__ .' requires the use of APC'); + } + + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('value' => 'foo')); + $region1 = new DefaultRegion('region1', new \Doctrine\Common\Cache\ApcCache()); + $region2 = new DefaultRegion('region2', new \Doctrine\Common\Cache\ApcCache()); + + $this->assertFalse($region1->contains($key)); + $this->assertFalse($region2->contains($key)); + + $region1->put($key, $entry); + $region2->put($key, $entry); + + $this->assertTrue($region1->contains($key)); + $this->assertTrue($region2->contains($key)); + + $region1->evictAll(); + + $this->assertFalse($region1->contains($key)); + $this->assertTrue($region2->contains($key)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php b/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php new file mode 100644 index 00000000000..94cd3985d11 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php @@ -0,0 +1,251 @@ +directory)) { + return; + } + + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->directory), RecursiveIteratorIterator::CHILD_FIRST) as $file) { + $file->isFile() + ? @unlink($file->getRealPath()) + : @rmdir($file->getRealPath()); + } + } + + /** + * @param \Doctrine\ORM\Cache\ConcurrentRegion $region + * @param \Doctrine\ORM\Cache\CacheKey $key + * + * @return string + */ + private function getFileName(ConcurrentRegion $region, CacheKey $key) + { + $reflection = new \ReflectionMethod($region, 'getLockFileName'); + + $reflection->setAccessible(true); + + return $reflection->invoke($region, $key); + } + + protected function createRegion() + { + $this->directory = sys_get_temp_dir() . '/doctrine_lock_'. uniqid(); + + $region = new DefaultRegion('concurren_region_test', $this->cache); + + return new FileLockRegion($region, $this->directory, 60); + } + + public function testGetRegionName() + { + $this->assertEquals('concurren_region_test', $this->region->getName()); + } + + public function testLockAndUnlock() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + $lock = $this->region->lock($key); + + $this->assertFileExists($file); + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock); + $this->assertEquals($lock->value, file_get_contents($file)); + + // should be not available after lock + $this->assertFalse($this->region->contains($key)); + $this->assertNull($this->region->get($key)); + + $this->assertTrue($this->region->unlock($key, $lock)); + $this->assertFileNotExists($file); + } + + public function testLockWithExistingLock() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + file_put_contents($file, 'foo'); + $this->assertFileExists($file); + $this->assertEquals('foo' , file_get_contents($file)); + + $this->assertNull($this->region->lock($key)); + $this->assertEquals('foo' , file_get_contents($file)); + $this->assertFileExists($file); + + // should be not available + $this->assertFalse($this->region->contains($key)); + $this->assertNull($this->region->get($key)); + } + + public function testUnlockWithExistingLock() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock = $this->region->lock($key)); + $this->assertEquals($lock->value, file_get_contents($file)); + $this->assertFileExists($file); + + // change the lock + file_put_contents($file, 'foo'); + $this->assertFileExists($file); + $this->assertEquals('foo' , file_get_contents($file)); + + //try to unlock + $this->assertFalse($this->region->unlock($key, $lock)); + $this->assertEquals('foo' , file_get_contents($file)); + $this->assertFileExists($file); + + // should be not available + $this->assertFalse($this->region->contains($key)); + $this->assertNull($this->region->get($key)); + } + + public function testPutWithExistingLock() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + // create lock + file_put_contents($file, 'foo'); + $this->assertFileExists($file); + $this->assertEquals('foo' , file_get_contents($file)); + + $this->assertFalse($this->region->contains($key)); + $this->assertFalse($this->region->put($key, $entry)); + $this->assertFalse($this->region->contains($key)); + + $this->assertFileExists($file); + $this->assertEquals('foo' , file_get_contents($file)); + } + + public function testLockedEvict() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock = $this->region->lock($key)); + $this->assertEquals($lock->value, file_get_contents($file)); + $this->assertFileExists($file); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->evict($key)); + $this->assertFalse($this->region->contains($key)); + $this->assertFileNotExists($file); + } + + public function testLockedEvictAll() + { + $key1 = new CacheKeyMock('key1'); + $entry1 = new CacheEntryMock(array('foo1' => 'bar1')); + $file1 = $this->getFileName($this->region, $key1); + + $key2 = new CacheKeyMock('key2'); + $entry2 = new CacheEntryMock(array('foo2' => 'bar2')); + $file2 = $this->getFileName($this->region, $key2); + + $this->assertFalse($this->region->contains($key1)); + $this->assertTrue($this->region->put($key1, $entry1)); + $this->assertTrue($this->region->contains($key1)); + + $this->assertFalse($this->region->contains($key2)); + $this->assertTrue($this->region->put($key2, $entry2)); + $this->assertTrue($this->region->contains($key2)); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock1 = $this->region->lock($key1)); + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock2 = $this->region->lock($key2)); + + $this->assertEquals($lock2->value, file_get_contents($file2)); + $this->assertEquals($lock1->value, file_get_contents($file1)); + + $this->assertFileExists($file1); + $this->assertFileExists($file2); + + $this->assertTrue($this->region->evictAll()); + + $this->assertFileNotExists($file1); + $this->assertFileNotExists($file2); + + $this->assertFalse($this->region->contains($key1)); + $this->assertFalse($this->region->contains($key2)); + } + + public function testLockLifetime() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + $property = new \ReflectionProperty($this->region, 'lockLifetime'); + + $property->setAccessible(true); + $property->setValue($this->region, -10); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock = $this->region->lock($key)); + $this->assertEquals($lock->value, file_get_contents($file)); + $this->assertFileExists($file); + + // outdated lock should be removed + $this->assertTrue($this->region->contains($key)); + $this->assertNotNull($this->region->get($key)); + $this->assertFileNotExists($file); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php new file mode 100644 index 00000000000..d0f76deb727 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php @@ -0,0 +1,301 @@ +getSharedSecondLevelCacheDriverImpl()->flushAll(); + $this->enableSecondLevelCache(); + parent::setUp(); + + $this->em = $this->_getTestEntityManager(); + $this->region = $this->createRegion(); + $this->collectionPersister = $this->getMock('Doctrine\ORM\Persisters\CollectionPersister', $this->collectionPersisterMockMethods); + } + + /** + * @return \Doctrine\ORM\Cache\Region + */ + protected function createRegion() + { + return $this->getMock('Doctrine\ORM\Cache\Region', $this->regionMockMethods); + } + + /** + * @return \Doctrine\ORM\PersistentCollection + */ + protected function createCollection($owner, $assoc = null, $class = null, $elements = null) + { + $em = $this->em; + $class = $class ?: $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\State'); + $assoc = $assoc ?: $class->associationMappings['cities']; + $coll = new \Doctrine\ORM\PersistentCollection($em, $class, $elements ?: new ArrayCollection); + + $coll->setOwner($owner, $assoc); + $coll->setInitialized(true); + + return $coll; + } + + protected function createPersisterDefault() + { + $assoc = $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\State')->associationMappings['cities']; + + return $this->createPersister($this->em, $this->collectionPersister, $this->region, $assoc); + } + + public function testImplementsEntityPersister() + { + $persister = $this->createPersisterDefault(); + + $this->assertInstanceOf('Doctrine\ORM\Persisters\CollectionPersister', $persister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedPersister', $persister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedCollectionPersister', $persister); + } + + public function testInvokeDelete() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('delete') + ->with($this->equalTo($collection)); + + $this->assertNull($persister->delete($collection)); + } + + public function testInvokeUpdate() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $collection->setDirty(true); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('update') + ->with($this->equalTo($collection)); + + $this->assertNull($persister->update($collection)); + } + + public function testInvokeDeleteRows() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('deleteRows') + ->with($this->equalTo($collection)); + + $this->assertNull($persister->deleteRows($collection)); + } + + public function testInvokeInsertRows() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('insertRows') + ->with($this->equalTo($collection)); + + $this->assertNull($persister->insertRows($collection)); + } + + public function testInvokeCount() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('count') + ->with($this->equalTo($collection)) + ->will($this->returnValue(0)); + + $this->assertEquals(0, $persister->count($collection)); + } + + public function testInvokEslice() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $slice = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('slice') + ->with($this->equalTo($collection), $this->equalTo(1), $this->equalTo(2)) + ->will($this->returnValue($slice)); + + $this->assertEquals($slice, $persister->slice($collection, 1 , 2)); + } + + public function testInvokeContains() + { + $entity = new State("Foo"); + $element = new State("Bar"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('contains') + ->with($this->equalTo($collection), $this->equalTo($element)) + ->will($this->returnValue(false)); + + $this->assertFalse($persister->contains($collection,$element)); + } + + public function testInvokeContainsKey() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('containsKey') + ->with($this->equalTo($collection), $this->equalTo(0)) + ->will($this->returnValue(false)); + + $this->assertFalse($persister->containsKey($collection, 0)); + } + + public function testInvokeRemoveElement() + { + $entity = new State("Foo"); + $element = new State("Bar"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('removeElement') + ->with($this->equalTo($collection), $this->equalTo($element)) + ->will($this->returnValue(false)); + + $this->assertFalse($persister->removeElement($collection, $element)); + } + + public function testInvokeRemoveKey() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('removeKey') + ->with($this->equalTo($collection), $this->equalTo(0)) + ->will($this->returnValue(false)); + + $this->assertFalse($persister->removeKey($collection, 0)); + } + + public function testInvokeGet() + { + $entity = new State("Foo"); + $element = new State("Bar"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('get') + ->with($this->equalTo($collection), $this->equalTo(0)) + ->will($this->returnValue($element)); + + $this->assertEquals($element, $persister->get($collection, 0)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractEntityPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractEntityPersisterTest.php new file mode 100644 index 00000000000..ed4093a88af --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractEntityPersisterTest.php @@ -0,0 +1,416 @@ +getSharedSecondLevelCacheDriverImpl()->flushAll(); + $this->enableSecondLevelCache(); + parent::setUp(); + + $this->em = $this->_getTestEntityManager(); + $this->region = $this->createRegion(); + $this->entityPersister = $this->getMock('Doctrine\ORM\Persisters\EntityPersister', $this->entityPersisterMockMethods); + } + + /** + * @return \Doctrine\ORM\Cache\Region + */ + protected function createRegion() + { + return $this->getMock('Doctrine\ORM\Cache\Region', $this->regionMockMethods); + } + + /** + * @return Doctrine\ORM\Cache\Persister\AbstractEntityPersister + */ + protected function createPersisterDefault() + { + return $this->createPersister($this->em, $this->entityPersister, $this->region, $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\Country')); + } + + public function testImplementsEntityPersister() + { + $persister = $this->createPersisterDefault(); + + $this->assertInstanceOf('Doctrine\ORM\Persisters\EntityPersister', $persister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedPersister', $persister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $persister); + } + + public function testInvokeAddInsert() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('addInsert') + ->with($this->equalTo($entity)); + + $this->assertNull($persister->addInsert($entity)); + } + + public function testInvokeGetInserts() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('getInserts') + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->getInserts()); + } + + public function testInvokeGetSelectSQL() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('getSelectSQL') + ->with($this->equalTo(array('name'=>'Foo')), $this->equalTo(array(0)), $this->equalTo(1), $this->equalTo(2), $this->equalTo(3), $this->equalTo(array(4))) + ->will($this->returnValue('SELECT * FROM foo WERE name = ?')); + + $this->assertEquals('SELECT * FROM foo WERE name = ?', $persister->getSelectSQL(array('name'=>'Foo'), array(0), 1, 2, 3, array(4))); + } + + public function testInvokeGetInsertSQL() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('getInsertSQL') + ->will($this->returnValue('INSERT INTO foo (?)')); + + $this->assertEquals('INSERT INTO foo (?)', $persister->getInsertSQL()); + } + + public function testInvokeExpandParameters() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('expandParameters') + ->with($this->equalTo(array('name'=>'Foo'))) + ->will($this->returnValue(array('name'=>'Foo'))); + + $this->assertEquals(array('name'=>'Foo'), $persister->expandParameters(array('name'=>'Foo'))); + } + + public function testInvokeSelectConditionStatementSQL() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('getSelectConditionStatementSQL') + ->with($this->equalTo('id'), $this->equalTo(1), $this->equalTo(array()), $this->equalTo('=')) + ->will($this->returnValue('name = 1')); + + $this->assertEquals('name = 1', $persister->getSelectConditionStatementSQL('id', 1, array(), '=')); + } + + public function testInvokeExecuteInserts() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('executeInserts') + ->will($this->returnValue(array('id' => 1))); + + $this->assertEquals(array('id' => 1), $persister->executeInserts()); + } + + public function testInvokeUpdate() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('update') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->assertNull($persister->update($entity)); + } + + public function testInvokeDelete() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('delete') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->assertNull($persister->delete($entity)); + } + + public function testInvokeGetOwningTable() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('getOwningTable') + ->with($this->equalTo('name')) + ->will($this->returnValue('t')); + + $this->assertEquals('t', $persister->getOwningTable('name')); + } + + public function testInvokeLoad() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('load') + ->with($this->equalTo(array('id' => 1)), $this->equalTo($entity), $this->equalTo(array(0)), $this->equalTo(array(1)), $this->equalTo(2), $this->equalTo(3), $this->equalTo(array(4))) + ->will($this->returnValue($entity)); + + $this->assertEquals($entity, $persister->load(array('id' => 1), $entity, array(0), array(1), 2, 3, array(4))); + } + + public function testInvokeLoadAll() + { + $rsm = new ResultSetMappingBuilder($this->em); + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $rsm->addEntityResult(Country::CLASSNAME, 'c'); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->entityPersister->expects($this->once()) + ->method('loadAll') + ->with($this->equalTo(array('id' => 1)), $this->equalTo(array(0)), $this->equalTo(1), $this->equalTo(2)) + ->will($this->returnValue(array($entity))); + + $this->entityPersister->expects($this->once()) + ->method('getResultSetMapping') + ->will($this->returnValue($rsm)); + + $this->assertEquals(array($entity), $persister->loadAll(array('id' => 1), array(0), 1, 2)); + } + + public function testInvokeLoadById() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('loadById') + ->with($this->equalTo(array('id' => 1)), $this->equalTo($entity)) + ->will($this->returnValue($entity)); + + $this->assertEquals($entity, $persister->loadById(array('id' => 1), $entity)); + } + + public function testInvokeLoadOneToOneEntity() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('loadOneToOneEntity') + ->with($this->equalTo(array()), $this->equalTo('foo'), $this->equalTo(array('id' => 11))) + ->will($this->returnValue($entity)); + + $this->assertEquals($entity, $persister->loadOneToOneEntity(array(), 'foo', array('id' => 11))); + } + + public function testInvokeRefresh() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('refresh') + ->with($this->equalTo(array('id' => 1)), $this->equalTo($entity), $this->equalTo(0)) + ->will($this->returnValue($entity)); + + $this->assertNull($persister->refresh(array('id' => 1), $entity), 0); + } + + public function testInvokeLoadCriteria() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + $criteria = new Criteria(); + + $this->entityPersister->expects($this->once()) + ->method('loadCriteria') + ->with($this->equalTo($criteria)) + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->loadCriteria($criteria)); + } + + public function testInvokeGetManyToManyCollection() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('getManyToManyCollection') + ->with($this->equalTo(array()), $this->equalTo('Foo'), $this->equalTo(1), $this->equalTo(2)) + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->getManyToManyCollection(array(), 'Foo', 1 ,2)); + } + + public function testInvokeGetOneToManyCollection() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('getOneToManyCollection') + ->with($this->equalTo(array()), $this->equalTo('Foo'), $this->equalTo(1), $this->equalTo(2)) + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->getOneToManyCollection(array(), 'Foo', 1 ,2)); + } + + public function testInvokeLoadManyToManyCollection() + { + $mapping = $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\Country'); + $assoc = array('type' => 1); + $coll = new PersistentCollection($this->em, 'Foo', $mapping); + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('loadManyToManyCollection') + ->with($this->equalTo($assoc), $this->equalTo('Foo'), $coll) + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->loadManyToManyCollection($assoc, 'Foo', $coll)); + } + + public function testInvokeLoadOneToManyCollection() + { + $mapping = $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\Country'); + $assoc = array('type' => 1); + $coll = new PersistentCollection($this->em, 'Foo', $mapping); + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('loadOneToManyCollection') + ->with($this->equalTo($assoc), $this->equalTo('Foo'), $coll) + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->loadOneToManyCollection($assoc, 'Foo', $coll)); + } + + public function testInvokeLock() + { + $identifier = array('id' => 1); + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('lock') + ->with($this->equalTo($identifier), $this->equalTo(1)); + + $this->assertNull($persister->lock($identifier, 1)); + } + + public function testInvokeExists() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('exists') + ->with($this->equalTo($entity), $this->equalTo(array())); + + $this->assertNull($persister->exists($entity, array())); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersisterTest.php new file mode 100644 index 00000000000..2a2b626926d --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersisterTest.php @@ -0,0 +1,23 @@ +createPersisterDefault(); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + $persister->delete($entity); + + $this->assertCount(2, $property->getValue($persister)); + + $persister->afterTransactionRolledBack(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testInsertTransactionCommitShouldPutCache() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $entry = new EntityCacheEntry(Country::CLASSNAME, array('id'=>1, 'name'=>'Foo')); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('put') + ->with($this->equalTo($key), $this->equalTo($entry)); + + $this->entityPersister->expects($this->once()) + ->method('addInsert') + ->with($this->equalTo($entity)); + + $this->entityPersister->expects($this->once()) + ->method('getInserts') + ->will($this->returnValue(array($entity))); + + $this->entityPersister->expects($this->once()) + ->method('executeInserts'); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->addInsert($entity); + $persister->executeInserts(); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testUpdateTransactionCommitShouldPutCache() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $entry = new EntityCacheEntry(Country::CLASSNAME, array('id'=>1, 'name'=>'Foo')); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('put') + ->with($this->equalTo($key), $this->equalTo($entry)); + + $this->entityPersister->expects($this->once()) + ->method('update') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testDeleteTransactionCommitShouldEvictCache() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->entityPersister->expects($this->once()) + ->method('delete') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($entity); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/ReadOnlyCachedCollectionPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadOnlyCachedCollectionPersisterTest.php new file mode 100644 index 00000000000..d01a8f214b2 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadOnlyCachedCollectionPersisterTest.php @@ -0,0 +1,22 @@ +createPersisterDefault(); + $entity = new Country("Foo"); + + $persister->update($entity); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedCollectionPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedCollectionPersisterTest.php new file mode 100644 index 00000000000..4c70140661a --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedCollectionPersisterTest.php @@ -0,0 +1,301 @@ +getMock('Doctrine\ORM\Cache\ConcurrentRegion', $this->regionMockMethods); + } + + public function testDeleteShouldLockItem() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($collection); + } + + public function testUpdateShouldLockItem() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($collection); + } + + public function testUpdateTransactionRollBackShouldEvictItem() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($collection); + $persister->afterTransactionRolledBack(); + } + + public function testDeleteTransactionRollBackShouldEvictItem() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($collection); + $persister->afterTransactionRolledBack(); + } + + public function testTransactionRollBackDeleteShouldClearQueue() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($collection); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionRolledBack(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testTransactionRollBackUpdateShouldClearQueue() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($collection); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionRolledBack(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testTransactionRollCommitDeleteShouldClearQueue() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($collection); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testTransactionRollCommitUpdateShouldClearQueue() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($collection); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testDeleteLockFailureShouldIgnoreQueue() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue(null)); + + $this->collectionPersister->expects($this->once()) + ->method('delete') + ->with($this->equalTo($collection)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($collection); + $this->assertCount(0, $property->getValue($persister)); + } + + public function testUpdateLockFailureShouldIgnoreQueue() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue(null)); + + $this->collectionPersister->expects($this->once()) + ->method('update') + ->with($this->equalTo($collection)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($collection); + $this->assertCount(0, $property->getValue($persister)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php new file mode 100644 index 00000000000..1b6ed6183b8 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php @@ -0,0 +1,234 @@ +getMock('Doctrine\ORM\Cache\ConcurrentRegion', $this->regionMockMethods); + } + + public function testDeleteShouldLockItem() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($entity); + } + + public function testUpdateShouldLockItem() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + } + + public function testUpdateTransactionRollBackShouldEvictItem() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + $persister->afterTransactionRolledBack(); + } + + public function testDeleteTransactionRollBackShouldEvictItem() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($entity); + $persister->afterTransactionRolledBack(); + } + + public function testTransactionRollBackShouldClearQueue() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->exactly(2)) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->exactly(2)) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + $persister->delete($entity); + + $this->assertCount(2, $property->getValue($persister)); + + $persister->afterTransactionRolledBack(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testTransactionlCommitShouldClearQueue() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->exactly(2)) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->exactly(2)) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + $persister->delete($entity); + + $this->assertCount(2, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testDeleteLockFailureShouldIgnoreQueue() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue(null)); + + $this->entityPersister->expects($this->once()) + ->method('delete') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($entity); + $this->assertCount(0, $property->getValue($persister)); + } + + public function testUpdateLockFailureShouldIgnoreQueue() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue(null)); + + $this->entityPersister->expects($this->once()) + ->method('update') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + $this->assertCount(0, $property->getValue($persister)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/ConfigurationTest.php b/tests/Doctrine/Tests/ORM/ConfigurationTest.php index 8405c30358b..1530a05a356 100644 --- a/tests/Doctrine/Tests/ORM/ConfigurationTest.php +++ b/tests/Doctrine/Tests/ORM/ConfigurationTest.php @@ -272,6 +272,21 @@ public function testSetGetEntityListenerResolver() $this->configuration->setEntityListenerResolver($resolver); $this->assertSame($resolver, $this->configuration->getEntityListenerResolver()); } + + /** + * @group DDC-2183 + */ + public function testSetGetSecondLevelCacheClassName() + { + $mockClass = get_class($this->getMock('Doctrine\ORM\Cache')); + + $this->assertEquals('Doctrine\ORM\Cache\DefaultCache', $this->configuration->getSecondLevelCacheClassName()); + $this->configuration->setSecondLevelCacheClassName($mockClass); + $this->assertEquals($mockClass, $this->configuration->getSecondLevelCacheClassName()); + + $this->setExpectedException('Doctrine\ORM\ORMException'); + $this->configuration->setSecondLevelCacheClassName(__CLASS__); + } } class ConfigurationTestAnnotationReaderChecker diff --git a/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php b/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php index 323d8bf4ab5..dc148707c37 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php @@ -23,6 +23,9 @@ protected function setUp() { } } + /** + * @group non-cacheable + */ public function testSimpleDetachMerge() { $user = new DefaultValueUser; $user->name = 'romanb'; diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index eb4dbe4d877..f6c8160372c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -58,6 +58,7 @@ public function tearDown() /** * @group DDC-546 + * @group non-cacheable */ public function testCountNotInitializesCollection() { @@ -93,6 +94,7 @@ public function testCountWhenNewEntityPresent() /** * @group DDC-546 + * @group non-cacheable */ public function testCountWhenInitialized() { @@ -143,6 +145,7 @@ public function testFullSlice() /** * @group DDC-546 + * @group non-cacheable */ public function testSlice() { @@ -173,6 +176,7 @@ public function testSlice() /** * @group DDC-546 + * @group non-cacheable */ public function testSliceInitializedCollection() { @@ -505,6 +509,7 @@ public function testCountAfterAddThenFlush() /** * @group DDC-1462 + * @group non-cacheable */ public function testSliceOnDirtyCollection() { @@ -526,6 +531,7 @@ public function testSliceOnDirtyCollection() /** * @group DDC-1398 + * @group non-cacheable */ public function testGetIndexByIdentifier() { diff --git a/tests/Doctrine/Tests/ORM/Functional/JoinedTableCompositeKeyTest.php b/tests/Doctrine/Tests/ORM/Functional/JoinedTableCompositeKeyTest.php index 3bc5e25adb8..8f6e0cc59fd 100644 --- a/tests/Doctrine/Tests/ORM/Functional/JoinedTableCompositeKeyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/JoinedTableCompositeKeyTest.php @@ -30,7 +30,7 @@ public function testInsertWithCompositeKey() } /** - * + * @group non-cacheable */ public function testUpdateWithCompositeKey() { diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index db62fa1baec..ad00c3993c0 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -117,6 +117,9 @@ public function testEagerLoadManyToOne() $this->assertNotNull($waggon->train); } + /** + * @group non-cacheable + */ public function testEagerLoadWithNullableColumnsGeneratesLeftJoinOnBothSides() { $train = new Train(new TrainOwner("Alexander")); @@ -141,6 +144,9 @@ public function testEagerLoadWithNullableColumnsGeneratesLeftJoinOnBothSides() ); } + /** + * @group non-cacheable + */ public function testEagerLoadWithNonNullableColumnsGeneratesInnerJoinOnOwningSide() { $waggon = new Waggon(); @@ -168,6 +174,9 @@ public function testEagerLoadWithNonNullableColumnsGeneratesInnerJoinOnOwningSid ); } + /** + * @group non-cacheable + */ public function testEagerLoadWithNonNullableColumnsGeneratesLeftJoinOnNonOwningSide() { $owner = new TrainOwner('Alexander'); diff --git a/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php b/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php index 1f94d86a3ba..4e9390cc6f9 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php @@ -31,6 +31,8 @@ * Tests SQLFilter functionality. * * @author Alexander + * + * @group non-cacheable */ class SQLFilterTest extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php new file mode 100644 index 00000000000..fa8e20b531a --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php @@ -0,0 +1,205 @@ +enableSecondLevelCache(); + + $this->useModelSet('cache'); + + parent::setUp(); + + $this->cache = $this->_em->getCache(); + } + + protected function loadFixturesCountries() + { + $brazil = new Country("Brazil"); + $germany = new Country("Germany"); + + $this->countries[] = $brazil; + $this->countries[] = $germany; + + $this->_em->persist($brazil); + $this->_em->persist($germany); + $this->_em->flush(); + } + + protected function loadFixturesStates() + { + $saopaulo = new State("São Paulo", $this->countries[0]); + $rio = new State("Rio de janeiro", $this->countries[0]); + $berlin = new State("Berlin", $this->countries[1]); + $bavaria = new State("Bavaria", $this->countries[1]); + + $this->states[] = $saopaulo; + $this->states[] = $rio; + $this->states[] = $bavaria; + $this->states[] = $berlin; + + $this->_em->persist($saopaulo); + $this->_em->persist($rio); + $this->_em->persist($bavaria); + $this->_em->persist($berlin); + + $this->_em->flush(); + } + + protected function loadFixturesCities() + { + $saopaulo = new City("São Paulo", $this->states[0]); + $rio = new City("Rio de janeiro", $this->states[0]); + $berlin = new City("Berlin", $this->states[1]); + $munich = new City("Munich", $this->states[1]); + + $this->states[0]->addCity($saopaulo); + $this->states[0]->addCity($rio); + $this->states[1]->addCity($berlin); + $this->states[1]->addCity($berlin); + + $this->cities[] = $saopaulo; + $this->cities[] = $rio; + $this->cities[] = $munich; + $this->cities[] = $berlin; + + $this->_em->persist($saopaulo); + $this->_em->persist($rio); + $this->_em->persist($munich); + $this->_em->persist($berlin); + + $this->_em->flush(); + } + + protected function loadFixturesTraveler() + { + $t1 = new Traveler("Fabio Silva"); + $t2 = new Traveler("Doctrine Bot"); + + $this->_em->persist($t1); + $this->_em->persist($t2); + + $this->travelers[] = $t1; + $this->travelers[] = $t2; + + $this->_em->flush(); + } + + protected function loadFixturesTravels() + { + $t1 = new Travel($this->travelers[0]); + $t2 = new Travel($this->travelers[1]); + + $t1->addVisitedCity($this->cities[0]); + $t1->addVisitedCity($this->cities[1]); + $t1->addVisitedCity($this->cities[2]); + + $t2->addVisitedCity($this->cities[1]); + $t2->addVisitedCity($this->cities[3]); + + $this->_em->persist($t1); + $this->_em->persist($t2); + + $this->travels[] = $t1; + $this->travels[] = $t2; + + $this->_em->flush(); + } + + protected function loadFixturesAttractions() + { + $this->attractions[] = new Bar('Boteco São Bento', $this->cities[0]); + $this->attractions[] = new Bar('Prainha Paulista', $this->cities[0]); + $this->attractions[] = new Beach('Copacabana', $this->cities[1]); + $this->attractions[] = new Beach('Ipanema', $this->cities[1]); + $this->attractions[] = new Bar('Schneider Weisse', $this->cities[2]); + $this->attractions[] = new Restaurant('Reinstoff', $this->cities[3]); + $this->attractions[] = new Restaurant('Fischers Fritz', $this->cities[3]); + + $this->cities[0]->addAttraction($this->attractions[0]); + $this->cities[0]->addAttraction($this->attractions[1]); + $this->cities[1]->addAttraction($this->attractions[2]); + $this->cities[1]->addAttraction($this->attractions[3]); + $this->cities[2]->addAttraction($this->attractions[4]); + $this->cities[3]->addAttraction($this->attractions[5]); + $this->cities[3]->addAttraction($this->attractions[6]); + + foreach ($this->attractions as $attraction) { + $this->_em->persist($attraction); + } + + $this->_em->flush(); + } + + protected function loadFixturesAttractionsInfo() + { + $this->attractionsInfo[] = new AttractionContactInfo('0000-0000', $this->attractions[0]); + $this->attractionsInfo[] = new AttractionContactInfo('1111-1111', $this->attractions[1]); + $this->attractionsInfo[] = new AttractionLocationInfo('Some St 1', $this->attractions[2]); + $this->attractionsInfo[] = new AttractionLocationInfo('Some St 2', $this->attractions[3]); + + foreach ($this->attractionsInfo as $info) { + $this->_em->persist($info); + } + + $this->_em->flush(); + } + + protected function getEntityRegion($className) + { + return $this->cache->getEntityCacheRegion($className)->getName(); + } + + protected function getCollectionRegion($className, $association) + { + return $this->cache->getCollectionCacheRegion($className, $association)->getName(); + } + + protected function getDefaultQueryRegionName() + { + return $this->cache->getQueryCache()->getRegion()->getName(); + } + + protected function evictRegions() + { + $this->cache->evictQueryRegions(); + $this->cache->evictEntityRegions(); + $this->cache->evictCollectionRegions(); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php new file mode 100644 index 00000000000..5c58b0aaa00 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php @@ -0,0 +1,176 @@ +loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->_em->clear(); + $this->evictRegions(); + + $leavingFromId = $this->cities[0]->getId(); + $goingToId = $this->cities[1]->getId(); + $leavingFrom = $this->_em->find(City::CLASSNAME, $leavingFromId); + $goingTo = $this->_em->find(City::CLASSNAME, $goingToId); + $flight = new Flight($leavingFrom, $goingTo); + $id = array( + 'leavingFrom' => $leavingFromId, + 'goingTo' => $goingToId, + ); + + $flight->setDeparture(new \DateTime('tomorrow')); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $this->_em->persist($flight); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Flight::CLASSNAME, $id)); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $flight = $this->_em->find(Flight::CLASSNAME, $id); + $leavingFrom = $flight->getLeavingFrom(); + $goingTo = $flight->getGoingTo(); + + $this->assertInstanceOf(Flight::CLASSNAME, $flight); + $this->assertInstanceOf(City::CLASSNAME, $goingTo); + $this->assertInstanceOf(City::CLASSNAME, $leavingFrom); + + $this->assertEquals($goingTo->getId(), $goingToId); + $this->assertEquals($leavingFrom->getId(), $leavingFromId); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + + public function testRemoveCompositPrimaryKeyEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->_em->clear(); + $this->evictRegions(); + + $leavingFromId = $this->cities[0]->getId(); + $goingToId = $this->cities[1]->getId(); + $leavingFrom = $this->_em->find(City::CLASSNAME, $leavingFromId); + $goingTo = $this->_em->find(City::CLASSNAME, $goingToId); + $flight = new Flight($leavingFrom, $goingTo); + $id = array( + 'leavingFrom' => $leavingFromId, + 'goingTo' => $goingToId, + ); + + $flight->setDeparture(new \DateTime('tomorrow')); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $this->_em->persist($flight); + $this->_em->flush(); + + $this->assertTrue($this->cache->containsEntity(Flight::CLASSNAME, $id)); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $this->_em->remove($flight); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Flight::CLASSNAME, $id)); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $this->assertNull($this->_em->find(Flight::CLASSNAME, $id)); + } + + public function testUpdateCompositPrimaryKeyEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->_em->clear(); + $this->evictRegions(); + + $now = new \DateTime('now'); + $tomorrow = new \DateTime('tomorrow'); + $leavingFromId = $this->cities[0]->getId(); + $goingToId = $this->cities[1]->getId(); + $leavingFrom = $this->_em->find(City::CLASSNAME, $leavingFromId); + $goingTo = $this->_em->find(City::CLASSNAME, $goingToId); + $flight = new Flight($leavingFrom, $goingTo); + $id = array( + 'leavingFrom' => $leavingFromId, + 'goingTo' => $goingToId, + ); + + $flight->setDeparture($now); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $this->_em->persist($flight); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Flight::CLASSNAME, $id)); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $flight = $this->_em->find(Flight::CLASSNAME, $id); + $leavingFrom = $flight->getLeavingFrom(); + $goingTo = $flight->getGoingTo(); + + $this->assertInstanceOf(Flight::CLASSNAME, $flight); + $this->assertInstanceOf(City::CLASSNAME, $goingTo); + $this->assertInstanceOf(City::CLASSNAME, $leavingFrom); + + $this->assertEquals($goingTo->getId(), $goingToId); + $this->assertEquals($flight->getDeparture(), $now); + $this->assertEquals($leavingFrom->getId(), $leavingFromId); + $this->assertEquals($leavingFrom->getId(), $leavingFromId); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $flight->setDeparture($tomorrow); + + $this->_em->persist($flight); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Flight::CLASSNAME, $id)); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $flight = $this->_em->find(Flight::CLASSNAME, $id); + $leavingFrom = $flight->getLeavingFrom(); + $goingTo = $flight->getGoingTo(); + + $this->assertInstanceOf(Flight::CLASSNAME, $flight); + $this->assertInstanceOf(City::CLASSNAME, $goingTo); + $this->assertInstanceOf(City::CLASSNAME, $leavingFrom); + + $this->assertEquals($goingTo->getId(), $goingToId); + $this->assertEquals($flight->getDeparture(), $tomorrow); + $this->assertEquals($leavingFrom->getId(), $leavingFromId); + $this->assertEquals($leavingFrom->getId(), $leavingFromId); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php new file mode 100644 index 00000000000..ed35992c8eb --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php @@ -0,0 +1,139 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->cacheFactory = new CacheFactorySecondLevelCacheConcurrentTest($this->getSharedSecondLevelCacheDriverImpl()); + + $this->_em->getConfiguration()->setSecondLevelCacheFactory($this->cacheFactory); + + $this->countryMetadata = $this->_em->getClassMetadata(Country::CLASSNAME); + $countryMetadata = clone $this->countryMetadata; + + $countryMetadata->cache['usage'] = ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE; + + $this->_em->getMetadataFactory()->setMetadataFor(Country::CLASSNAME, $countryMetadata); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->_em->getMetadataFactory()->setMetadataFor(Country::CLASSNAME, $this->countryMetadata); + } + + public function testBasicConcurrentEntityReadLock() + { + $this->loadFixturesCountries(); + $this->_em->clear(); + + $countryId = $this->countries[0]->getId(); + $cacheId = new EntityCacheKey(Country::CLASSNAME, array('id'=>$countryId)); + $region = $this->_em->getCache()->getEntityCacheRegion(Country::CLASSNAME); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $countryId)); + + /** @var \Doctrine\Tests\Mocks\ConcurrentRegionMock */ + $region->setLock($cacheId, Lock::createLockRead()); // another proc lock the entity cache + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $countryId)); + + $queryCount = $this->getCurrentQueryCount(); + $country = $this->_em->find(Country::CLASSNAME, $countryId); + + $this->assertInstanceOf(Country::CLASSNAME, $country); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $countryId)); + } + + public function testBasicConcurrentCollectionReadLock() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->_em->clear(); + $this->evictRegions(); + + $stateId = $this->states[0]->getId(); + $state = $this->_em->find(State::CLASSNAME, $stateId); + + $this->assertInstanceOf(State::CLASSNAME, $state); + $this->assertCount(2, $state->getCities()); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $stateId = $this->states[0]->getId(); + $cacheId = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>$stateId)); + $region = $this->_em->getCache()->getCollectionCacheRegion(State::CLASSNAME, 'cities'); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, 'cities', $stateId)); + + /* @var $region \Doctrine\Tests\Mocks\ConcurrentRegionMock */ + $region->setLock($cacheId, Lock::createLockRead()); // another proc lock the entity cache + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $stateId)); + + $queryCount = $this->getCurrentQueryCount(); + $state = $this->_em->find(State::CLASSNAME, $stateId); + + $this->assertEquals(0, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + + $this->assertEquals(0, $this->secondLevelCacheLogger->getRegionMissCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + + $this->assertInstanceOf(State::CLASSNAME, $state); + $this->assertCount(2, $state->getCities()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $stateId)); + } +} + +class CacheFactorySecondLevelCacheConcurrentTest extends \Doctrine\ORM\Cache\DefaultCacheFactory +{ + public function __construct(Cache $cache) + { + $this->cache = $cache; + } + + public function getRegion(array $cache) + { + $region = new DefaultRegion($cache['region'], $this->cache); + $mock = new ConcurrentRegionMock($region); + + return $mock; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php new file mode 100644 index 00000000000..b96c41e1f26 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php @@ -0,0 +1,82 @@ +_em->getClassMetadata(Travel::CLASSNAME); + $targetEntity = $this->_em->getClassMetadata(City::CLASSNAME); + + $sourceEntity->associationMappings['visitedCities']['fetch'] = ClassMetadata::FETCH_EXTRA_LAZY; + $targetEntity->associationMappings['travels']['fetch'] = ClassMetadata::FETCH_EXTRA_LAZY; + } + + public function tearDown() + { + parent::tearDown(); + + $sourceEntity = $this->_em->getClassMetadata(Travel::CLASSNAME); + $targetEntity = $this->_em->getClassMetadata(City::CLASSNAME); + + $sourceEntity->associationMappings['visitedCities']['fetch'] = ClassMetadata::FETCH_LAZY; + $targetEntity->associationMappings['travels']['fetch'] = ClassMetadata::FETCH_LAZY; + } + + public function testCacheCountAfterAddThenFlush() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTraveler(); + $this->loadFixturesTravels(); + + $this->_em->clear(); + + $ownerId = $this->travels[0]->getId(); + $owner = $this->_em->find(Travel::CLASSNAME, $ownerId); + $ref = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $ownerId)); + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $ownerId)); + + $newItem = new City("New City", $ref); + $owner->getVisitedCities()->add($newItem); + + $this->_em->persist($newItem); + $this->_em->persist($owner); + + $queryCount = $this->getCurrentQueryCount(); + + $this->assertFalse($owner->getVisitedCities()->isInitialized()); + $this->assertEquals(4, $owner->getVisitedCities()->count()); + $this->assertFalse($owner->getVisitedCities()->isInitialized()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->_em->flush(); + + $this->assertFalse($owner->getVisitedCities()->isInitialized()); + $this->assertFalse($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $ownerId)); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $owner = $this->_em->find(Travel::CLASSNAME, $ownerId); + + $this->assertEquals(4, $owner->getVisitedCities()->count()); + $this->assertFalse($owner->getVisitedCities()->isInitialized()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php new file mode 100644 index 00000000000..b063cd319cb --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php @@ -0,0 +1,192 @@ +cache->getEntityCacheRegion(AttractionInfo::CLASSNAME); + $contactRegion = $this->cache->getEntityCacheRegion(AttractionContactInfo::CLASSNAME); + $locationRegion = $this->cache->getEntityCacheRegion(AttractionLocationInfo::CLASSNAME); + + $this->assertEquals($infoRegion->getName(), $contactRegion->getName()); + $this->assertEquals($infoRegion->getName(), $locationRegion->getName()); + } + + public function testPutOnPersistJoinTableInheritance() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->loadFixturesAttractionsInfo(); + + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $this->attractionsInfo[0]->getId())); + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $this->attractionsInfo[1]->getId())); + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $this->attractionsInfo[2]->getId())); + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $this->attractionsInfo[3]->getId())); + } + + public function testJoinTableCountaisRootClass() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->loadFixturesAttractionsInfo(); + + $this->_em->clear(); + + foreach ($this->attractionsInfo as $info) { + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $info->getId())); + $this->assertTrue($this->cache->containsEntity(get_class($info), $info->getId())); + } + } + + public function testPutAndLoadJoinTableEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->loadFixturesAttractionsInfo(); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(AttractionInfo::CLASSNAME); + + $entityId1 = $this->attractionsInfo[0]->getId(); + $entityId2 = $this->attractionsInfo[1]->getId(); + + $this->assertFalse($this->cache->containsEntity(AttractionInfo::CLASSNAME, $entityId1)); + $this->assertFalse($this->cache->containsEntity(AttractionInfo::CLASSNAME, $entityId2)); + $this->assertFalse($this->cache->containsEntity(AttractionContactInfo::CLASSNAME, $entityId1)); + $this->assertFalse($this->cache->containsEntity(AttractionContactInfo::CLASSNAME, $entityId2)); + + $queryCount = $this->getCurrentQueryCount(); + $entity1 = $this->_em->find(AttractionInfo::CLASSNAME, $entityId1); + $entity2 = $this->_em->find(AttractionInfo::CLASSNAME, $entityId2); + + //load entity and relation whit sub classes + $this->assertEquals($queryCount + 4, $this->getCurrentQueryCount()); + + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $entityId1)); + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $entityId2)); + $this->assertTrue($this->cache->containsEntity(AttractionContactInfo::CLASSNAME, $entityId1)); + $this->assertTrue($this->cache->containsEntity(AttractionContactInfo::CLASSNAME, $entityId2)); + + $this->assertInstanceOf(AttractionInfo::CLASSNAME, $entity1); + $this->assertInstanceOf(AttractionInfo::CLASSNAME, $entity2); + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity1); + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity2); + + $this->assertEquals($this->attractionsInfo[0]->getId(), $entity1->getId()); + $this->assertEquals($this->attractionsInfo[0]->getFone(), $entity1->getFone()); + + $this->assertEquals($this->attractionsInfo[1]->getId(), $entity2->getId()); + $this->assertEquals($this->attractionsInfo[1]->getFone(), $entity2->getFone()); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $entity3 = $this->_em->find(AttractionInfo::CLASSNAME, $entityId1); + $entity4 = $this->_em->find(AttractionInfo::CLASSNAME, $entityId2); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(AttractionInfo::CLASSNAME, $entity3); + $this->assertInstanceOf(AttractionInfo::CLASSNAME, $entity4); + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity3); + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity4); + + $this->assertNotSame($entity1, $entity3); + $this->assertEquals($entity1->getId(), $entity3->getId()); + $this->assertEquals($entity1->getFone(), $entity3->getFone()); + + $this->assertNotSame($entity2, $entity4); + $this->assertEquals($entity2->getId(), $entity4->getId()); + $this->assertEquals($entity2->getFone(), $entity4->getFone()); + } + + public function testQueryCacheFindAllJoinTableEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->loadFixturesAttractionsInfo(); + $this->evictRegions(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT i, a FROM Doctrine\Tests\Models\Cache\AttractionInfo i JOIN i.attraction a'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(count($this->attractionsInfo), $result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(count($this->attractionsInfo), $result2); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + foreach ($result2 as $entity) { + $this->assertInstanceOf(AttractionInfo::CLASSNAME, $entity); + } + } + + public function testOneToManyRelationJoinTable() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->loadFixturesAttractionsInfo(); + $this->evictRegions(); + $this->_em->clear(); + + $entity = $this->_em->find(Attraction::CLASSNAME, $this->attractions[0]->getId()); + + $this->assertInstanceOf(Attraction::CLASSNAME, $entity); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $entity->getInfos()); + $this->assertCount(1, $entity->getInfos()); + + $ownerId = $this->attractions[0]->getId(); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $ownerId)); + $this->assertTrue($this->cache->containsCollection(Attraction::CLASSNAME, 'infos', $ownerId)); + + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity->getInfos()->get(0)); + $this->assertEquals($this->attractionsInfo[0]->getFone(), $entity->getInfos()->get(0)->getFone()); + + $this->_em->clear(); + + $entity = $this->_em->find(Attraction::CLASSNAME, $this->attractions[0]->getId()); + + $this->assertInstanceOf(Attraction::CLASSNAME, $entity); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $entity->getInfos()); + $this->assertCount(1, $entity->getInfos()); + + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity->getInfos()->get(0)); + $this->assertEquals($this->attractionsInfo[0]->getFone(), $entity->getInfos()->get(0)->getFone()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php new file mode 100644 index 00000000000..9b1b993a0af --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php @@ -0,0 +1,214 @@ +evictRegions(); + + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTraveler(); + $this->loadFixturesTravels(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[1]->getId())); + + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[0]->getId())); + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[1]->getId())); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[2]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[3]->getId())); + } + + public function testPutAndLoadManyToManyRelation() + { + $this->evictRegions(); + + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTraveler(); + $this->loadFixturesTravels(); + + $this->_em->clear(); + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + + $this->assertFalse($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[1]->getId())); + + $this->assertFalse($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[0]->getId())); + $this->assertFalse($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[1]->getId())); + + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->cities[2]->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->cities[3]->getId())); + + $t1 = $this->_em->find(Travel::CLASSNAME, $this->travels[0]->getId()); + $t2 = $this->_em->find(Travel::CLASSNAME, $this->travels[1]->getId()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Travel::CLASSNAME))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getEntityRegion(Travel::CLASSNAME))); + + //trigger lazy load + $this->assertCount(3, $t1->getVisitedCities()); + $this->assertCount(2, $t2->getVisitedCities()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(4, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getCollectionRegion(Travel::CLASSNAME, 'visitedCities'))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getCollectionRegion(Travel::CLASSNAME, 'visitedCities'))); + + $this->assertInstanceOf(City::CLASSNAME, $t1->getVisitedCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $t1->getVisitedCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $t1->getVisitedCities()->get(2)); + + $this->assertInstanceOf(City::CLASSNAME, $t2->getVisitedCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $t2->getVisitedCities()->get(1)); + + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[1]->getId())); + + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[0]->getId())); + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[1]->getId())); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[2]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[3]->getId())); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + + $t3 = $this->_em->find(Travel::CLASSNAME, $this->travels[0]->getId()); + $t4 = $this->_em->find(Travel::CLASSNAME, $this->travels[1]->getId()); + + //trigger lazy load from cache + $this->assertCount(3, $t3->getVisitedCities()); + $this->assertCount(2, $t4->getVisitedCities()); + + $this->assertInstanceOf(City::CLASSNAME, $t3->getVisitedCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $t3->getVisitedCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $t3->getVisitedCities()->get(2)); + + $this->assertInstanceOf(City::CLASSNAME, $t4->getVisitedCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $t4->getVisitedCities()->get(1)); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(Travel::CLASSNAME))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getCollectionRegion(Travel::CLASSNAME, 'visitedCities'))); + + $this->assertNotSame($t1->getVisitedCities()->get(0), $t3->getVisitedCities()->get(0)); + $this->assertEquals($t1->getVisitedCities()->get(0)->getId(), $t3->getVisitedCities()->get(0)->getId()); + $this->assertEquals($t1->getVisitedCities()->get(0)->getName(), $t3->getVisitedCities()->get(0)->getName()); + + $this->assertNotSame($t1->getVisitedCities()->get(1), $t3->getVisitedCities()->get(1)); + $this->assertEquals($t1->getVisitedCities()->get(1)->getId(), $t3->getVisitedCities()->get(1)->getId()); + $this->assertEquals($t1->getVisitedCities()->get(1)->getName(), $t3->getVisitedCities()->get(1)->getName()); + + $this->assertNotSame($t1->getVisitedCities()->get(2), $t3->getVisitedCities()->get(2)); + $this->assertEquals($t1->getVisitedCities()->get(2)->getId(), $t3->getVisitedCities()->get(2)->getId()); + $this->assertEquals($t1->getVisitedCities()->get(2)->getName(), $t3->getVisitedCities()->get(2)->getName()); + + $this->assertNotSame($t2->getVisitedCities()->get(0), $t4->getVisitedCities()->get(0)); + $this->assertEquals($t2->getVisitedCities()->get(0)->getId(), $t4->getVisitedCities()->get(0)->getId()); + $this->assertEquals($t2->getVisitedCities()->get(0)->getName(), $t4->getVisitedCities()->get(0)->getName()); + + $this->assertNotSame($t2->getVisitedCities()->get(1), $t4->getVisitedCities()->get(1)); + $this->assertEquals($t2->getVisitedCities()->get(1)->getId(), $t4->getVisitedCities()->get(1)->getId()); + $this->assertEquals($t2->getVisitedCities()->get(1)->getName(), $t4->getVisitedCities()->get(1)->getName()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + + public function testStoreManyToManyAssociationWhitCascade() + { + $this->evictRegions(); + + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->cache->evictEntityRegion(City::CLASSNAME); + $this->cache->evictEntityRegion(Traveler::CLASSNAME); + $this->cache->evictEntityRegion(Travel::CLASSNAME); + $this->cache->evictCollectionRegion(State::CLASSNAME, 'cities'); + $this->cache->evictCollectionRegion(Traveler::CLASSNAME, 'travels'); + + $traveler = new Traveler('Doctrine Bot'); + $travel = new Travel($traveler); + + $travel->addVisitedCity($this->cities[0]); + $travel->addVisitedCity($this->cities[1]); + $travel->addVisitedCity($this->cities[3]); + + $this->_em->persist($traveler); + $this->_em->persist($travel); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $travel->getId())); + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $traveler->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[3]->getId())); + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $travel->getId())); + + $queryCount1 = $this->getCurrentQueryCount(); + $t1 = $this->_em->find(Travel::CLASSNAME, $travel->getId()); + + $this->assertInstanceOf(Travel::CLASSNAME, $t1); + $this->assertCount(3, $t1->getVisitedCities()); + $this->assertEquals($queryCount1, $this->getCurrentQueryCount()); + } + + /** + * @expectedException \Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Cannot update a readonly collection "Doctrine\Tests\Models\Cache\Travel#visitedCities + */ + public function testReadOnlyCollection() + { + $this->evictRegions(); + + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTraveler(); + $this->loadFixturesTravels(); + + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[0]->getId())); + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[0]->getId())); + + $travel = $this->_em->find(Travel::CLASSNAME, $this->travels[0]->getId()); + + $this->assertCount(3, $travel->getVisitedCities()); + + $travel->getVisitedCities()->remove(0); + + $this->_em->persist($travel); + $this->_em->flush(); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php new file mode 100644 index 00000000000..c271434be33 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php @@ -0,0 +1,121 @@ +loadFixturesCountries(); + $this->loadFixturesStates(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[0]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[1]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + } + + public function testPutAndLoadManyToOneRelation() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->_em->clear(); + + $this->cache->evictEntityRegion(State::CLASSNAME); + $this->cache->evictEntityRegion(Country::CLASSNAME); + + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->states[0]->getCountry()->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->states[1]->getCountry()->getId())); + + $c1 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $c2 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + //trigger lazy load + $this->assertNotNull($c1->getCountry()->getName()); + $this->assertNotNull($c2->getCountry()->getName()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[0]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[1]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertInstanceOf(State::CLASSNAME, $c1); + $this->assertInstanceOf(State::CLASSNAME, $c2); + $this->assertInstanceOf(Country::CLASSNAME, $c1->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $c2->getCountry()); + + $this->assertEquals($this->states[0]->getId(), $c1->getId()); + $this->assertEquals($this->states[0]->getName(), $c1->getName()); + $this->assertEquals($this->states[0]->getCountry()->getId(), $c1->getCountry()->getId()); + $this->assertEquals($this->states[0]->getCountry()->getName(), $c1->getCountry()->getName()); + + $this->assertEquals($this->states[1]->getId(), $c2->getId()); + $this->assertEquals($this->states[1]->getName(), $c2->getName()); + $this->assertEquals($this->states[1]->getCountry()->getId(), $c2->getCountry()->getId()); + $this->assertEquals($this->states[1]->getCountry()->getName(), $c2->getCountry()->getName()); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $c3 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $c4 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + //trigger lazy load from cache + $this->assertNotNull($c3->getCountry()->getName()); + $this->assertNotNull($c4->getCountry()->getName()); + + $this->assertInstanceOf(State::CLASSNAME, $c3); + $this->assertInstanceOf(State::CLASSNAME, $c4); + $this->assertInstanceOf(Country::CLASSNAME, $c3->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $c4->getCountry()); + + $this->assertEquals($c1->getId(), $c3->getId()); + $this->assertEquals($c1->getName(), $c3->getName()); + + $this->assertEquals($c2->getId(), $c4->getId()); + $this->assertEquals($c2->getName(), $c4->getName()); + + $this->assertEquals($this->states[0]->getCountry()->getId(), $c3->getCountry()->getId()); + $this->assertEquals($this->states[0]->getCountry()->getName(), $c3->getCountry()->getName()); + + $this->assertEquals($this->states[1]->getCountry()->getId(), $c4->getCountry()->getId()); + $this->assertEquals($this->states[1]->getCountry()->getName(), $c4->getCountry()->getName()); + } + + public function testLoadFromDatabaseWhenAssociationIsMissing() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[0]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[1]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->states[0]->getCountry()->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->states[1]->getCountry()->getId())); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $state1 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $state2 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php new file mode 100644 index 00000000000..931ba88ba17 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php @@ -0,0 +1,351 @@ +loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[0]->getId())); + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[1]->getId())); + } + + public function testPutAndLoadOneToManyRelation() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $this->cache->evictEntityRegion(State::CLASSNAME); + $this->cache->evictEntityRegion(City::CLASSNAME); + $this->cache->evictCollectionRegion(State::CLASSNAME, 'cities'); + + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[0]->getId())); + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[1]->getId())); + + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(0)->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(1)->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[1]->getCities()->get(0)->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[1]->getCities()->get(1)->getId())); + + $s1 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $s2 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getEntityRegion(State::CLASSNAME))); + + //trigger lazy load + $this->assertCount(2, $s1->getCities()); + $this->assertCount(2, $s2->getCities()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(4, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $this->assertInstanceOf(City::CLASSNAME, $s1->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $s1->getCities()->get(1)); + + $this->assertInstanceOf(City::CLASSNAME, $s2->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $s2->getCities()->get(1)); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[0]->getId())); + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[1]->getId())); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(0)->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(1)->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[1]->getCities()->get(0)->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[1]->getCities()->get(1)->getId())); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + + $s3 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $s4 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + //trigger lazy load from cache + $this->assertCount(2, $s3->getCities()); + $this->assertCount(2, $s4->getCities()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $this->assertInstanceOf(City::CLASSNAME, $s3->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $s3->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $s4->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $s4->getCities()->get(1)); + + $this->assertNotSame($s1->getCities()->get(0), $s3->getCities()->get(0)); + $this->assertEquals($s1->getCities()->get(0)->getId(), $s3->getCities()->get(0)->getId()); + $this->assertEquals($s1->getCities()->get(0)->getName(), $s3->getCities()->get(0)->getName()); + + $this->assertNotSame($s1->getCities()->get(1), $s3->getCities()->get(1)); + $this->assertEquals($s1->getCities()->get(1)->getId(), $s3->getCities()->get(1)->getId()); + $this->assertEquals($s1->getCities()->get(1)->getName(), $s3->getCities()->get(1)->getName()); + + $this->assertNotSame($s2->getCities()->get(0), $s4->getCities()->get(0)); + $this->assertEquals($s2->getCities()->get(0)->getId(), $s4->getCities()->get(0)->getId()); + $this->assertEquals($s2->getCities()->get(0)->getName(), $s4->getCities()->get(0)->getName()); + + $this->assertNotSame($s2->getCities()->get(1), $s4->getCities()->get(1)); + $this->assertEquals($s2->getCities()->get(1)->getId(), $s4->getCities()->get(1)->getId()); + $this->assertEquals($s2->getCities()->get(1)->getName(), $s4->getCities()->get(1)->getName()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + + public function testLoadOnoToManyCollectionFromDatabaseWhenEntityMissing() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + + //trigger lazy load from database + $this->assertCount(2, $this->_em->find(State::CLASSNAME, $this->states[0]->getId())->getCities()); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(0)->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(1)->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $stateId = $this->states[0]->getId(); + $state = $this->_em->find(State::CLASSNAME, $stateId); + $cityId = $this->states[0]->getCities()->get(1)->getId(); + + //trigger lazy load from cache + $this->assertCount(2, $state->getCities()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $cityId)); + + $this->cache->evictEntity(City::CLASSNAME, $cityId); + + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $cityId)); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $stateId)); + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, 'cities', $stateId)); + + $this->_em->clear(); + + $state = $this->_em->find(State::CLASSNAME, $stateId); + + //trigger lazy load from database + $this->assertCount(2, $state->getCities()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + + + public function testShoudNotPutOneToManyRelationOnPersist() + { + $this->loadFixturesCountries(); + $this->evictRegions(); + + $state = new State("State Foo", $this->countries[0]); + + $this->_em->persist($state); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $state->getId())); + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $state->getId())); + } + + public function testOneToManyRemove() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + $this->evictRegions(); + + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[0]->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(0)->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(1)->getId())); + + $entity = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getEntityRegion(State::CLASSNAME))); + + //trigger lazy load + $this->assertCount(2, $entity->getCities()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $this->assertInstanceOf(City::CLASSNAME, $entity->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $entity->getCities()->get(1)); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + $state = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + + //trigger lazy load from cache + $this->assertCount(2, $state->getCities()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $city0 = $state->getCities()->get(0); + $city1 = $state->getCities()->get(1); + + $this->assertInstanceOf(City::CLASSNAME, $city0); + $this->assertInstanceOf(City::CLASSNAME, $city1); + + $this->assertEquals($entity->getCities()->get(0)->getName(), $city0->getName()); + $this->assertEquals($entity->getCities()->get(1)->getName(), $city1->getName()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $state->getCities()->removeElement($city0); + + $this->_em->remove($city0); + $this->_em->persist($state); + $this->_em->flush(); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + $state = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + + //trigger lazy load from cache + $this->assertCount(1, $state->getCities()); + + $city1 = $state->getCities()->get(0); + $this->assertInstanceOf(City::CLASSNAME, $city1); + $this->assertEquals($entity->getCities()->get(1)->getName(), $city1->getName()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(0, $this->secondLevelCacheLogger->getRegionHitCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $state->getCities()->remove(0); + + $this->_em->remove($city1); + $this->_em->persist($state); + $this->_em->flush(); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + $state = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + + $this->assertCount(0, $state->getCities()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(0, $this->secondLevelCacheLogger->getRegionHitCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + } + + public function testOneToManyCount() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->secondLevelCacheLogger->clearStats(); + $this->evictRegions(); + $this->_em->clear(); + + $entitiId = $this->states[0]->getId(); + $queryCount = $this->getCurrentQueryCount(); + $entity = $this->_em->find(State::CLASSNAME, $entitiId); + + $this->assertEquals(2, $entity->getCities()->count()); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $entity = $this->_em->find(State::CLASSNAME, $entitiId); + + $this->assertEquals(2, $entity->getCities()->count()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + + public function testCacheInitializeCollectionWithNewObjects() + { + $this->_em->clear(); + $this->evictRegions(); + + $traveler = new Traveler("Doctrine Bot"); + + for ($i=0; $i<3; ++$i) { + $traveler->getTravels()->add(new Travel($traveler)); + } + + $this->_em->persist($traveler); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertCount(3, $traveler->getTravels()); + + $travelerId = $traveler->getId(); + $queryCount = $this->getCurrentQueryCount(); + $entity = $this->_em->find(Traveler::CLASSNAME, $travelerId); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + $this->assertFalse($entity->getTravels()->isInitialized()); + + $newItem = new Travel($entity); + $entity->getTravels()->add($newItem); + + $this->assertFalse($entity->getTravels()->isInitialized()); + $this->assertCount(4, $entity->getTravels()); + $this->assertTrue($entity->getTravels()->isInitialized()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->_em->flush(); + $this->_em->clear(); + + $query = "SELECT t, tt FROM Doctrine\Tests\Models\Cache\Traveler t JOIN t.travels tt WHERE t.id = $travelerId"; + $result = $this->_em->createQuery($query)->getSingleResult(); + + $this->assertEquals(4, $result->getTravels()->count()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php new file mode 100644 index 00000000000..99703f2b664 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -0,0 +1,828 @@ +evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result1 = $this->_em->createQuery($dql)->setCacheable(true)->getResult(); + + $this->assertCount(2, $result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals($this->countries[0]->getId(), $result1[0]->getId()); + $this->assertEquals($this->countries[1]->getId(), $result1[1]->getId()); + $this->assertEquals($this->countries[0]->getName(), $result1[0]->getName()); + $this->assertEquals($this->countries[1]->getName(), $result1[1]->getName()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertCount(2, $result2); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[1]); + + $this->assertEquals($result1[0]->getId(), $result2[0]->getId()); + $this->assertEquals($result1[1]->getId(), $result2[1]->getId()); + + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + } + + public function testQueryCacheModeGet() + { + $this->evictRegions(); + $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())); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $queryGet = $this->_em->createQuery($dql) + ->setCacheMode(Cache::MODE_GET) + ->setCacheable(true); + + // MODE_GET should never add items to the cache. + $this->assertCount(2, $queryGet->getResult()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->assertCount(2, $queryGet->getResult()); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + + $result = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(2, $result); + $this->assertEquals($queryCount + 3, $this->getCurrentQueryCount()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + // MODE_GET should read items if exists. + $this->assertCount(2, $queryGet->getResult()); + $this->assertEquals($queryCount + 3, $this->getCurrentQueryCount()); + } + + public function testQueryCacheModePut() + { + $this->evictRegions(); + $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())); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertCount(2, $result); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $queryPut = $this->_em->createQuery($dql) + ->setCacheMode(Cache::MODE_PUT) + ->setCacheable(true); + + // MODE_PUT should never read itens from cache. + $this->assertCount(2, $queryPut->getResult()); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertCount(2, $queryPut->getResult()); + $this->assertEquals($queryCount + 3, $this->getCurrentQueryCount()); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + } + + public function testQueryCacheModeRefresh() + { + $this->evictRegions(); + $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())); + + $region = $this->cache->getEntityCacheRegion(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertCount(2, $result); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $countryId1 = $this->countries[0]->getId(); + $countryId2 = $this->countries[1]->getId(); + $countryName1 = $this->countries[0]->getName(); + $countryName2 = $this->countries[1]->getName(); + + $key1 = new EntityCacheKey(Country::CLASSNAME, array('id'=>$countryId1)); + $key2 = new EntityCacheKey(Country::CLASSNAME, array('id'=>$countryId2)); + $entry1 = new EntityCacheEntry(Country::CLASSNAME, array('id'=>$countryId1, 'name'=>'outdated')); + $entry2 = new EntityCacheEntry(Country::CLASSNAME, array('id'=>$countryId2, 'name'=>'outdated')); + + $region->put($key1, $entry1); + $region->put($key2, $entry2); + $this->_em->clear(); + + $queryRefresh = $this->_em->createQuery($dql) + ->setCacheMode(Cache::MODE_REFRESH) + ->setCacheable(true); + + // MODE_REFRESH should never read itens from cache. + $result1 = $queryRefresh->getResult(); + $this->assertCount(2, $result1); + $this->assertEquals($countryName1, $result1[0]->getName()); + $this->assertEquals($countryName2, $result1[1]->getName()); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + + $this->_em->clear(); + + $result2 = $queryRefresh->getResult(); + $this->assertCount(2, $result2); + $this->assertEquals($countryName1, $result2[0]->getName()); + $this->assertEquals($countryName2, $result2[1]->getName()); + $this->assertEquals($queryCount + 3, $this->getCurrentQueryCount()); + } + + public function testBasicQueryCachePutEntityCache() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result1 = $this->_em->createQuery($dql)->setCacheable(true)->getResult(); + + $this->assertCount(2, $result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals($this->countries[0]->getId(), $result1[0]->getId()); + $this->assertEquals($this->countries[1]->getId(), $result1[1]->getId()); + $this->assertEquals($this->countries[0]->getName(), $result1[0]->getName()); + $this->assertEquals($this->countries[1]->getName(), $result1[1]->getName()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertCount(2, $result2); + + $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[1]); + + $this->assertEquals($result1[0]->getId(), $result2[0]->getId()); + $this->assertEquals($result1[1]->getId(), $result2[1]->getId()); + + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); + + $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + } + + public function testBasicQueryParams() + { + $this->evictRegions(); + + $this->loadFixturesCountries(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $name = $this->countries[0]->getName(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c WHERE c.name = :name'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->setParameter('name', $name) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals($this->countries[0]->getId(), $result1[0]->getId()); + $this->assertEquals($this->countries[0]->getName(), $result1[0]->getName()); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql)->setCacheable(true) + ->setParameter('name', $name) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertCount(1, $result2); + + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[0]); + + $this->assertEquals($result1[0]->getId(), $result2[0]->getId()); + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + } + + public function testLoadFromDatabaseWhenEntityMissing() + { + $this->evictRegions(); + + $this->loadFixturesCountries(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result1 = $this->_em->createQuery($dql)->setCacheable(true)->getResult(); + + $this->assertCount(2, $result1); + $this->assertEquals($queryCount + 1 , $this->getCurrentQueryCount()); + $this->assertEquals($this->countries[0]->getId(), $result1[0]->getId()); + $this->assertEquals($this->countries[1]->getId(), $result1[1]->getId()); + $this->assertEquals($this->countries[0]->getName(), $result1[0]->getName()); + $this->assertEquals($this->countries[1]->getName(), $result1[1]->getName()); + + $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->cache->evictEntity(Country::CLASSNAME, $result1[0]->getId()); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $result1[0]->getId())); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 2 , $this->getCurrentQueryCount()); + $this->assertCount(2, $result2); + + $this->assertEquals(5, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[1]); + + $this->assertEquals($result1[0]->getId(), $result2[0]->getId()); + $this->assertEquals($result1[1]->getId(), $result2[1]->getId()); + + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); + + $this->assertEquals($queryCount + 2 , $this->getCurrentQueryCount()); + } + + public function testBasicQueryFetchJoinsOneToMany() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->evictRegions(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT s, c FROM Doctrine\Tests\Models\Cache\State s JOIN s.cities c'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertInstanceOf(State::CLASSNAME, $result1[0]); + $this->assertInstanceOf(State::CLASSNAME, $result1[1]); + $this->assertCount(2, $result1[0]->getCities()); + $this->assertCount(2, $result1[1]->getCities()); + + $this->assertInstanceOf(City::CLASSNAME, $result1[0]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result1[0]->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]->getCities()->get(1)); + + $this->assertNotNull($result1[0]->getCities()->get(0)->getId()); + $this->assertNotNull($result1[0]->getCities()->get(1)->getId()); + $this->assertNotNull($result1[1]->getCities()->get(0)->getId()); + $this->assertNotNull($result1[1]->getCities()->get(1)->getId()); + + $this->assertNotNull($result1[0]->getCities()->get(0)->getName()); + $this->assertNotNull($result1[0]->getCities()->get(1)->getName()); + $this->assertNotNull($result1[1]->getCities()->get(0)->getName()); + $this->assertNotNull($result1[1]->getCities()->get(1)->getName()); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertInstanceOf(State::CLASSNAME, $result2[0]); + $this->assertInstanceOf(State::CLASSNAME, $result2[1]); + $this->assertCount(2, $result2[0]->getCities()); + $this->assertCount(2, $result2[1]->getCities()); + + $this->assertInstanceOf(City::CLASSNAME, $result2[0]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result2[0]->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]->getCities()->get(1)); + + $this->assertNotNull($result2[0]->getCities()->get(0)->getId()); + $this->assertNotNull($result2[0]->getCities()->get(1)->getId()); + $this->assertNotNull($result2[1]->getCities()->get(0)->getId()); + $this->assertNotNull($result2[1]->getCities()->get(1)->getId()); + + $this->assertNotNull($result2[0]->getCities()->get(0)->getName()); + $this->assertNotNull($result2[0]->getCities()->get(1)->getName()); + $this->assertNotNull($result2[1]->getCities()->get(0)->getName()); + $this->assertNotNull($result2[1]->getCities()->get(1)->getName()); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + + public function testBasicQueryFetchJoinsManyToOne() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c, s FROM Doctrine\Tests\Models\Cache\City c JOIN c.state s'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(4, $result1); + $this->assertInstanceOf(City::CLASSNAME, $result1[0]); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]); + $this->assertInstanceOf(State::CLASSNAME, $result1[0]->getState()); + $this->assertInstanceOf(State::CLASSNAME, $result1[1]->getState()); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $result1[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $result1[1]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $result1[0]->getState()->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $result1[1]->getState()->getId())); + + $this->assertEquals(7, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(4, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(City::CLASSNAME))); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(4, $result1); + $this->assertInstanceOf(City::CLASSNAME, $result2[0]); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]); + $this->assertInstanceOf(State::CLASSNAME, $result2[0]->getState()); + $this->assertInstanceOf(State::CLASSNAME, $result2[1]->getState()); + + $this->assertNotNull($result2[0]->getId()); + $this->assertNotNull($result2[0]->getId()); + $this->assertNotNull($result2[1]->getState()->getId()); + $this->assertNotNull($result2[1]->getState()->getId()); + + $this->assertNotNull($result2[0]->getName()); + $this->assertNotNull($result2[0]->getName()); + $this->assertNotNull($result2[1]->getState()->getName()); + $this->assertNotNull($result2[1]->getState()->getName()); + + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); + $this->assertEquals($result1[0]->getState()->getName(), $result2[0]->getState()->getName()); + $this->assertEquals($result1[1]->getState()->getName(), $result2[1]->getState()->getName()); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + + public function testReloadQueryIfToOneIsNotFound() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c, s FROM Doctrine\Tests\Models\Cache\City c JOIN c.state s'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(4, $result1); + $this->assertInstanceOf(City::CLASSNAME, $result1[0]); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]); + $this->assertInstanceOf(State::CLASSNAME, $result1[0]->getState()); + $this->assertInstanceOf(State::CLASSNAME, $result1[1]->getState()); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $result1[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $result1[1]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $result1[0]->getState()->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $result1[1]->getState()->getId())); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(State::CLASSNAME); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(4, $result1); + $this->assertInstanceOf(City::CLASSNAME, $result2[0]); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]); + $this->assertInstanceOf(State::CLASSNAME, $result2[0]->getState()); + $this->assertInstanceOf(State::CLASSNAME, $result2[1]->getState()); + + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + } + + public function testReloadQueryIfToManyAssociationItemIsNotFound() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->evictRegions(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT s, c FROM Doctrine\Tests\Models\Cache\State s JOIN s.cities c'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertInstanceOf(State::CLASSNAME, $result1[0]); + $this->assertInstanceOf(State::CLASSNAME, $result1[1]); + $this->assertCount(2, $result1[0]->getCities()); + $this->assertCount(2, $result1[1]->getCities()); + + $this->assertInstanceOf(City::CLASSNAME, $result1[0]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result1[0]->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]->getCities()->get(1)); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(City::CLASSNAME); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertInstanceOf(State::CLASSNAME, $result2[0]); + $this->assertInstanceOf(State::CLASSNAME, $result2[1]); + $this->assertCount(2, $result2[0]->getCities()); + $this->assertCount(2, $result2[1]->getCities()); + + $this->assertInstanceOf(City::CLASSNAME, $result2[0]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result2[0]->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]->getCities()->get(1)); + + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + } + + public function testBasicNativeQueryCache() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $rsm = new ResultSetMapping; + $rsm->addEntityResult(Country::CLASSNAME, 'c'); + $rsm->addFieldResult('c', 'name', 'name'); + $rsm->addFieldResult('c', 'id', 'id'); + + $queryCount = $this->getCurrentQueryCount(); + $sql = 'SELECT id, name FROM cache_country'; + $result1 = $this->_em->createNativeQuery($sql, $rsm)->setCacheable(true)->getResult(); + + $this->assertCount(2, $result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals($this->countries[0]->getId(), $result1[0]->getId()); + $this->assertEquals($this->countries[1]->getId(), $result1[1]->getId()); + $this->assertEquals($this->countries[0]->getName(), $result1[0]->getName()); + $this->assertEquals($this->countries[1]->getName(), $result1[1]->getName()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->_em->clear(); + + $result2 = $this->_em->createNativeQuery($sql, $rsm) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertCount(2, $result2); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[1]); + + $this->assertEquals($result1[0]->getId(), $result2[0]->getId()); + $this->assertEquals($result1[1]->getId(), $result2[1]->getId()); + + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + } + + public function testQueryDependsOnFirstAndMaxResultResult() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->setFirstResult(1) + ->setMaxResults(1) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->setFirstResult(2) + ->setMaxResults(1) + ->getResult(); + + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + + $this->_em->clear(); + + $result3 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 3, $this->getCurrentQueryCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getMissCount()); + } + + public function testQueryCacheLifetime() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $getHash = function(\Doctrine\ORM\AbstractQuery $query){ + $method = new \ReflectionMethod($query, 'getHash'); + $method->setAccessible(true); + + return $method->invoke($query); + }; + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $query = $this->_em->createQuery($dql); + $result1 = $query->setCacheable(true) + ->setLifetime(3600) + ->getResult(); + + $this->assertNotEmpty($result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->_em->clear(); + + $key = new QueryCacheKey($getHash($query), 3600); + $entry = $this->cache->getQueryCache() + ->getRegion() + ->get($key); + + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheEntry', $entry); + $entry->time = $entry->time / 2; + + $this->cache->getQueryCache() + ->getRegion() + ->put($key, $entry); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->setLifetime(3600) + ->getResult(); + + $this->assertNotEmpty($result2); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + } + + public function testQueryCacheRegion() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $query = $this->_em->createQuery($dql); + + $query1 = clone $query; + $result1 = $query1->setCacheable(true) + ->setCacheRegion('foo_region') + ->getResult(); + + $this->assertNotEmpty($result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals(0, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount('foo_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount('foo_region')); + + $query2 = clone $query; + $result2 = $query2->setCacheable(true) + ->setCacheRegion('bar_region') + ->getResult(); + + $this->assertNotEmpty($result2); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertEquals(0, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount('bar_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount('bar_region')); + + $query3 = clone $query; + $result3 = $query3->setCacheable(true) + ->setCacheRegion('foo_region') + ->getResult(); + + $this->assertNotEmpty($result3); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount('foo_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount('foo_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount('foo_region')); + + $query4 = clone $query; + $result4 = $query4->setCacheable(true) + ->setCacheRegion('bar_region') + ->getResult(); + + $this->assertNotEmpty($result3); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount('bar_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount('bar_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount('bar_region')); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php new file mode 100644 index 00000000000..ca546bb1037 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php @@ -0,0 +1,128 @@ +evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $repository = $this->_em->getRepository(Country::CLASSNAME); + $country1 = $repository->find($this->countries[0]->getId()); + $country2 = $repository->find($this->countries[1]->getId()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(Country::CLASSNAME, $country1); + $this->assertInstanceOf(Country::CLASSNAME, $country2); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(0, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(Country::CLASSNAME))); + + } + + public function testRepositoryCacheFindAll() + { + $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->assertInstanceOf(Country::CLASSNAME, $countries[0]); + $this->assertInstanceOf(Country::CLASSNAME, $countries[1]); + + $this->assertEquals(1, $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 testRepositoryCacheFindBy() + { + $this->loadFixturesCountries(); + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + + $criteria = array('name'=>$this->countries[0]->getName()); + $repository = $this->_em->getRepository(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(1, $repository->findBy($criteria)); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $queryCount = $this->getCurrentQueryCount(); + $countries = $repository->findBy($criteria); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertCount(1, $countries); + $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + } + + public function testRepositoryCacheFindOneBy() + { + $this->loadFixturesCountries(); + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + + $criteria = array('name'=>$this->countries[0]->getName()); + $repository = $this->_em->getRepository(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertNotNull($repository->findOneBy($criteria)); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $queryCount = $this->getCurrentQueryCount(); + $country = $repository->findOneBy($criteria); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(Country::CLASSNAME, $country); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php new file mode 100644 index 00000000000..98c822ca548 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php @@ -0,0 +1,209 @@ +cache->getEntityCacheRegion(Attraction::CLASSNAME); + $restaurantRegion = $this->cache->getEntityCacheRegion(Restaurant::CLASSNAME); + $beachRegion = $this->cache->getEntityCacheRegion(Beach::CLASSNAME); + $barRegion = $this->cache->getEntityCacheRegion(Bar::CLASSNAME); + + $this->assertEquals($attractionRegion->getName(), $restaurantRegion->getName()); + $this->assertEquals($attractionRegion->getName(), $beachRegion->getName()); + $this->assertEquals($attractionRegion->getName(), $barRegion->getName()); + } + + public function testPutOnPersistSingleTableInheritance() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Bar::CLASSNAME, $this->attractions[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Bar::CLASSNAME, $this->attractions[1]->getId())); + } + + public function testCountaisRootClass() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + + $this->_em->clear(); + + foreach ($this->attractions as $attraction) { + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $attraction->getId())); + $this->assertTrue($this->cache->containsEntity(get_class($attraction), $attraction->getId())); + } + } + + public function testPutAndLoadEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(Attraction::CLASSNAME); + + $entityId1 = $this->attractions[0]->getId(); + $entityId2 = $this->attractions[1]->getId(); + + $this->assertFalse($this->cache->containsEntity(Attraction::CLASSNAME, $entityId1)); + $this->assertFalse($this->cache->containsEntity(Attraction::CLASSNAME, $entityId2)); + $this->assertFalse($this->cache->containsEntity(Bar::CLASSNAME, $entityId1)); + $this->assertFalse($this->cache->containsEntity(Bar::CLASSNAME, $entityId2)); + + $entity1 = $this->_em->find(Attraction::CLASSNAME, $entityId1); + $entity2 = $this->_em->find(Attraction::CLASSNAME, $entityId2); + + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $entityId1)); + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $entityId2)); + $this->assertTrue($this->cache->containsEntity(Bar::CLASSNAME, $entityId1)); + $this->assertTrue($this->cache->containsEntity(Bar::CLASSNAME, $entityId2)); + + $this->assertInstanceOf(Attraction::CLASSNAME, $entity1); + $this->assertInstanceOf(Attraction::CLASSNAME, $entity2); + $this->assertInstanceOf(Bar::CLASSNAME, $entity1); + $this->assertInstanceOf(Bar::CLASSNAME, $entity2); + + $this->assertEquals($this->attractions[0]->getId(), $entity1->getId()); + $this->assertEquals($this->attractions[0]->getName(), $entity1->getName()); + + $this->assertEquals($this->attractions[1]->getId(), $entity2->getId()); + $this->assertEquals($this->attractions[1]->getName(), $entity2->getName()); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $entity3 = $this->_em->find(Attraction::CLASSNAME, $entityId1); + $entity4 = $this->_em->find(Attraction::CLASSNAME, $entityId2); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(Attraction::CLASSNAME, $entity3); + $this->assertInstanceOf(Attraction::CLASSNAME, $entity4); + $this->assertInstanceOf(Bar::CLASSNAME, $entity3); + $this->assertInstanceOf(Bar::CLASSNAME, $entity4); + + $this->assertNotSame($entity1, $entity3); + $this->assertEquals($entity1->getId(), $entity3->getId()); + $this->assertEquals($entity1->getName(), $entity3->getName()); + + $this->assertNotSame($entity2, $entity4); + $this->assertEquals($entity2->getId(), $entity4->getId()); + $this->assertEquals($entity2->getName(), $entity4->getName()); + } + + public function testQueryCacheFindAll() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT a FROM Doctrine\Tests\Models\Cache\Attraction a'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(count($this->attractions), $result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(count($this->attractions), $result2); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + foreach ($result2 as $entity) { + $this->assertInstanceOf(Attraction::CLASSNAME, $entity); + } + } + + public function testShouldNotPutOneToManyRelationOnPersist() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + + $this->_em->clear(); + + foreach ($this->cities as $city) { + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $city->getId())); + $this->assertFalse($this->cache->containsCollection(City::CLASSNAME, 'attractions', $city->getId())); + } + + foreach ($this->attractions as $attraction) { + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $attraction->getId())); + } + } + + public function testOneToManyRelationSingleTable() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->evictRegions(); + $this->_em->clear(); + + $entity = $this->_em->find(City::CLASSNAME, $this->cities[0]->getId()); + + $this->assertInstanceOf(City::CLASSNAME, $entity); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $entity->getAttractions()); + $this->assertCount(2, $entity->getAttractions()); + + $ownerId = $this->cities[0]->getId(); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $ownerId)); + $this->assertTrue($this->cache->containsCollection(City::CLASSNAME, 'attractions', $ownerId)); + + $this->assertInstanceOf(Bar::CLASSNAME, $entity->getAttractions()->get(0)); + $this->assertInstanceOf(Bar::CLASSNAME, $entity->getAttractions()->get(1)); + $this->assertEquals($this->attractions[0]->getName(), $entity->getAttractions()->get(0)->getName()); + $this->assertEquals($this->attractions[1]->getName(), $entity->getAttractions()->get(1)->getName()); + + $this->_em->clear(); + + $entity = $this->_em->find(City::CLASSNAME, $ownerId); + + $this->assertInstanceOf(City::CLASSNAME, $entity); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $entity->getAttractions()); + $this->assertCount(2, $entity->getAttractions()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(Bar::CLASSNAME, $entity->getAttractions()->get(0)); + $this->assertInstanceOf(Bar::CLASSNAME, $entity->getAttractions()->get(1)); + $this->assertEquals($this->attractions[0]->getName(), $entity->getAttractions()->get(0)->getName()); + $this->assertEquals($this->attractions[1]->getName(), $entity->getAttractions()->get(1)->getName()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheTest.php new file mode 100644 index 00000000000..37dadd26ce7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheTest.php @@ -0,0 +1,351 @@ +loadFixturesCountries(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + } + + public function testPutAndLoadEntities() + { + $this->loadFixturesCountries(); + $this->_em->clear(); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $c1 = $this->_em->find(Country::CLASSNAME, $this->countries[0]->getId()); + $c2 = $this->_em->find(Country::CLASSNAME, $this->countries[1]->getId()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertInstanceOf(Country::CLASSNAME, $c1); + $this->assertInstanceOf(Country::CLASSNAME, $c2); + + $this->assertEquals($this->countries[0]->getId(), $c1->getId()); + $this->assertEquals($this->countries[0]->getName(), $c1->getName()); + + $this->assertEquals($this->countries[1]->getId(), $c2->getId()); + $this->assertEquals($this->countries[1]->getName(), $c2->getName()); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $c3 = $this->_em->find(Country::CLASSNAME, $this->countries[0]->getId()); + $c4 = $this->_em->find(Country::CLASSNAME, $this->countries[1]->getId()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(Country::CLASSNAME))); + + $this->assertInstanceOf(Country::CLASSNAME, $c3); + $this->assertInstanceOf(Country::CLASSNAME, $c4); + + $this->assertEquals($c1->getId(), $c3->getId()); + $this->assertEquals($c1->getName(), $c3->getName()); + + $this->assertEquals($c2->getId(), $c4->getId()); + $this->assertEquals($c2->getName(), $c4->getName()); + } + + public function testRemoveEntities() + { + $this->loadFixturesCountries(); + $this->_em->clear(); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + $this->secondLevelCacheLogger->clearRegionStats($this->getEntityRegion(Country::CLASSNAME)); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $c1 = $this->_em->find(Country::CLASSNAME, $this->countries[0]->getId()); + $c2 = $this->_em->find(Country::CLASSNAME, $this->countries[1]->getId()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $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())); + + $this->assertInstanceOf(Country::CLASSNAME, $c1); + $this->assertInstanceOf(Country::CLASSNAME, $c2); + + $this->assertEquals($this->countries[0]->getId(), $c1->getId()); + $this->assertEquals($this->countries[0]->getName(), $c1->getName()); + + $this->assertEquals($this->countries[1]->getId(), $c2->getId()); + $this->assertEquals($this->countries[1]->getName(), $c2->getName()); + + $this->_em->remove($c1); + $this->_em->remove($c2); + $this->_em->flush(); + $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())); + + $this->assertNull($this->_em->find(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertNull($this->_em->find(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + } + + public function testUpdateEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->_em->clear(); + + $this->assertEquals(6, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + $this->assertEquals(4, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + + $this->cache->evictEntityRegion(State::CLASSNAME); + $this->secondLevelCacheLogger->clearRegionStats($this->getEntityRegion(State::CLASSNAME)); + + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $s1 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $s2 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertInstanceOf(State::CLASSNAME, $s1); + $this->assertInstanceOf(State::CLASSNAME, $s2); + + $this->assertEquals($this->states[0]->getId(), $s1->getId()); + $this->assertEquals($this->states[0]->getName(), $s1->getName()); + + $this->assertEquals($this->states[1]->getId(), $s2->getId()); + $this->assertEquals($this->states[1]->getName(), $s2->getName()); + + $s1->setName("NEW NAME 1"); + $s2->setName("NEW NAME 2"); + + $this->_em->persist($s1); + $this->_em->persist($s2); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertEquals(6, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + $this->assertEquals(4, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + + $queryCount = $this->getCurrentQueryCount(); + + $c3 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $c4 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertInstanceOf(State::CLASSNAME, $c3); + $this->assertInstanceOf(State::CLASSNAME, $c4); + + $this->assertEquals($s1->getId(), $c3->getId()); + $this->assertEquals("NEW NAME 1", $c3->getName()); + + $this->assertEquals($s2->getId(), $c4->getId()); + $this->assertEquals("NEW NAME 2", $c4->getName()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + } + + public function testPostFlushFailure() + { + $listener = new ListenerSecondLevelCacheTest(array(Events::postFlush => function(){ + throw new \RuntimeException('post flush failure'); + })); + + $this->_em->getEventManager() + ->addEventListener(Events::postFlush, $listener); + + $country = new Country("Brazil"); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + + try { + + $this->_em->persist($country); + $this->_em->flush(); + $this->fail('Should throw exception'); + + } catch (\RuntimeException $exc) { + $this->assertNotNull($country->getId()); + $this->assertEquals('post flush failure', $exc->getMessage()); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $country->getId())); + } + } + + public function testPostUpdateFailure() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->_em->clear(); + + $listener = new ListenerSecondLevelCacheTest(array(Events::postUpdate => function(){ + throw new \RuntimeException('post update failure'); + })); + + $this->_em->getEventManager() + ->addEventListener(Events::postUpdate, $listener); + + $this->cache->evictEntityRegion(State::CLASSNAME); + + $stateId = $this->states[0]->getId(); + $stateName = $this->states[0]->getName(); + $state = $this->_em->find(State::CLASSNAME, $stateId); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $stateId)); + $this->assertInstanceOf(State::CLASSNAME, $state); + $this->assertEquals($stateName, $state->getName()); + + $state->setName($stateName . uniqid()); + + $this->_em->persist($state); + + try { + $this->_em->flush(); + $this->fail('Should throw exception'); + + } catch (\Exception $exc) { + $this->assertEquals('post update failure', $exc->getMessage()); + } + + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $stateId)); + + $state = $this->_em->find(State::CLASSNAME, $stateId); + + $this->assertInstanceOf(State::CLASSNAME, $state); + $this->assertEquals($stateName, $state->getName()); + } + + public function testPostRemoveFailure() + { + $this->loadFixturesCountries(); + $this->_em->clear(); + + $listener = new ListenerSecondLevelCacheTest(array(Events::postRemove => function(){ + throw new \RuntimeException('post remove failure'); + })); + + $this->_em->getEventManager() + ->addEventListener(Events::postRemove, $listener); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + + $countryId = $this->countries[0]->getId(); + $country = $this->_em->find(Country::CLASSNAME, $countryId); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $countryId)); + $this->assertInstanceOf(Country::CLASSNAME, $country); + + $this->_em->remove($country); + + try { + $this->_em->flush(); + $this->fail('Should throw exception'); + + } catch (\Exception $exc) { + $this->assertEquals('post remove failure', $exc->getMessage()); + } + + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $countryId)); + + $country = $this->_em->find(Country::CLASSNAME, $countryId); + $this->assertInstanceOf(Country::CLASSNAME, $country); + } + + public function testCachedNewEntityExists() + { + $this->loadFixturesCountries(); + + $persister = $this->_em->getUnitOfWork()->getEntityPersister(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertTrue($persister->exists($this->countries[0])); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertFalse($persister->exists(new Country('Foo'))); + } +} + + +class ListenerSecondLevelCacheTest +{ + public $callbacks; + + public function __construct(array $callbacks = array()) + { + $this->callbacks = $callbacks; + } + + private function dispatch($eventName, $args) + { + if (isset($this->callbacks[$eventName])) { + call_user_func($this->callbacks[$eventName], $args); + } + } + + public function postFlush($args) + { + $this->dispatch(__FUNCTION__, $args); + } + + public function postUpdate($args) + { + $this->dispatch(__FUNCTION__, $args); + } + + public function postRemove($args) + { + $this->dispatch(__FUNCTION__, $args); + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/SingleTableCompositeKeyTest.php b/tests/Doctrine/Tests/ORM/Functional/SingleTableCompositeKeyTest.php index 3d1fe7714cb..53829a1c05e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SingleTableCompositeKeyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SingleTableCompositeKeyTest.php @@ -30,7 +30,7 @@ public function testInsertWithCompositeKey() } /** - * + * @group non-cacheable */ public function testUpdateWithCompositeKey() { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php index 6673afd1a4f..17ecbafd6e8 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php @@ -12,6 +12,9 @@ require_once __DIR__ . '/../../../TestInit.php'; +/** + * @group DDC-117 + */ class DDC117Test extends \Doctrine\Tests\OrmFunctionalTestCase { private $article1; @@ -138,6 +141,7 @@ public function testRemoveCompositeElement() /** * @group DDC-117 + * @group non-cacheable */ public function testDqlRemoveCompositeElement() { @@ -471,6 +475,10 @@ public function testArrayHydrationWithCompositeKey() */ public function testGetEntityState() { + if ($this->isSecondLevelCacheEnabled) { + $this->markTestIncomplete('Second level cache - not supported yet'); + } + $this->article1 = $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Article", $this->article1->id()); $this->article2 = $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Article", $this->article2->id()); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1301Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1301Test.php index 94d02f905a8..502101bf91e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1301Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1301Test.php @@ -8,6 +8,9 @@ /** * @author asm89 + * + * @group non-cacheable + * @group DDC-1301 */ class DDC1301Test extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1595Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1595Test.php index e6bc589426c..2c631750481 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1595Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1595Test.php @@ -5,6 +5,7 @@ /** * @group DDC-1595 * @group DDC-1596 + * @group non-cacheable */ class DDC1595Test extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2012Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2012Test.php index eca15cea3fb..ac93906dab6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2012Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2012Test.php @@ -9,6 +9,7 @@ /** * @group DDC-2012 + * @group non-cacheable */ class DDC2012Test extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2090Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2090Test.php index c5da3deaeb4..cd6db885712 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2090Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2090Test.php @@ -6,6 +6,7 @@ /** * @group DDC-2090 + * @group non-cacheable */ class DDC2090Test extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2350Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2350Test.php index 229bbff52ee..75f34a25c19 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2350Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2350Test.php @@ -6,6 +6,7 @@ /** * @group DDC-2350 + * @group non-cacheable */ class DDC2350Test extends OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php index 1b2ca881edf..b5a09f9223a 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php @@ -7,6 +7,7 @@ /** * @group DDC-2494 + * @group non-cacheable */ class DDC2494Test extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC742Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC742Test.php index 94936a12587..334c2c6235c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC742Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC742Test.php @@ -6,6 +6,9 @@ require_once __DIR__ . '/../../../TestInit.php'; +/** + * @group non-cacheable + */ class DDC742Test extends \Doctrine\Tests\OrmFunctionalTestCase { private $userCm; diff --git a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php index 28e6f1ede6f..a30845777b5 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php @@ -6,7 +6,7 @@ use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\Tests\Models\Company\CompanyFixContract; use Doctrine\Tests\Models\Company\CompanyFlexContract; - +use Doctrine\Tests\Models\Cache\City; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; @@ -873,6 +873,34 @@ public function testEntityListenersNamingConvention() $this->assertEquals(Events::postLoad, $postLoad['method']); $this->assertEquals(Events::preFlush, $preFlush['method']); } + + /** + * @group DDC-2183 + */ + public function testSecondLevelCacheMapping() + { + $em = $this->_getTestEntityManager(); + $factory = $this->createClassMetadataFactory($em); + $class = $factory->getMetadataFor(City::CLASSNAME); + $this->assertArrayHasKey('usage', $class->cache); + $this->assertArrayHasKey('region', $class->cache); + $this->assertEquals(ClassMetadata::CACHE_USAGE_READ_ONLY, $class->cache['usage']); + $this->assertEquals('doctrine_tests_models_cache_city', $class->cache['region']); + + $this->assertArrayHasKey('state', $class->associationMappings); + $this->assertArrayHasKey('cache', $class->associationMappings['state']); + $this->assertArrayHasKey('usage', $class->associationMappings['state']['cache']); + $this->assertArrayHasKey('region', $class->associationMappings['state']['cache']); + $this->assertEquals(ClassMetadata::CACHE_USAGE_READ_ONLY, $class->associationMappings['state']['cache']['usage']); + $this->assertEquals('doctrine_tests_models_cache_city__state', $class->associationMappings['state']['cache']['region']); + + $this->assertArrayHasKey('attractions', $class->associationMappings); + $this->assertArrayHasKey('cache', $class->associationMappings['attractions']); + $this->assertArrayHasKey('usage', $class->associationMappings['attractions']['cache']); + $this->assertArrayHasKey('region', $class->associationMappings['attractions']['cache']); + $this->assertEquals(ClassMetadata::CACHE_USAGE_READ_ONLY, $class->associationMappings['attractions']['cache']['usage']); + $this->assertEquals('doctrine_tests_models_cache_city__attractions', $class->associationMappings['attractions']['cache']['region']); + } } /** diff --git a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Cache.City.php b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Cache.City.php new file mode 100644 index 00000000000..6dd477fa628 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Cache.City.php @@ -0,0 +1,54 @@ +setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_NONE); +$metadata->setPrimaryTable(array('name' => 'cache_city')); +$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_IDENTITY); +$metadata->setChangeTrackingPolicy(ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT); + +$metadata->enableCache(array( + 'usage' => ClassMetadataInfo::CACHE_USAGE_READ_ONLY +)); + +$metadata->mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'id' => true, + )); + +$metadata->mapField(array( + 'fieldName' => 'name', + 'type' => 'string', +)); + + +$metadata->mapOneToOne(array( + 'fieldName' => 'state', + 'targetEntity' => 'Doctrine\\Tests\\Models\\Cache\\State', + 'inversedBy' => 'cities', + 'joinColumns' => + array(array( + 'name' => 'state_id', + 'referencedColumnName' => 'id', + )) +)); +$metadata->enableAssociationCache('state', array( + 'usage' => ClassMetadataInfo::CACHE_USAGE_READ_ONLY +)); + +$metadata->mapManyToMany(array( + 'fieldName' => 'travels', + 'targetEntity' => 'Doctrine\\Tests\\Models\\Cache\\Travel', + 'mappedBy' => 'visitedCities', +)); + +$metadata->mapOneToMany(array( + 'fieldName' => 'attractions', + 'targetEntity' => 'Doctrine\\Tests\\Models\\Cache\\Attraction', + 'mappedBy' => 'city', + 'orderBy' => array('name' => 'ASC',), +)); +$metadata->enableAssociationCache('attractions', array( + 'usage' => ClassMetadataInfo::CACHE_USAGE_READ_ONLY +)); \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Cache.City.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Cache.City.dcm.xml new file mode 100644 index 00000000000..84b786a7bd7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Cache.City.dcm.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Cache.City.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Cache.City.dcm.yml new file mode 100644 index 00000000000..05286e0df56 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Cache.City.dcm.yml @@ -0,0 +1,36 @@ +Doctrine\Tests\Models\Cache\City: + type: entity + table: cache_city + cache: + usage : READ_ONLY + id: + id: + type: integer + id: true + generator: + strategy: IDENTITY + fields: + name: + type: string + manyToOne: + state: + targetEntity: Doctrine\Tests\Models\Cache\State + inversedBy: cities + joinColumns: + state_id: + referencedColumnName: id + cache: + usage : READ_ONLY + manyToMany: + travels: + targetEntity: Doctrine\Tests\Models\Cache\Travel + mappedBy: visitedCities + + oneToMany: + attractions: + targetEntity: Doctrine\Tests\Models\Cache\Attraction + mappedBy: city + cache: + usage : READ_ONLY + orderBy: + name: ASC \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php b/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php new file mode 100644 index 00000000000..3e628acbd80 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php @@ -0,0 +1,284 @@ +_getEntityManager(); + + $em->getConnection()->getConfiguration()->setSQLLogger($logger); + $em->getConfiguration()->setSQLLogger($logger); + + return $em; + } + + /** + * @param \Doctrine\ORM\EntityManagerInterface $em + * @return integer + */ + public function countQuery(EntityManagerInterface $em) + { + return count($em->getConfiguration()->getSQLLogger()->queries); + } + + public function testFindEntityWithoutCache() + { + $em = $this->createEntityManager(); + + $this->findEntity($em, __FUNCTION__); + + $this->assertEquals(6002, $this->countQuery($em)); + } + + public function testFindEntityWithCache() + { + parent::enableSecondLevelCache(false); + + $em = $this->createEntityManager(); + + $this->findEntity($em, __FUNCTION__); + + $this->assertEquals(502, $this->countQuery($em)); + } + + public function testFindAllEntityWithoutCache() + { + $em = $this->createEntityManager(); + + $this->findAllEntity($em, __FUNCTION__); + + $this->assertEquals(153, $this->countQuery($em)); + } + + public function testFindAllEntityWithCache() + { + parent::enableSecondLevelCache(false); + + $em = $this->createEntityManager(); + + $this->findAllEntity($em, __FUNCTION__); + + $this->assertEquals(53, $this->countQuery($em)); + } + + public function testFindEntityOneToManyWithoutCache() + { + $em = $this->createEntityManager(); + + $this->findEntityOneToMany($em, __FUNCTION__); + + $this->assertEquals(502, $this->countQuery($em)); + } + + public function testFindEntityOneToManyWithCache() + { + parent::enableSecondLevelCache(false); + + $em = $this->createEntityManager(); + + $this->findEntityOneToMany($em, __FUNCTION__); + + $this->assertEquals(487, $this->countQuery($em)); + } + + public function testQueryEntityWithoutCache() + { + $em = $this->createEntityManager(); + + $this->queryEntity($em, __FUNCTION__); + + $this->assertEquals(602, $this->countQuery($em)); + } + + public function testQueryEntityWithCache() + { + parent::enableSecondLevelCache(false); + + $em = $this->createEntityManager(); + + $this->queryEntity($em, __FUNCTION__); + + $this->assertEquals(503, $this->countQuery($em)); + } + + private function queryEntity(EntityManagerInterface $em, $label) + { + $times = 100; + $size = 500; + $startPersist = microtime(true); + + echo PHP_EOL . $label; + + for ($i = 0; $i < $size; $i++) { + $em->persist(new Country("Country $i")); + } + + $em->flush(); + $em->clear(); + + printf("\n[%s] persist %s countries", number_format(microtime(true) - $startPersist, 6), $size); + + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c WHERE c.name LIKE :name'; + $startFind = microtime(true); + + for ($i = 0; $i < $times; $i++) { + $em->createQuery($dql) + ->setParameter('name', "%Country%") + ->setCacheable(true) + ->getResult(); + } + + printf("\n[%s] select %s countries (%s times)", number_format(microtime(true) - $startFind, 6), $size, $times); + printf("\n%s\n", str_repeat('-', 50)); + } + + public function findEntityOneToMany(EntityManagerInterface $em, $label) + { + $times = 50; + $size = 30; + $states = array(); + $cities = array(); + $startPersist = microtime(true); + $country = new Country("Country"); + + echo PHP_EOL . $label; + + $em->persist($country); + $em->flush(); + + for ($i = 0; $i < $size / 2; $i++) { + $state = new State("State $i", $country); + + $em->persist($state); + + $states[] = $state; + } + + $em->flush(); + + foreach ($states as $key => $state) { + for ($i = 0; $i < $size; $i++) { + $city = new City("City $key - $i", $state); + + $em->persist($city); + + $state->addCity($city); + + $cities[] = $city; + } + } + + $em->flush(); + $em->clear(); + + printf("\n[%s] persist %s states and %s cities", number_format( microtime(true) - $startPersist, 6), count($states), count($cities)); + + $startFind = microtime(true); + + for ($i = 0; $i < $times; $i++) { + + foreach ($states as $state) { + + $state = $em->find(State::CLASSNAME, $state->getId()); + + foreach ($state->getCities() as $city) { + $city->getName(); + } + } + } + + printf("\n[%s] find %s states and %s cities (%s times)", number_format(microtime(true) - $startFind, 6), count($states), count($cities), $times); + printf("\n%s\n", str_repeat('-', 50)); + } + + private function findEntity(EntityManagerInterface $em, $label) + { + $times = 10; + $size = 500; + $countries = array(); + $startPersist = microtime(true); + + echo PHP_EOL . $label; + + for ($i = 0; $i < $size; $i++) { + $country = new Country("Country $i"); + + $em->persist($country); + + $countries[] = $country; + } + + $em->flush(); + $em->clear(); + + printf("\n[%s] persist %s countries", number_format(microtime(true) - $startPersist, 6), $size); + + $startFind = microtime(true); + + for ($i = 0; $i <= $times; $i++) { + foreach ($countries as $country) { + $em->find(Country::CLASSNAME, $country->getId()); + $em->clear(); + } + } + + printf("\n[%s] find %s countries (%s times)", number_format(microtime(true) - $startFind, 6), $size, $times); + printf("\n%s\n", str_repeat('-', 50)); + } + + private function findAllEntity(EntityManagerInterface $em, $label) + { + $times = 100; + $size = 50; + $startPersist = microtime(true); + $rep = $em->getRepository(Country::CLASSNAME); + + echo PHP_EOL . $label; + + for ($i = 0; $i < $size; $i++) { + $em->persist(new Country("Country $i")); + } + + $em->flush(); + $em->clear(); + + printf("\n[%s] persist %s countries", number_format(microtime(true) - $startPersist, 6), $size); + + $startFind = microtime(true); + + for ($i = 0; $i <= $times; $i++) { + $list = $rep->findAll(); + $em->clear(); + + $this->assertCount($size, $list); + } + + printf("\n[%s] find %s countries (%s times)", number_format(microtime(true) - $startFind, 6), $size, $times); + printf("\n%s\n", str_repeat('-', 50)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index d2a41cfb968..dba7bd09f33 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -2,6 +2,10 @@ namespace Doctrine\Tests; +use Doctrine\Tests\EventListener\CacheMetadataListener; +use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger; +use Doctrine\ORM\Cache\DefaultCacheFactory; + /** * Base testcase class for all functional ORM testcases. * @@ -162,6 +166,21 @@ abstract class OrmFunctionalTestCase extends OrmTestCase 'Doctrine\Tests\Models\Taxi\Car', 'Doctrine\Tests\Models\Taxi\Driver', ), + 'cache' => array( + 'Doctrine\Tests\Models\Cache\Country', + 'Doctrine\Tests\Models\Cache\State', + 'Doctrine\Tests\Models\Cache\City', + 'Doctrine\Tests\Models\Cache\Traveler', + 'Doctrine\Tests\Models\Cache\Travel', + 'Doctrine\Tests\Models\Cache\Attraction', + 'Doctrine\Tests\Models\Cache\Restaurant', + 'Doctrine\Tests\Models\Cache\Beach', + 'Doctrine\Tests\Models\Cache\Bar', + 'Doctrine\Tests\Models\Cache\Flight', + 'Doctrine\Tests\Models\Cache\AttractionInfo', + 'Doctrine\Tests\Models\Cache\AttractionContactInfo', + 'Doctrine\Tests\Models\Cache\AttractionLocationInfo' + ), ); /** @@ -297,6 +316,20 @@ protected function tearDown() $conn->executeUpdate('DELETE FROM taxi_driver'); } + if (isset($this->_usedModelSets['cache'])) { + $conn->executeUpdate('DELETE FROM cache_attraction_location_info'); + $conn->executeUpdate('DELETE FROM cache_attraction_contact_info'); + $conn->executeUpdate('DELETE FROM cache_attraction_info'); + $conn->executeUpdate('DELETE FROM cache_visited_cities'); + $conn->executeUpdate('DELETE FROM cache_flight'); + $conn->executeUpdate('DELETE FROM cache_attraction'); + $conn->executeUpdate('DELETE FROM cache_travel'); + $conn->executeUpdate('DELETE FROM cache_traveler'); + $conn->executeUpdate('DELETE FROM cache_city'); + $conn->executeUpdate('DELETE FROM cache_state'); + $conn->executeUpdate('DELETE FROM cache_country'); + } + $this->_em->clear(); } @@ -411,7 +444,29 @@ protected function _getEntityManager($config = null, $eventManager = null) { $config->setProxyDir(__DIR__ . '/Proxies'); $config->setProxyNamespace('Doctrine\Tests\Proxies'); - $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver(array(), true)); + $enableSecondLevelCache = getenv('ENABLE_SECOND_LEVEL_CACHE'); + + if ($this->isSecondLevelCacheEnabled || $enableSecondLevelCache) { + + $cache = $this->getSharedSecondLevelCacheDriverImpl(); + $factory = new DefaultCacheFactory($config, $cache); + + $this->secondLevelCacheFactory = $factory; + + if ($this->isSecondLevelCacheLogEnabled) { + $this->secondLevelCacheLogger = new StatisticsCacheLogger(); + $config->setSecondLevelCacheLogger($this->secondLevelCacheLogger); + } + + $config->setSecondLevelCacheEnabled(); + $config->setSecondLevelCacheFactory($factory); + + $this->isSecondLevelCacheEnabled = true; + } + + $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver(array( + realpath(__DIR__ . '/Models/Cache') + ), true)); $conn = static::$_sharedConn; $conn->getConfiguration()->setSQLLogger($this->_sqlLoggerStack); @@ -424,6 +479,10 @@ protected function _getEntityManager($config = null, $eventManager = null) { } } + if ($enableSecondLevelCache) { + $evm->addEventListener('loadClassMetadata', new CacheMetadataListener()); + } + if (isset($GLOBALS['db_event_subscribers'])) { foreach (explode(",", $GLOBALS['db_event_subscribers']) AS $subscriberClass) { $subscriberInstance = new $subscriberClass(); diff --git a/tests/Doctrine/Tests/OrmTestCase.php b/tests/Doctrine/Tests/OrmTestCase.php index 9ba32cf8bd7..541e094a31f 100644 --- a/tests/Doctrine/Tests/OrmTestCase.php +++ b/tests/Doctrine/Tests/OrmTestCase.php @@ -3,12 +3,14 @@ namespace Doctrine\Tests; use Doctrine\Common\Cache\ArrayCache; +use Doctrine\ORM\Cache\DefaultCacheFactory; /** * Base testcase class for all ORM testcases. */ abstract class OrmTestCase extends DoctrineTestCase { + /** * The metadata cache that is shared between all ORM tests (except functional tests). * @@ -23,6 +25,31 @@ abstract class OrmTestCase extends DoctrineTestCase */ private static $_queryCacheImpl = null; + /** + * @var boolean + */ + protected $isSecondLevelCacheEnabled = false; + + /** + * @var boolean + */ + protected $isSecondLevelCacheLogEnabled = false; + + /** + * @var \Doctrine\ORM\Cache\CacheFactory + */ + protected $secondLevelCacheFactory; + + /** + * @var \Doctrine\ORM\Cache\Logging\StatisticsCacheLogger + */ + protected $secondLevelCacheLogger; + + /** + * @var \Doctrine\Common\Cache\Cache|null + */ + protected $secondLevelCacheDriverImpl = null; + /** * @param array $paths * @param mixed $alias @@ -95,6 +122,19 @@ protected function _getTestEntityManager($conn = null, $conf = null, $eventManag $config->setQueryCacheImpl(self::getSharedQueryCacheImpl()); $config->setProxyDir(__DIR__ . '/Proxies'); $config->setProxyNamespace('Doctrine\Tests\Proxies'); + $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver(array( + realpath(__DIR__ . '/Models/Cache') + ), true)); + + if ($this->isSecondLevelCacheEnabled) { + $cache = $this->getSharedSecondLevelCacheDriverImpl(); + $factory = new DefaultCacheFactory($config, $cache); + + $this->secondLevelCacheFactory = $factory; + + $config->setSecondLevelCacheEnabled(); + $config->setSecondLevelCacheFactory($factory); + } if ($conn === null) { $conn = array( @@ -112,6 +152,12 @@ protected function _getTestEntityManager($conn = null, $conf = null, $eventManag return \Doctrine\Tests\Mocks\EntityManagerMock::create($conn, $config, $eventManager); } + protected function enableSecondLevelCache($log = true) + { + $this->isSecondLevelCacheEnabled = true; + $this->isSecondLevelCacheLogEnabled = $log; + } + /** * @return \Doctrine\Common\Cache\Cache */ @@ -135,4 +181,16 @@ private static function getSharedQueryCacheImpl() return self::$_queryCacheImpl; } + + /** + * @return \Doctrine\Common\Cache\Cache + */ + protected function getSharedSecondLevelCacheDriverImpl() + { + if ($this->secondLevelCacheDriverImpl === null) { + $this->secondLevelCacheDriverImpl = new \Doctrine\Common\Cache\ArrayCache(); + } + + return $this->secondLevelCacheDriverImpl; + } } diff --git a/tests/travis/mysql.travis.xml b/tests/travis/mysql.travis.xml index 82559afdf90..e278e88596d 100644 --- a/tests/travis/mysql.travis.xml +++ b/tests/travis/mysql.travis.xml @@ -1,5 +1,5 @@ - + @@ -25,9 +25,15 @@ ./../Doctrine/Tests/ORM + + + ./../../lib/Doctrine + + performance + non-cacheable locking_functional diff --git a/tests/travis/pgsql.travis.xml b/tests/travis/pgsql.travis.xml index b92f775aa54..a2993fcb173 100644 --- a/tests/travis/pgsql.travis.xml +++ b/tests/travis/pgsql.travis.xml @@ -1,5 +1,5 @@ - + @@ -28,10 +28,15 @@ ./../Doctrine/Tests/ORM - + + + ./../../lib/Doctrine + + performance + non-cacheable locking_functional diff --git a/tests/travis/sqlite.travis.xml b/tests/travis/sqlite.travis.xml index a4c400caaf1..242d4dea8c4 100644 --- a/tests/travis/sqlite.travis.xml +++ b/tests/travis/sqlite.travis.xml @@ -1,5 +1,5 @@ - + @@ -10,10 +10,15 @@ ./../Doctrine/Tests/ORM - + + + ./../../lib/Doctrine + + performance + non-cacheable locking_functional