From 3140593e9b88f97cc6a2aa99d3c539d2735711c9 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Wed, 13 Feb 2013 20:42:13 -0200 Subject: [PATCH 1/9] Second level cache --- .travis.yml | 11 +- docs/en/index.rst | 1 + docs/en/reference/second-level-cache.rst | 804 +++++++++++++++++ docs/en/toc.rst | 12 +- doctrine-mapping.xsd | 18 + lib/Doctrine/ORM/AbstractQuery.php | 228 ++++- lib/Doctrine/ORM/Cache.php | 185 ++++ lib/Doctrine/ORM/Cache/CacheEntry.php | 32 + lib/Doctrine/ORM/Cache/CacheException.php | 72 ++ lib/Doctrine/ORM/Cache/CacheFactory.php | 95 ++ lib/Doctrine/ORM/Cache/CacheKey.php | 36 + .../ORM/Cache/CollectionCacheEntry.php | 51 ++ lib/Doctrine/ORM/Cache/CollectionCacheKey.php | 60 ++ lib/Doctrine/ORM/Cache/CollectionHydrator.php | 54 ++ lib/Doctrine/ORM/Cache/ConcurrentRegion.php | 59 ++ lib/Doctrine/ORM/Cache/DefaultCache.php | 346 +++++++ .../ORM/Cache/DefaultCacheFactory.php | 202 +++++ .../ORM/Cache/DefaultCollectionHydrator.php | 103 +++ .../ORM/Cache/DefaultEntityHydrator.php | 151 ++++ lib/Doctrine/ORM/Cache/DefaultQueryCache.php | 294 ++++++ lib/Doctrine/ORM/Cache/EntityCacheEntry.php | 58 ++ lib/Doctrine/ORM/Cache/EntityCacheKey.php | 53 ++ lib/Doctrine/ORM/Cache/EntityHydrator.php | 51 ++ lib/Doctrine/ORM/Cache/Lock.php | 58 ++ lib/Doctrine/ORM/Cache/LockException.php | 32 + .../ORM/Cache/Logging/CacheLogger.php | 106 +++ .../Cache/Logging/StatisticsCacheLogger.php | 227 +++++ .../Persister/AbstractCollectionPersister.php | 275 ++++++ .../Persister/AbstractEntityPersister.php | 515 +++++++++++ .../Persister/CachedCollectionPersister.php | 64 ++ .../Cache/Persister/CachedEntityPersister.php | 45 + .../ORM/Cache/Persister/CachedPersister.php | 46 + ...rictReadWriteCachedCollectionPersister.php | 105 +++ ...onStrictReadWriteCachedEntityPersister.php | 116 +++ .../ReadOnlyCachedCollectionPersister.php | 44 + .../ReadOnlyCachedEntityPersister.php | 41 + .../ReadWriteCachedCollectionPersister.php | 140 +++ .../ReadWriteCachedEntityPersister.php | 133 +++ lib/Doctrine/ORM/Cache/QueryCache.php | 60 ++ lib/Doctrine/ORM/Cache/QueryCacheEntry.php | 58 ++ lib/Doctrine/ORM/Cache/QueryCacheKey.php | 52 ++ .../ORM/Cache/QueryCacheValidator.php | 42 + lib/Doctrine/ORM/Cache/Region.php | 86 ++ .../ORM/Cache/Region/DefaultRegion.php | 126 +++ .../ORM/Cache/Region/FileLockRegion.php | 245 +++++ .../Cache/TimestampQueryCacheValidator.php | 43 + lib/Doctrine/ORM/Configuration.php | 171 ++++ .../ORM/Decorator/EntityManagerDecorator.php | 8 + lib/Doctrine/ORM/EntityManager.php | 20 +- lib/Doctrine/ORM/EntityManagerInterface.php | 7 + lib/Doctrine/ORM/Mapping/Cache.php | 44 + .../ORM/Mapping/ClassMetadataFactory.php | 4 + .../ORM/Mapping/ClassMetadataInfo.php | 63 +- .../ORM/Mapping/Driver/AnnotationDriver.php | 18 + .../Mapping/Driver/DoctrineAnnotations.php | 3 +- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 41 + .../ORM/Mapping/Driver/YamlDriver.php | 43 + lib/Doctrine/ORM/ORMException.php | 24 + .../AbstractCollectionPersister.php | 94 +- .../ORM/Persisters/BasicEntityPersister.php | 220 ++--- .../ORM/Persisters/CollectionPersister.php | 141 +++ .../ORM/Persisters/EntityPersister.php | 300 +++++++ .../Persisters/JoinedSubclassPersister.php | 2 +- .../ORM/Persisters/ManyToManyPersister.php | 16 +- .../ORM/Persisters/OneToManyPersister.php | 20 +- lib/Doctrine/ORM/Proxy/ProxyFactory.php | 33 +- lib/Doctrine/ORM/Query.php | 45 +- lib/Doctrine/ORM/Query/SqlWalker.php | 3 +- lib/Doctrine/ORM/UnitOfWork.php | 171 +++- .../EventListener/CacheMetadataListener.php | 35 + tests/Doctrine/Tests/Mocks/CacheEntryMock.php | 10 + tests/Doctrine/Tests/Mocks/CacheKeyMock.php | 14 + .../Doctrine/Tests/Mocks/CacheRegionMock.php | 77 ++ .../Tests/Mocks/ConcurrentRegionMock.php | 150 ++++ .../Tests/Models/Cache/Attraction.php | 95 ++ .../Models/Cache/AttractionContactInfo.php | 33 + .../Tests/Models/Cache/AttractionInfo.php | 54 ++ .../Models/Cache/AttractionLocationInfo.php | 33 + tests/Doctrine/Tests/Models/Cache/Bar.php | 11 + tests/Doctrine/Tests/Models/Cache/Beach.php | 11 + tests/Doctrine/Tests/Models/Cache/City.php | 109 +++ tests/Doctrine/Tests/Models/Cache/Country.php | 50 ++ tests/Doctrine/Tests/Models/Cache/Flight.php | 65 ++ .../Tests/Models/Cache/Restaurant.php | 11 + tests/Doctrine/Tests/Models/Cache/State.php | 92 ++ tests/Doctrine/Tests/Models/Cache/Travel.php | 112 +++ .../Doctrine/Tests/Models/Cache/Traveler.php | 91 ++ .../Tests/ORM/Cache/AbstractRegionTest.php | 85 ++ .../Doctrine/Tests/ORM/Cache/CacheKeyTest.php | 68 ++ .../ORM/Cache/DefaultCacheFactoryTest.php | 262 ++++++ .../Tests/ORM/Cache/DefaultCacheTest.php | 263 ++++++ .../Cache/DefaultCollectionHydratorTest.php | 77 ++ .../ORM/Cache/DefaultEntityHydratorTest.php | 125 +++ .../Tests/ORM/Cache/DefaultQueryCacheTest.php | 526 +++++++++++ .../Tests/ORM/Cache/DefaultRegionTest.php | 50 ++ .../Tests/ORM/Cache/FileLockRegionTest.php | 251 ++++++ .../AbstractCollectionPersisterTest.php | 301 +++++++ .../Persister/AbstractEntityPersisterTest.php | 416 +++++++++ ...ReadWriteCachedCollectionPersisterTest.php | 23 + ...rictReadWriteCachedEntityPersisterTest.php | 140 +++ .../ReadOnlyCachedCollectionPersisterTest.php | 22 + .../ReadOnlyCachedEntityPersisterTest.php | 36 + ...ReadWriteCachedCollectionPersisterTest.php | 301 +++++++ .../ReadWriteCachedEntityPersisterTest.php | 234 +++++ .../Doctrine/Tests/ORM/ConfigurationTest.php | 15 + .../ORM/Functional/DefaultValuesTest.php | 3 + .../Functional/ExtraLazyCollectionTest.php | 6 + .../JoinedTableCompositeKeyTest.php | 2 +- .../Functional/OneToOneEagerLoadingTest.php | 9 + .../Tests/ORM/Functional/SQLFilterTest.php | 2 + .../SecondLevelCacheAbstractTest.php | 205 +++++ ...SecondLevelCacheCompositPrimaryKeyTest.php | 176 ++++ .../SecondLevelCacheConcurrentTest.php | 139 +++ ...econdLevelCacheExtraLazyCollectionTest.php | 82 ++ ...condLevelCacheJoinTableInheritanceTest.php | 192 ++++ .../SecondLevelCacheManyToManyTest.php | 214 +++++ .../SecondLevelCacheManyToOneTest.php | 121 +++ .../SecondLevelCacheOneToManyTest.php | 351 ++++++++ .../SecondLevelCacheQueryCacheTest.php | 843 ++++++++++++++++++ .../SecondLevelCacheRepositoryTest.php | 128 +++ ...ndLevelCacheSingleTableInheritanceTest.php | 209 +++++ .../ORM/Functional/SecondLevelCacheTest.php | 351 ++++++++ .../SingleTableCompositeKeyTest.php | 2 +- .../ORM/Functional/Ticket/DDC117Test.php | 8 + .../ORM/Functional/Ticket/DDC1301Test.php | 3 + .../ORM/Functional/Ticket/DDC1595Test.php | 1 + .../ORM/Functional/Ticket/DDC2012Test.php | 1 + .../ORM/Functional/Ticket/DDC2090Test.php | 1 + .../ORM/Functional/Ticket/DDC2350Test.php | 1 + .../ORM/Functional/Ticket/DDC2494Test.php | 1 + .../ORM/Functional/Ticket/DDC742Test.php | 3 + .../ORM/Mapping/AbstractMappingDriverTest.php | 30 +- .../php/Doctrine.Tests.Models.Cache.City.php | 54 ++ .../Doctrine.Tests.Models.Cache.City.dcm.xml | 23 + .../Doctrine.Tests.Models.Cache.City.dcm.yml | 36 + .../ORM/Performance/SecondLevelCacheTest.php | 284 ++++++ .../Doctrine/Tests/OrmFunctionalTestCase.php | 61 +- tests/Doctrine/Tests/OrmTestCase.php | 58 ++ tests/travis/mysql.travis.xml | 8 +- tests/travis/pgsql.travis.xml | 9 +- tests/travis/sqlite.travis.xml | 9 +- 141 files changed, 14620 insertions(+), 380 deletions(-) create mode 100644 docs/en/reference/second-level-cache.rst create mode 100644 lib/Doctrine/ORM/Cache.php create mode 100644 lib/Doctrine/ORM/Cache/CacheEntry.php create mode 100644 lib/Doctrine/ORM/Cache/CacheException.php create mode 100644 lib/Doctrine/ORM/Cache/CacheFactory.php create mode 100644 lib/Doctrine/ORM/Cache/CacheKey.php create mode 100644 lib/Doctrine/ORM/Cache/CollectionCacheEntry.php create mode 100644 lib/Doctrine/ORM/Cache/CollectionCacheKey.php create mode 100644 lib/Doctrine/ORM/Cache/CollectionHydrator.php create mode 100644 lib/Doctrine/ORM/Cache/ConcurrentRegion.php create mode 100644 lib/Doctrine/ORM/Cache/DefaultCache.php create mode 100644 lib/Doctrine/ORM/Cache/DefaultCacheFactory.php create mode 100644 lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php create mode 100644 lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php create mode 100644 lib/Doctrine/ORM/Cache/DefaultQueryCache.php create mode 100644 lib/Doctrine/ORM/Cache/EntityCacheEntry.php create mode 100644 lib/Doctrine/ORM/Cache/EntityCacheKey.php create mode 100644 lib/Doctrine/ORM/Cache/EntityHydrator.php create mode 100644 lib/Doctrine/ORM/Cache/Lock.php create mode 100644 lib/Doctrine/ORM/Cache/LockException.php create mode 100644 lib/Doctrine/ORM/Cache/Logging/CacheLogger.php create mode 100644 lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/CachedCollectionPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/CachedEntityPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/CachedPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedCollectionPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedEntityPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php create mode 100644 lib/Doctrine/ORM/Cache/QueryCache.php create mode 100644 lib/Doctrine/ORM/Cache/QueryCacheEntry.php create mode 100644 lib/Doctrine/ORM/Cache/QueryCacheKey.php create mode 100644 lib/Doctrine/ORM/Cache/QueryCacheValidator.php create mode 100644 lib/Doctrine/ORM/Cache/Region.php create mode 100644 lib/Doctrine/ORM/Cache/Region/DefaultRegion.php create mode 100644 lib/Doctrine/ORM/Cache/Region/FileLockRegion.php create mode 100644 lib/Doctrine/ORM/Cache/TimestampQueryCacheValidator.php create mode 100644 lib/Doctrine/ORM/Mapping/Cache.php create mode 100644 lib/Doctrine/ORM/Persisters/CollectionPersister.php create mode 100644 lib/Doctrine/ORM/Persisters/EntityPersister.php create mode 100644 tests/Doctrine/Tests/EventListener/CacheMetadataListener.php create mode 100644 tests/Doctrine/Tests/Mocks/CacheEntryMock.php create mode 100644 tests/Doctrine/Tests/Mocks/CacheKeyMock.php create mode 100644 tests/Doctrine/Tests/Mocks/CacheRegionMock.php create mode 100644 tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Attraction.php create mode 100644 tests/Doctrine/Tests/Models/Cache/AttractionContactInfo.php create mode 100644 tests/Doctrine/Tests/Models/Cache/AttractionInfo.php create mode 100644 tests/Doctrine/Tests/Models/Cache/AttractionLocationInfo.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Bar.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Beach.php create mode 100644 tests/Doctrine/Tests/Models/Cache/City.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Country.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Flight.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Restaurant.php create mode 100644 tests/Doctrine/Tests/Models/Cache/State.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Travel.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Traveler.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/AbstractRegionTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/CacheKeyTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultCollectionHydratorTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultRegionTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/AbstractEntityPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/ReadOnlyCachedCollectionPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/ReadOnlyCachedEntityPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedCollectionPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheTest.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Cache.City.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Cache.City.dcm.xml create mode 100644 tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Cache.City.dcm.yml create mode 100644 tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php diff --git a/.travis.yml b/.travis.yml index 6c35e4d80..f06fdbc79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,9 +7,12 @@ php: - hhvm 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" @@ -19,7 +22,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 eb995278e..2df33766b 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -79,6 +79,7 @@ Advanced Topics * :doc:`Best Practices ` * :doc:`Metadata Drivers ` * :doc:`Batch Processing ` +* :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 000000000..7139a1c38 --- /dev/null +++ b/docs/en/reference/second-level-cache.rst @@ -0,0 +1,804 @@ +The Second Level Cache +====================== + +The Second Level Cache is designed to reduce 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 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, misses and puts. + +``\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, entity values are actually stored in the 2nd level cache. + +.. note:: + + Query cache should always be used in conjunction with the second-level-cache for those entities which should be cached. + +.. 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. +By default, Doctrine provides a 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 1a331fa23..9d5553704 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 f9c774d57..493bfa128 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 4c1f9ac64..ae366e930 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -23,8 +23,10 @@ use Doctrine\Common\Util\ClassUtils; 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 @@ abstract class AbstractQuery 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. * @@ -747,11 +884,10 @@ abstract class AbstractQuery $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); } /** @@ -763,6 +899,23 @@ abstract class AbstractQuery * @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); @@ -804,15 +957,52 @@ abstract class AbstractQuery 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 @@ -886,4 +1076,26 @@ abstract class AbstractQuery $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 000000000..fe0df9702 --- /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 000000000..cdcb6c0fb --- /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 000000000..5b548fe5e --- /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 000000000..89d54d43f --- /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 000000000..ede203091 --- /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 000000000..4bb329a7e --- /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 000000000..184fa9bf2 --- /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 000000000..9b3141dd5 --- /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 000000000..7bb50086b --- /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 000000000..92f0c6396 --- /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 000000000..5ce9b5b7a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -0,0 +1,202 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache; +use Doctrine\ORM\Cache\Region; +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; + } + + /** + * @param \Doctrine\ORM\Cache\Region $region + */ + public function setRegion(Region $region) + { + $this->regions[$region->getName()] = $region; + } + + /** + * {@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 000000000..fb4a45d2f --- /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 000000000..fede42d51 --- /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 000000000..8c13d7419 --- /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} + */ + 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} + */ + public function put(QueryCacheKey $key, ResultSetMapping $rsm, array $result) + { + if ($rsm->scalarMappings) { + throw new CacheException("Second level cache does not suport scalar results."); + } + + if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) { + throw new CacheException("Second level cache does not support partial entities."); + } + + 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 000000000..73ce222b3 --- /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 000000000..7729691a6 --- /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 000000000..3e382cd75 --- /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 000000000..5346aa323 --- /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 000000000..d4c76240a --- /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 000000000..bdae4eea8 --- /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 000000000..283bd4512 --- /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 000000000..23a249940 --- /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 000000000..25e4bd559 --- /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 000000000..2318c5906 --- /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 000000000..adf5fa0c6 --- /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 000000000..89afd3209 --- /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 000000000..619a520b3 --- /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 000000000..281f70562 --- /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 000000000..20890e87d --- /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 000000000..69db37f60 --- /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 000000000..573257bdd --- /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 000000000..e49c22541 --- /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 000000000..7bdc0d337 --- /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 000000000..46e0d603f --- /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 000000000..36126582a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCacheKey.php @@ -0,0 +1,52 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * A key that identifies a particular query. + * + * @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 000000000..b4dfa79aa --- /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 000000000..16609400f --- /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 000000000..0b62c5b81 --- /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 000000000..680f1940b --- /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 000000000..c213beefe --- /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 7ab147f98..be57376e2 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -35,6 +35,10 @@ use Doctrine\ORM\Mapping\NamingStrategy; 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; /** * Configuration container for all configuration options of Doctrine. @@ -233,6 +237,141 @@ class Configuration extends \Doctrine\DBAL\Configuration : 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 setSecondLevelCacheQueryValidator(QueryCacheValidator $validator) + { + $this->_attributes['secondLevelCacheQueryValidator'] = $validator; + } + /** * Gets the cache driver implementation that is used for the query cache (SQL cache). * @@ -696,6 +835,38 @@ class Configuration extends \Doctrine\DBAL\Configuration : '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 dc123118f..1a768f953 100644 --- a/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php +++ b/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php @@ -268,4 +268,12 @@ abstract class EntityManagerDecorator extends ObjectManagerDecorator implements { 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 a00697eb3..ef2b58fd0 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -131,6 +131,11 @@ use Doctrine\Common\Util\ClassUtils; */ 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. @@ -159,6 +164,11 @@ use Doctrine\Common\Util\ClassUtils; $config->getProxyNamespace(), $config->getAutoGenerateProxyClasses() ); + + if ($config->isSecondLevelCacheEnabled()) { + $cacheClass = $config->getSecondLevelCacheClassName(); + $this->cache = new $cacheClass($this); + } } /** @@ -199,6 +209,14 @@ use Doctrine\Common\Util\ClassUtils; $this->conn->beginTransaction(); } + /** + * {@inheritDoc} + */ + public function getCache() + { + return $this->cache; + } + /** * {@inheritDoc} */ @@ -405,7 +423,7 @@ use Doctrine\Common\Util\ClassUtils; 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 68f680c62..fa8ec902a 100644 --- a/lib/Doctrine/ORM/EntityManagerInterface.php +++ b/lib/Doctrine/ORM/EntityManagerInterface.php @@ -30,6 +30,13 @@ use Doctrine\ORM\Query\ResultSetMapping; */ interface EntityManagerInterface extends ObjectManager { + /** + * Returns the cache API for managing the second level cache regions or NULL if the cache is not anabled. + * + * @return \Doctrine\ORM\Cache|null + */ + public function getCache(); + /** * Gets the database connection object used by the EntityManager. * diff --git a/lib/Doctrine/ORM/Mapping/Cache.php b/lib/Doctrine/ORM/Mapping/Cache.php new file mode 100644 index 000000000..560a9c4eb --- /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 63c36474a..dc2a80a0d 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -144,6 +144,10 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory $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 86c7d9818..6337326d4 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -26,7 +26,6 @@ use Doctrine\DBAL\Types\Type; 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 @@ class ClassMetadataInfo implements ClassMetadata $serialized[] = "customGeneratorDefinition"; } + if ($this->cache) { + $serialized[] = "cache"; + } + return $serialized; } @@ -979,6 +1002,44 @@ class ClassMetadataInfo implements ClassMetadata 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 f9aaddba7..359f26322 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -128,6 +128,16 @@ class AnnotationDriver extends AbstractAnnotationDriver $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 @@ class AnnotationDriver extends AbstractAnnotationDriver $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 14abadb9e..611087a1c 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -64,4 +64,5 @@ require_once __DIR__.'/../AssociationOverride.php'; 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 6e024d041..778f44d84 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -81,6 +81,11 @@ class XmlDriver extends FileDriver $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 @@ class XmlDriver extends FileDriver } $metadata->mapOneToOne($mapping); + + // Evaluate second level cache + if (isset($oneToOneElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement->{'cache'})); + } } } @@ -388,6 +398,11 @@ class XmlDriver extends FileDriver } $metadata->mapOneToMany($mapping); + + // Evaluate second level cache + if (isset($oneToManyElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement->{'cache'})); + } } } @@ -428,6 +443,11 @@ class XmlDriver extends FileDriver } $metadata->mapManyToOne($mapping); + + // Evaluate second level cache + if (isset($manyToOneElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement->{'cache'})); + } } } @@ -493,6 +513,11 @@ class XmlDriver extends FileDriver } $metadata->mapManyToMany($mapping); + + // Evaluate second level cache + if (isset($manyToManyElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement->{'cache'})); + } } } @@ -701,6 +726,22 @@ class XmlDriver extends FileDriver 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 1e7aa3356..80b2d6a2a 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -72,9 +72,16 @@ class YamlDriver extends FileDriver // 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 @@ class YamlDriver extends FileDriver } $metadata->mapOneToOne($mapping); + + // Evaluate second level cache + if (isset($oneToOneElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement['cache'])); + } } } @@ -394,6 +406,11 @@ class YamlDriver extends FileDriver } $metadata->mapOneToMany($mapping); + + // Evaluate second level cache + if (isset($oneToManyElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement['cache'])); + } } } @@ -438,6 +455,11 @@ class YamlDriver extends FileDriver } $metadata->mapManyToOne($mapping); + + // Evaluate second level cache + if (isset($manyToOneElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement['cache'])); + } } } @@ -506,6 +528,11 @@ class YamlDriver extends FileDriver } $metadata->mapManyToMany($mapping); + + // Evaluate second level cache + if (isset($manyToManyElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement['cache'])); + } } } @@ -704,6 +731,22 @@ class YamlDriver extends FileDriver 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 99333f034..8349c4f6d 100644 --- a/lib/Doctrine/ORM/ORMException.php +++ b/lib/Doctrine/ORM/ORMException.php @@ -100,6 +100,20 @@ class ORMException extends Exception 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 @@ class ORMException extends Exception 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 fdc54aee1..61222318f 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php @@ -28,7 +28,7 @@ use Doctrine\ORM\PersistentCollection; * @since 2.0 * @author Roman Borschel */ -abstract class AbstractCollectionPersister +abstract class AbstractCollectionPersister implements CollectionPersister { /** * @var EntityManager @@ -74,11 +74,7 @@ abstract class AbstractCollectionPersister } /** - * 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 @@ abstract class AbstractCollectionPersister 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 class AbstractCollectionPersister 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 @@ abstract class AbstractCollectionPersister } /** - * Deletes rows. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return void + * {@inheritdoc} */ public function deleteRows(PersistentCollection $coll) { @@ -150,11 +135,7 @@ abstract class AbstractCollectionPersister } /** - * Inserts rows. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return void + * {@inheritdoc} */ public function insertRows(PersistentCollection $coll) { @@ -167,13 +148,7 @@ abstract class AbstractCollectionPersister } /** - * 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 @@ abstract class AbstractCollectionPersister } /** - * 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 @@ abstract class AbstractCollectionPersister } /** - * 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 @@ abstract class AbstractCollectionPersister } /** - * 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 @@ abstract class AbstractCollectionPersister } /** - * 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 @@ abstract class AbstractCollectionPersister } /** - * 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 @@ abstract class AbstractCollectionPersister } /** - * 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 2540425a2..32488aa09 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -78,7 +78,7 @@ use Doctrine\Common\Collections\Expr\Comparison; * @author Fabio B. Silva * @since 2.0 */ -class BasicEntityPersister +class BasicEntityPersister implements EntityPersister { /** * @var array @@ -223,7 +223,7 @@ class BasicEntityPersister } /** - * @return \Doctrine\ORM\Mapping\ClassMetadata + * {@inheritdoc} */ public function getClassMetadata() { @@ -231,12 +231,15 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * 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 @@ class BasicEntityPersister } /** - * Gets an SQL column alias for a column name. - * - * @param string $columnName - * - * @return string + * {@inheritdoc} */ public function getSQLColumnAlias($columnName) { diff --git a/lib/Doctrine/ORM/Persisters/CollectionPersister.php b/lib/Doctrine/ORM/Persisters/CollectionPersister.php new file mode 100644 index 000000000..f8d194442 --- /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 000000000..a27a59d14 --- /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 1fce9c701..00d03b51c 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -296,7 +296,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister /** * {@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 1ec6e5e7c..14664d3ce 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -257,11 +257,7 @@ class ManyToManyPersister extends AbstractCollectionPersister } /** - * @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 @@ class ManyToManyPersister extends AbstractCollectionPersister } /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return boolean + * {@inheritdoc} */ public function contains(PersistentCollection $coll, $element) { @@ -300,10 +293,7 @@ class ManyToManyPersister extends AbstractCollectionPersister } /** - * @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 2915fd2ed..6c0c1e78e 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 @@ class OneToManyPersister extends AbstractCollectionPersister } /** - * @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 @@ class OneToManyPersister extends AbstractCollectionPersister 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 @@ class OneToManyPersister extends AbstractCollectionPersister } /** - * @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 dfac9a3e7..d33a590f0 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -26,7 +26,7 @@ use Doctrine\Common\Util\ClassUtils; 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 @@ class ProxyFactory extends AbstractProxyFactory * 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 @@ class ProxyFactory extends AbstractProxyFactory $properties = $proxy->__getLazyProperties(); foreach ($properties as $propertyName => $property) { - if (!isset($proxy->$propertyName)) { + if ( ! isset($proxy->$propertyName)) { $proxy->$propertyName = $properties[$propertyName]; } } @@ -138,7 +138,7 @@ class ProxyFactory extends AbstractProxyFactory $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 @@ class ProxyFactory extends AbstractProxyFactory $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 @@ class ProxyFactory extends AbstractProxyFactory * 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 @@ class ProxyFactory extends AbstractProxyFactory $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 71f5f5550..de8411df0 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 @@ final class Query extends AbstractQuery 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 @@ final class Query extends AbstractQuery 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 @@ final class Query extends AbstractQuery ); } + /** + * {@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 37f970843..0852664ec 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -401,12 +401,13 @@ class SqlWalker implements TreeWalker 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 ecc2268e7..6e7ea488b 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -38,6 +38,13 @@ use Doctrine\ORM\Event\OnFlushEventArgs; 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 @@ class UnitOfWork implements PropertyChangedListener $this->em = $em; $this->evm = $em->getEventManager(); $this->listenersInvoker = new ListenersInvoker($em); + $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled(); } /** @@ -351,6 +364,7 @@ class UnitOfWork implements PropertyChangedListener 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 @@ class UnitOfWork implements PropertyChangedListener $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 @@ class UnitOfWork implements PropertyChangedListener // 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 @@ class UnitOfWork implements PropertyChangedListener /** * 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 @@ class UnitOfWork implements PropertyChangedListener $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 @@ class UnitOfWork implements PropertyChangedListener $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 @@ class UnitOfWork implements PropertyChangedListener $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate); foreach ($this->entityUpdates as $oid => $entity) { + if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { continue; } @@ -2591,6 +2608,14 @@ class UnitOfWork implements PropertyChangedListener 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? @@ -2697,6 +2722,22 @@ class UnitOfWork implements PropertyChangedListener 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); @@ -2942,7 +2983,7 @@ class UnitOfWork implements PropertyChangedListener * * @param string $entityName The name of the Entity. * - * @return \Doctrine\ORM\Persisters\BasicEntityPersister + * @return \Doctrine\ORM\Persisters\EntityPersister */ public function getEntityPersister($entityName) { @@ -2954,21 +2995,27 @@ class UnitOfWork implements PropertyChangedListener 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]; @@ -2979,29 +3026,31 @@ class UnitOfWork implements PropertyChangedListener * * @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]; } /** @@ -3194,6 +3243,50 @@ class UnitOfWork implements PropertyChangedListener 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 000000000..9f5f4cc39 --- /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 000000000..702295cda --- /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 000000000..06a40c2eb --- /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 000000000..a84672bea --- /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 000000000..08e84487e --- /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 000000000..76ef305a4 --- /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 000000000..418ef49b0 --- /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 000000000..ebdb592ae --- /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 000000000..f0d093127 --- /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 000000000..33a9cf40c --- /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 000000000..a95caab24 --- /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 000000000..00d3ac061 --- /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 000000000..75e3275a4 --- /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 000000000..ebc5b239c --- /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 000000000..ca01b069d --- /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 000000000..e0fd5d2a4 --- /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 000000000..d52196d44 --- /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 000000000..eda38cfe7 --- /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 000000000..74082d8a7 --- /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 000000000..4c44430b0 --- /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 000000000..8cc1c54ef --- /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 000000000..e8f7764b5 --- /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 000000000..94cd3985d --- /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 000000000..d0f76deb7 --- /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 000000000..ed4093a88 --- /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 000000000..2a2b62692 --- /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 000000000..d01a8f214 --- /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 000000000..4c7014066 --- /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 000000000..1b6ed6183 --- /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 8405c3035..1530a05a3 100644 --- a/tests/Doctrine/Tests/ORM/ConfigurationTest.php +++ b/tests/Doctrine/Tests/ORM/ConfigurationTest.php @@ -272,6 +272,21 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase $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 323d8bf4a..dc148707c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php @@ -23,6 +23,9 @@ class DefaultValuesTest extends \Doctrine\Tests\OrmFunctionalTestCase } } + /** + * @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 eb4dbe4d8..f6c816037 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -58,6 +58,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-546 + * @group non-cacheable */ public function testCountNotInitializesCollection() { @@ -93,6 +94,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-546 + * @group non-cacheable */ public function testCountWhenInitialized() { @@ -143,6 +145,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-546 + * @group non-cacheable */ public function testSlice() { @@ -173,6 +176,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-546 + * @group non-cacheable */ public function testSliceInitializedCollection() { @@ -505,6 +509,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-1462 + * @group non-cacheable */ public function testSliceOnDirtyCollection() { @@ -526,6 +531,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @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 3bc5e25ad..8f6e0cc59 100644 --- a/tests/Doctrine/Tests/ORM/Functional/JoinedTableCompositeKeyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/JoinedTableCompositeKeyTest.php @@ -30,7 +30,7 @@ class JoinedTableCompositeKeyTest extends OrmFunctionalTestCase } /** - * + * @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 db62fa1ba..ad00c3993 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -117,6 +117,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertNotNull($waggon->train); } + /** + * @group non-cacheable + */ public function testEagerLoadWithNullableColumnsGeneratesLeftJoinOnBothSides() { $train = new Train(new TrainOwner("Alexander")); @@ -141,6 +144,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase ); } + /** + * @group non-cacheable + */ public function testEagerLoadWithNonNullableColumnsGeneratesInnerJoinOnOwningSide() { $waggon = new Waggon(); @@ -168,6 +174,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase ); } + /** + * @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 1f94d86a3..4e9390cc6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php @@ -31,6 +31,8 @@ require_once __DIR__ . '/../../TestInit.php'; * 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 000000000..fa8e20b53 --- /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 000000000..5c58b0aaa --- /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 000000000..ed35992c8 --- /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 000000000..b96c41e1f --- /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 000000000..b063cd319 --- /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 000000000..9b1b993a0 --- /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 000000000..c271434be --- /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 000000000..931ba88ba --- /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 000000000..b7ebca196 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -0,0 +1,843 @@ +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')); + } + + /** + * @expectedException \Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Second level cache does not support partial entities. + */ + public function testCacheablePartialQueryException() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->_em->createQuery("SELECT PARTIAL c.{id} FROM Doctrine\Tests\Models\Cache\Country c") + ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) + ->setCacheable(true) + ->getResult(); + } +} \ 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 000000000..ca546bb10 --- /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 000000000..98c822ca5 --- /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 000000000..37dadd26c --- /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 3d1fe7714..53829a1c0 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SingleTableCompositeKeyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SingleTableCompositeKeyTest.php @@ -30,7 +30,7 @@ class SingleTableCompositeKeyTest extends OrmFunctionalTestCase } /** - * + * @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 6673afd1a..17ecbafd6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php @@ -12,6 +12,9 @@ use Doctrine\Tests\Models\DDC117\DDC117Link; require_once __DIR__ . '/../../../TestInit.php'; +/** + * @group DDC-117 + */ class DDC117Test extends \Doctrine\Tests\OrmFunctionalTestCase { private $article1; @@ -138,6 +141,7 @@ class DDC117Test extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-117 + * @group non-cacheable */ public function testDqlRemoveCompositeElement() { @@ -471,6 +475,10 @@ class DDC117Test extends \Doctrine\Tests\OrmFunctionalTestCase */ 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 94d02f905..502101bf9 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1301Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1301Test.php @@ -8,6 +8,9 @@ require_once __DIR__ . '/../../../TestInit.php'; /** * @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 e6bc58942..2c6317504 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1595Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1595Test.php @@ -5,6 +5,7 @@ namespace Doctrine\Tests\ORM\Functional\Ticket; /** * @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 eca15cea3..ac93906da 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2012Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2012Test.php @@ -9,6 +9,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; /** * @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 c5da3deae..cd6db8857 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2090Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2090Test.php @@ -6,6 +6,7 @@ use Doctrine\Tests\Models\Company\CompanyEmployee; /** * @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 229bbff52..75f34a25c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2350Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2350Test.php @@ -6,6 +6,7 @@ use Doctrine\Tests\OrmFunctionalTestCase; /** * @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 1b2ca881e..b5a09f922 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php @@ -7,6 +7,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; /** * @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 94936a125..334c2c623 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC742Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC742Test.php @@ -6,6 +6,9 @@ use Doctrine\Common\Collections\ArrayCollection; 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 28e6f1ede..a30845777 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php @@ -6,7 +6,7 @@ use Doctrine\ORM\Events; 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 @@ abstract class AbstractMappingDriverTest extends \Doctrine\Tests\OrmTestCase $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 000000000..6dd477fa6 --- /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 000000000..84b786a7b --- /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 000000000..05286e0df --- /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 000000000..3e628acbd --- /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 d2a41cfb9..dba7bd09f 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 @@ abstract class OrmFunctionalTestCase extends OrmTestCase $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 @@ abstract class OrmFunctionalTestCase extends OrmTestCase $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 @@ abstract class OrmFunctionalTestCase extends OrmTestCase } } + 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 9ba32cf8b..541e094a3 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 @@ abstract class OrmTestCase extends DoctrineTestCase $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 @@ abstract class OrmTestCase extends DoctrineTestCase 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 @@ abstract class OrmTestCase extends DoctrineTestCase 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 82559afdf..e278e8859 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 b92f775aa..a2993fcb1 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 a4c400caa..242d4dea8 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 From 1bfa8f0fc322d3cd5702eb57385915e415d7988f Mon Sep 17 00:00:00 2001 From: fabios Date: Thu, 3 Oct 2013 13:55:55 -0400 Subject: [PATCH 2/9] Extract cache config --- doctrine-mapping.xsd | 10 +- lib/Doctrine/ORM/AbstractQuery.php | 7 +- lib/Doctrine/ORM/Cache/CacheConfiguration.php | 159 ++++++++++++ lib/Doctrine/ORM/Cache/CacheFactory.php | 8 +- lib/Doctrine/ORM/Cache/DefaultCache.php | 4 +- .../ORM/Cache/DefaultCacheFactory.php | 18 +- .../ORM/Cache/DefaultCollectionHydrator.php | 2 +- .../ORM/Cache/DefaultEntityHydrator.php | 2 +- lib/Doctrine/ORM/Cache/DefaultQueryCache.php | 4 +- .../Persister/AbstractCollectionPersister.php | 13 +- .../Persister/AbstractEntityPersister.php | 19 +- ...onStrictReadWriteCachedEntityPersister.php | 4 +- .../ORM/Cache/RegionsConfiguration.php | 138 +++++++++++ lib/Doctrine/ORM/Configuration.php | 232 +++++------------- lib/Doctrine/ORM/EntityManager.php | 2 +- lib/Doctrine/ORM/UnitOfWork.php | 6 +- .../Tests/ORM/Cache/CacheConfigTest.php | 82 +++++++ .../ORM/Cache/DefaultCacheFactoryTest.php | 21 +- .../Tests/ORM/Cache/DefaultQueryCacheTest.php | 4 +- .../Doctrine/Tests/ORM/ConfigurationTest.php | 13 +- .../SecondLevelCacheConcurrentTest.php | 4 +- .../Doctrine/Tests/OrmFunctionalTestCase.php | 16 +- tests/Doctrine/Tests/OrmTestCase.php | 11 +- 23 files changed, 532 insertions(+), 247 deletions(-) create mode 100644 lib/Doctrine/ORM/Cache/CacheConfiguration.php create mode 100644 lib/Doctrine/ORM/Cache/RegionsConfiguration.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/CacheConfigTest.php diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index 493bfa128..345fe4898 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -167,7 +167,7 @@ - + @@ -459,7 +459,7 @@ - + @@ -477,7 +477,7 @@ - + @@ -493,7 +493,7 @@ - + @@ -512,7 +512,7 @@ - + diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index ae366e930..cfc3e22dd 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -162,7 +162,12 @@ abstract class AbstractQuery { $this->_em = $em; $this->parameters = new ArrayCollection(); - $this->cacheLogger = $em->getConfiguration()->getSecondLevelCacheLogger(); + + if ($this->_em->getConfiguration()->isSecondLevelCacheEnabled()) { + $this->cacheLogger = $em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->getCacheLogger(); + } } /** diff --git a/lib/Doctrine/ORM/Cache/CacheConfiguration.php b/lib/Doctrine/ORM/Cache/CacheConfiguration.php new file mode 100644 index 000000000..78540f3f7 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheConfiguration.php @@ -0,0 +1,159 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\ORMException; +use Doctrine\ORM\Cache\CacheFactory; +use Doctrine\ORM\Cache\Logging\CacheLogger; +use Doctrine\ORM\Cache\QueryCacheValidator; +use Doctrine\ORM\Cache\TimestampQueryCacheValidator; + +/** + * Configuration container for second-level cache. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CacheConfiguration +{ + /** + * @var \Doctrine\ORM\Cache\CacheFactory + */ + private $cacheFactory; + + /** + * @var \Doctrine\ORM\Cache\RegionsConfiguration + */ + private $regionsConfig; + + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + private $cacheLogger; + + /** + * @var \Doctrine\ORM\Cache\QueryCacheValidator + */ + private $queryValidator; + + /** + * @var string + */ + private $cacheClassName = 'Doctrine\ORM\Cache\DefaultCache'; + + /** + * @return \Doctrine\ORM\Cache\CacheFactory|null + */ + public function getCacheFactory() + { + return $this->cacheFactory; + } + + /** + * @param \Doctrine\ORM\Cache\CacheFactory $factory + * + * @return void + */ + public function setCacheFactory(CacheFactory $factory) + { + $this->cacheFactory = $factory; + } + + /** + * @return \Doctrine\ORM\Cache\Logging\CacheLogger|null + */ + public function getCacheLogger() + { + return $this->cacheLogger; + } + + /** + * @param \Doctrine\ORM\Cache\Logging\CacheLogger $logger + */ + public function setCacheLogger(CacheLogger $logger) + { + $this->cacheLogger = $logger; + } + + /** + * @return \Doctrine\ORM\Cache\QueryCacheValidator + */ + public function getRegionsConfiguration() + { + if ($this->regionsConfig == null) { + $this->regionsConfig = new RegionsConfiguration(); + } + + return $this->regionsConfig; + } + + /** + * @param \Doctrine\ORM\Cache\RegionsConfiguration $regionsConfig + */ + public function setRegionsConfiguration(RegionsConfiguration $regionsConfig) + { + $this->regionsConfig = $regionsConfig; + } + + /** + * @return \Doctrine\ORM\Cache\QueryCacheValidator + */ + public function getQueryValidator() + { + if ($this->queryValidator == null) { + $this->queryValidator = new TimestampQueryCacheValidator(); + } + + return $this->queryValidator; + } + + /** + * @param \Doctrine\ORM\Cache\QueryCacheValidator $validator + */ + public function setQueryValidator(QueryCacheValidator $validator) + { + $this->queryValidator = $validator; + } + + /** + * @param string $className + * + * @throws ORMException If not is a \Doctrine\ORM\Cache + */ + public function setCacheClassName($className) + { + $reflectionClass = new \ReflectionClass($className); + + if ( ! $reflectionClass->implementsInterface('Doctrine\ORM\Cache')) { + throw ORMException::invalidSecondLevelCache($className); + } + + $this->cacheClassName = $className; + } + + /** + * @return string A \Doctrine\ORM\Cache class name + */ + public function getCacheClassName() + { + return $this->cacheClassName; + } +} diff --git a/lib/Doctrine/ORM/Cache/CacheFactory.php b/lib/Doctrine/ORM/Cache/CacheFactory.php index 89d54d43f..77bcb24a6 100644 --- a/lib/Doctrine/ORM/Cache/CacheFactory.php +++ b/lib/Doctrine/ORM/Cache/CacheFactory.php @@ -65,22 +65,22 @@ interface CacheFactory public function buildQueryCache(EntityManagerInterface $em, $regionName = null); /** - * Build an entity hidrator + * Build an entity hydrator * * @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. + * @return \Doctrine\ORM\Cache\EntityHydrator The built entity hydrator. */ public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata); /** - * Build a collection hidrator + * Build a collection hydrator * * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. * @param array $mapping The association mapping. * - * @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hidrator. + * @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hydrator. */ public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping); diff --git a/lib/Doctrine/ORM/Cache/DefaultCache.php b/lib/Doctrine/ORM/Cache/DefaultCache.php index 92f0c6396..1e76653d9 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultCache.php @@ -69,7 +69,9 @@ class DefaultCache implements Cache { $this->em = $em; $this->uow = $em->getUnitOfWork(); - $this->cacheFactory = $em->getConfiguration()->getSecondLevelCacheFactory(); + $this->cacheFactory = $em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory(); } /** diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 5ce9b5b7a..360a97210 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM\Cache; use Doctrine\ORM\Cache; use Doctrine\ORM\Cache\Region; -use Doctrine\ORM\Configuration; +use Doctrine\ORM\Cache\RegionsConfiguration; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Cache\Region\DefaultRegion; @@ -50,9 +50,9 @@ class DefaultCacheFactory implements CacheFactory private $cache; /** - * @var \Doctrine\ORM\Configuration + * @var \Doctrine\ORM\Cache\RegionsConfiguration */ - private $configuration; + private $regionsConfig; /** * @var array @@ -65,13 +65,13 @@ class DefaultCacheFactory implements CacheFactory private $fileLockRegionDirectory; /** - * @param \Doctrine\ORM\Configuration $configuration - * @param \Doctrine\Common\Cache\Cache $cache + * @param \Doctrine\ORM\Cache\RegionsConfiguration $cacheConfig + * @param \Doctrine\Common\Cache\Cache $cache */ - public function __construct(Configuration $configuration, CacheDriver $cache) + public function __construct(RegionsConfiguration $cacheConfig, CacheDriver $cache) { $this->cache = $cache; - $this->configuration = $configuration; + $this->regionsConfig = $cacheConfig; } /** @@ -181,7 +181,7 @@ class DefaultCacheFactory implements CacheFactory } $region = new DefaultRegion($cache['region'], clone $this->cache, array( - 'lifetime' => $this->configuration->getSecondLevelCacheRegionLifetime($cache['region']) + 'lifetime' => $this->regionsConfig->getLifetime($cache['region']) )); if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { @@ -194,7 +194,7 @@ class DefaultCacheFactory implements CacheFactory } $directory = $this->fileLockRegionDirectory . DIRECTORY_SEPARATOR . $cache['region']; - $region = new FileLockRegion($region, $directory, $this->configuration->getSecondLevelCacheLockLifetime()); + $region = new FileLockRegion($region, $directory, $this->regionsConfig->getLockLifetime($cache['region'])); } return $this->regions[$cache['region']] = $region; diff --git a/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php b/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php index fb4a45d2f..53ab2c58f 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php @@ -28,7 +28,7 @@ use Doctrine\ORM\Cache\CollectionCacheKey; use Doctrine\ORM\Cache\CollectionCacheEntry; /** - * Default hidrator cache for collections + * Default hydrator cache for collections * * @since 2.5 * @author Fabio B. Silva diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index fede42d51..f022ae59f 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -28,7 +28,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Cache\EntityCacheEntry; /** - * Default hidrator cache for entities + * Default hydrator cache for entities * * @since 2.5 * @author Fabio B. Silva diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php index 8c13d7419..3ff1dd06b 100644 --- a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -75,7 +75,9 @@ class DefaultQueryCache implements QueryCache $this->em = $em; $this->region = $region; $this->uow = $em->getUnitOfWork(); - $this->validator = $em->getConfiguration()->getSecondLevelCacheQueryValidator(); + $this->validator = $em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->getQueryValidator(); } /** diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php index 23a249940..ca56a1bcc 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php @@ -82,7 +82,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister /** * @var \Doctrine\ORM\Cache\CollectionHydrator */ - protected $hidrator; + protected $hydrator; /** * @var \Doctrine\ORM\Cache\Logging\CacheLogger @@ -98,7 +98,8 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister public function __construct(CollectionPersister $persister, Region $region, EntityManagerInterface $em, array $association) { $configuration = $em->getConfiguration(); - $cacheFactory = $configuration->getSecondLevelCacheFactory(); + $cacheConfig = $configuration->getSecondLevelCacheConfiguration(); + $cacheFactory = $cacheConfig->getCacheFactory(); $this->region = $region; $this->persister = $persister; @@ -106,8 +107,8 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister $this->regionName = $region->getName(); $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); - $this->cacheLogger = $configuration->getSecondLevelCacheLogger(); - $this->hidrator = $cacheFactory->buildCollectionHydrator($em, $association); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->hydrator = $cacheFactory->buildCollectionHydrator($em, $association); $this->sourceEntity = $em->getClassMetadata($association['sourceEntity']); $this->targetEntity = $em->getClassMetadata($association['targetEntity']); } @@ -149,7 +150,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister return null; } - if (($cache = $this->hidrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection)) === null) { + if (($cache = $this->hydrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection)) === null) { return null; } @@ -164,7 +165,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister $targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName); $targetRegion = $targetPersister->getCacheRegion(); $targetHidrator = $targetPersister->getEntityHydrator(); - $entry = $this->hidrator->buildCacheEntry($this->targetEntity, $key, $elements); + $entry = $this->hydrator->buildCacheEntry($this->targetEntity, $key, $elements); foreach ($entry->identifiers as $index => $identifier) { $entityKey = new EntityCacheKey($this->targetEntity->rootEntityName, $identifier); diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php index 25e4bd559..ad113b609 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -72,7 +72,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister /** * @var \Doctrine\ORM\Cache\EntityHydrator */ - protected $hidrator; + protected $hydrator; /** * @var \Doctrine\ORM\Cache @@ -97,8 +97,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ public function __construct(EntityPersister $persister, Region $region, EntityManagerInterface $em, ClassMetadata $class) { - $config = $em->getConfiguration(); - $factory = $config->getSecondLevelCacheFactory(); + $configuration = $em->getConfiguration(); + $cacheConfig = $configuration->getSecondLevelCacheConfiguration(); + $cacheFactory = $cacheConfig->getCacheFactory(); $this->class = $class; $this->region = $region; @@ -107,8 +108,8 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $this->regionName = $region->getName(); $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); - $this->cacheLogger = $config->getSecondLevelCacheLogger(); - $this->hidrator = $factory->buildEntityHydrator($em, $class); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->hydrator = $cacheFactory->buildEntityHydrator($em, $class); } /** @@ -189,7 +190,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ public function getEntityHydrator() { - return $this->hidrator; + return $this->hydrator; } /** @@ -204,7 +205,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $class = $this->metadataFactory->getMetadataFor($className); } - $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); $cached = $this->region->put($key, $entry); if ($this->cacheLogger && $cached) { @@ -370,7 +371,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $class = $this->metadataFactory->getMetadataFor($cacheEntry->class); } - if (($entity = $this->hidrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity)) !== null) { + if (($entity = $this->hydrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity)) !== null) { if ($this->cacheLogger) { $this->cacheLogger->entityCacheHit($this->regionName, $cacheKey); @@ -393,7 +394,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $class = $this->metadataFactory->getMetadataFor($className); } - $cacheEntry = $this->hidrator->buildCacheEntry($class, $cacheKey, $entity); + $cacheEntry = $this->hydrator->buildCacheEntry($class, $cacheKey, $entity); $cached = $this->region->put($cacheKey, $cacheEntry); if ($this->cacheLogger && $cached) { diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php index 281f70562..f6420ca21 100644 --- a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php @@ -48,7 +48,7 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister } $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); - $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); $cached = $this->region->put($key, $entry); if ($this->cacheLogger && $cached) { @@ -68,7 +68,7 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister } $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); - $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); $cached = $this->region->put($key, $entry); if ($this->cacheLogger && $cached) { diff --git a/lib/Doctrine/ORM/Cache/RegionsConfiguration.php b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php new file mode 100644 index 000000000..58bcbdef9 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php @@ -0,0 +1,138 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Cache regions configuration + * + * @since 2.5 + * @author Fabio B. Silva + */ +class RegionsConfiguration +{ + /** + * @var array + */ + private $lifetimes; + + /** + * @var array + */ + private $lockLifetimes; + + /** + * @var integer + */ + private $defaultLifetime; + + /** + * @var integer + */ + private $defaultLockLifetime; + + /** + * @param integer $defaultLifetime + * @param integer $defaultLockLifetime + */ + public function __construct($defaultLifetime = 3600, $defaultLockLifetime = 60) + { + $this->defaultLifetime = $defaultLifetime; + $this->defaultLockLifetime = $defaultLockLifetime; + } + + /** + * @return integer + */ + public function getDefaultLifetime() + { + return $this->defaultLifetime; + } + + /** + * @param integer $defaultLifetime + */ + public function setDefaultLifetime($defaultLifetime) + { + $this->defaultLifetime = $defaultLifetime; + } + + /** + * @return integer + */ + public function getDefaultLockLifetime() + { + return $this->defaultLockLifetime; + } + + /** + * @param integer $defaultLockLifetime + */ + public function setDefaultLockLifetime($defaultLockLifetime) + { + $this->defaultLockLifetime = $defaultLockLifetime; + } + + /** + * @param string $regionName + * + * @return integer + */ + public function getLifetime($regionName) + { + if (isset($this->lifetimes[$regionName])) { + return $this->lifetimes[$regionName]; + } + + return $this->defaultLifetime; + } + + /** + * @param string $name + * @param integer $lifetime + */ + public function setLifetime($name, $lifetime) + { + $this->lifetimes[$name] = (integer) $lifetime; + } + + /** + * @param string $regionName + * + * @return integer + */ + public function getLockLifetime($regionName) + { + if (isset($this->lockLifetimes[$regionName])) { + return $this->lockLifetimes[$regionName]; + } + + return $this->defaultLockLifetime; + } + + /** + * @param string $name + * @param integer $lifetime + */ + public function setLockLifetime($name, $lifetime) + { + $this->lockLifetimes[$name] = (integer) $lifetime; + } +} diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index be57376e2..094f223a3 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -24,7 +24,8 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\SimpleAnnotationReader; use Doctrine\Common\Cache\ArrayCache; -use Doctrine\Common\Cache\Cache; +use Doctrine\Common\Cache\Cache as CacheDriver; +use Doctrine\ORM\Cache\CacheConfiguration; use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; use Doctrine\ORM\Mapping\DefaultEntityListenerResolver; use Doctrine\ORM\Mapping\DefaultNamingStrategy; @@ -35,10 +36,6 @@ use Doctrine\ORM\Mapping\NamingStrategy; 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; /** * Configuration container for all configuration options of Doctrine. @@ -237,141 +234,6 @@ class Configuration extends \Doctrine\DBAL\Configuration : 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 setSecondLevelCacheQueryValidator(QueryCacheValidator $validator) - { - $this->_attributes['secondLevelCacheQueryValidator'] = $validator; - } - /** * Gets the cache driver implementation that is used for the query cache (SQL cache). * @@ -391,7 +253,7 @@ class Configuration extends \Doctrine\DBAL\Configuration * * @return void */ - public function setQueryCacheImpl(Cache $cacheImpl) + public function setQueryCacheImpl(CacheDriver $cacheImpl) { $this->_attributes['queryCacheImpl'] = $cacheImpl; } @@ -415,7 +277,7 @@ class Configuration extends \Doctrine\DBAL\Configuration * * @return void */ - public function setHydrationCacheImpl(Cache $cacheImpl) + public function setHydrationCacheImpl(CacheDriver $cacheImpl) { $this->_attributes['hydrationCacheImpl'] = $cacheImpl; } @@ -439,7 +301,7 @@ class Configuration extends \Doctrine\DBAL\Configuration * * @return void */ - public function setMetadataCacheImpl(Cache $cacheImpl) + public function setMetadataCacheImpl(CacheDriver $cacheImpl) { $this->_attributes['metadataCacheImpl'] = $cacheImpl; } @@ -835,38 +697,6 @@ class Configuration extends \Doctrine\DBAL\Configuration : '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. * @@ -976,4 +806,56 @@ class Configuration extends \Doctrine\DBAL\Configuration ? $this->_attributes['repositoryFactory'] : new DefaultRepositoryFactory(); } + + /** + * @since 2.5 + * + * @return boolean + */ + public function isSecondLevelCacheEnabled() + { + return isset($this->_attributes['isSecondLevelCacheEnabled']) + ? $this->_attributes['isSecondLevelCacheEnabled'] + : false; + } + + /** + * @since 2.5 + * + * @param boolean $flag + * + * @return void + */ + public function setSecondLevelCacheEnabled($flag = true) + { + $this->_attributes['isSecondLevelCacheEnabled'] = (boolean) $flag; + } + + /** + * @since 2.5 + * + * @param \Doctrine\ORM\Cache\CacheConfiguration $cacheConfig + * + * @return void + */ + public function setSecondLevelCacheConfiguration(CacheConfiguration $cacheConfig) + { + $this->_attributes['secondLevelCacheConfiguration'] = $cacheConfig; + } + + /** + * @since 2.5 + * + * @return \Doctrine\ORM\Cache\CacheConfiguration|null + */ + public function getSecondLevelCacheConfiguration() + { + if ( ! isset($this->_attributes['secondLevelCacheConfiguration']) && $this->isSecondLevelCacheEnabled()) { + $this->_attributes['secondLevelCacheConfiguration'] = new CacheConfiguration(); + } + + return isset($this->_attributes['secondLevelCacheConfiguration']) + ? $this->_attributes['secondLevelCacheConfiguration'] + : null; + } } diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index ef2b58fd0..0df60eed9 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -166,7 +166,7 @@ use Doctrine\Common\Util\ClassUtils; ); if ($config->isSecondLevelCacheEnabled()) { - $cacheClass = $config->getSecondLevelCacheClassName(); + $cacheClass = $config->getSecondLevelCacheConfiguration()->getCacheClassName(); $this->cache = new $cacheClass($this); } } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 6e7ea488b..e0909d4b6 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -3012,7 +3012,8 @@ class UnitOfWork implements PropertyChangedListener if ($this->hasCache && $class->cache !== null) { $persister = $this->em->getConfiguration() - ->getSecondLevelCacheFactory() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory() ->buildCachedEntityPersister($this->em, $persister, $class); } @@ -3044,7 +3045,8 @@ class UnitOfWork implements PropertyChangedListener if ($this->hasCache && isset($association['cache'])) { $persister = $this->em->getConfiguration() - ->getSecondLevelCacheFactory() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory() ->buildCachedCollectionPersister($this->em, $persister, $association); } diff --git a/tests/Doctrine/Tests/ORM/Cache/CacheConfigTest.php b/tests/Doctrine/Tests/ORM/Cache/CacheConfigTest.php new file mode 100644 index 000000000..fff090330 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/CacheConfigTest.php @@ -0,0 +1,82 @@ +config = new CacheConfiguration(); + } + + public function testSetGetCacheClassName() + { + $mockClass = get_class($this->getMock('Doctrine\ORM\Cache')); + + $this->assertEquals('Doctrine\ORM\Cache\DefaultCache', $this->config->getCacheClassName()); + $this->config->setCacheClassName($mockClass); + $this->assertEquals($mockClass, $this->config->getCacheClassName()); + + $this->setExpectedException('Doctrine\ORM\ORMException'); + $this->config->setCacheClassName(__CLASS__); + } + + public function testSetGetRegionLifetime() + { + $config = $this->config->getRegionsConfiguration(); + + $config->setDefaultLifetime(111); + + $this->assertEquals($config->getDefaultLifetime(), $config->getLifetime('foo_region')); + + $config->setLifetime('foo_region', 222); + + $this->assertEquals(222, $config->getLifetime('foo_region')); + } + + public function testSetGetCacheLogger() + { + $logger = $this->getMock('Doctrine\ORM\Cache\Logging\CacheLogger'); + + $this->assertNull($this->config->getCacheLogger()); + + $this->config->setCacheLogger($logger); + + $this->assertEquals($logger, $this->config->getCacheLogger()); + } + + public function testSetGetCacheFactory() + { + $factory = $this->getMock('Doctrine\ORM\Cache\CacheFactory'); + + $this->assertNull($this->config->getCacheFactory()); + + $this->config->setCacheFactory($factory); + + $this->assertEquals($factory, $this->config->getCacheFactory()); + } + + public function testSetGetQueryValidator() + { + $validator = $this->getMock('Doctrine\ORM\Cache\QueryCacheValidator'); + + $this->assertInstanceOf('Doctrine\ORM\Cache\TimestampQueryCacheValidator', $this->config->getQueryValidator()); + + $this->config->setQueryValidator($validator); + + $this->assertEquals($validator, $this->config->getQueryValidator()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php index d52196d44..fa7cb219a 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -9,6 +9,7 @@ use Doctrine\ORM\Cache\Region\DefaultRegion; use Doctrine\Tests\Mocks\ConcurrentRegionMock; use Doctrine\ORM\Persisters\BasicEntityPersister; use Doctrine\ORM\Persisters\OneToManyPersister; +use Doctrine\ORM\Cache\RegionsConfiguration; /** * @group DDC-2183 @@ -25,16 +26,20 @@ class DefaultCacheFactoryTest extends OrmTestCase */ private $em; + /** + * @var \Doctrine\ORM\Cache\RegionsConfiguration + */ + private $regionsConfig; + protected function setUp() { $this->enableSecondLevelCache(); parent::setUp(); - $this->em = $this->_getTestEntityManager(); - - - $arguments = array($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); - $this->factory = $this->getMock('\Doctrine\ORM\Cache\DefaultCacheFactory', array( + $this->em = $this->_getTestEntityManager(); + $this->regionsConfig = new RegionsConfiguration; + $arguments = array($this->regionsConfig, $this->getSharedSecondLevelCacheDriverImpl()); + $this->factory = $this->getMock('\Doctrine\ORM\Cache\DefaultCacheFactory', array( 'getRegion' ), $arguments); } @@ -182,7 +187,7 @@ class DefaultCacheFactoryTest extends OrmTestCase $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()); + $factory = new DefaultCacheFactory($this->regionsConfig, $this->getSharedSecondLevelCacheDriverImpl()); $cachedPersister1 = $factory->buildCachedEntityPersister($em, $persister1, $metadata1); $cachedPersister2 = $factory->buildCachedEntityPersister($em, $persister2, $metadata2); @@ -201,7 +206,7 @@ class DefaultCacheFactoryTest extends OrmTestCase $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()); + $factory = new DefaultCacheFactory($this->regionsConfig, $this->getSharedSecondLevelCacheDriverImpl()); $cachedPersister1 = $factory->buildCachedEntityPersister($em, $persister1, $metadata1); $cachedPersister2 = $factory->buildCachedEntityPersister($em, $persister2, $metadata2); @@ -252,7 +257,7 @@ class DefaultCacheFactoryTest extends OrmTestCase */ public function testInvalidFileLockRegionDirectoryException() { - $factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + $factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($this->regionsConfig, $this->getSharedSecondLevelCacheDriverImpl()); $factory->getRegion(array( 'usage' => ClassMetadata::CACHE_USAGE_READ_WRITE, diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php index 8cc1c54ef..fd58fedc2 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -53,7 +53,9 @@ class DefaultQueryCacheTest extends OrmTestCase $this->queryCache = new DefaultQueryCache($this->em, $this->region); $this->cacheFactory = new CacheFactoryDefaultQueryCacheTest($this->queryCache, $this->region); - $this->em->getConfiguration()->setSecondLevelCacheFactory($this->cacheFactory); + $this->em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->setCacheFactory($this->cacheFactory); } public function testImplementQueryCache() diff --git a/tests/Doctrine/Tests/ORM/ConfigurationTest.php b/tests/Doctrine/Tests/ORM/ConfigurationTest.php index 1530a05a3..bd8e90d8a 100644 --- a/tests/Doctrine/Tests/ORM/ConfigurationTest.php +++ b/tests/Doctrine/Tests/ORM/ConfigurationTest.php @@ -276,16 +276,13 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase /** * @group DDC-2183 */ - public function testSetGetSecondLevelCacheClassName() + public function testSetGetSecondLevelCacheConfig() { - $mockClass = get_class($this->getMock('Doctrine\ORM\Cache')); + $mockClass = $this->getMock('Doctrine\ORM\Cache\CacheConfiguration'); - $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__); + $this->assertNull($this->configuration->getSecondLevelCacheConfiguration()); + $this->configuration->setSecondLevelCacheConfiguration($mockClass); + $this->assertEquals($mockClass, $this->configuration->getSecondLevelCacheConfiguration()); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php index ed35992c8..4297e147a 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php @@ -31,7 +31,9 @@ class SecondLevelCacheConcurrentTest extends SecondLevelCacheAbstractTest $this->cacheFactory = new CacheFactorySecondLevelCacheConcurrentTest($this->getSharedSecondLevelCacheDriverImpl()); - $this->_em->getConfiguration()->setSecondLevelCacheFactory($this->cacheFactory); + $this->_em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->setCacheFactory($this->cacheFactory); $this->countryMetadata = $this->_em->getClassMetadata(Country::CLASSNAME); $countryMetadata = clone $this->countryMetadata; diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index dba7bd09f..aa6c063a8 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -448,18 +448,20 @@ abstract class OrmFunctionalTestCase extends OrmTestCase if ($this->isSecondLevelCacheEnabled || $enableSecondLevelCache) { - $cache = $this->getSharedSecondLevelCacheDriverImpl(); - $factory = new DefaultCacheFactory($config, $cache); + $cacheConfig = new \Doctrine\ORM\Cache\CacheConfiguration(); + $cache = $this->getSharedSecondLevelCacheDriverImpl(); + $factory = new DefaultCacheFactory($cacheConfig->getRegionsConfiguration(), $cache); $this->secondLevelCacheFactory = $factory; if ($this->isSecondLevelCacheLogEnabled) { - $this->secondLevelCacheLogger = new StatisticsCacheLogger(); - $config->setSecondLevelCacheLogger($this->secondLevelCacheLogger); + $this->secondLevelCacheLogger = new StatisticsCacheLogger(); + $cacheConfig->setCacheLogger($this->secondLevelCacheLogger); } - - $config->setSecondLevelCacheEnabled(); - $config->setSecondLevelCacheFactory($factory); + + $cacheConfig->setCacheFactory($factory); + $config->setSecondLevelCacheEnabled(true); + $config->setSecondLevelCacheConfiguration($cacheConfig); $this->isSecondLevelCacheEnabled = true; } diff --git a/tests/Doctrine/Tests/OrmTestCase.php b/tests/Doctrine/Tests/OrmTestCase.php index 541e094a3..8546fae03 100644 --- a/tests/Doctrine/Tests/OrmTestCase.php +++ b/tests/Doctrine/Tests/OrmTestCase.php @@ -127,13 +127,16 @@ abstract class OrmTestCase extends DoctrineTestCase ), true)); if ($this->isSecondLevelCacheEnabled) { - $cache = $this->getSharedSecondLevelCacheDriverImpl(); - $factory = new DefaultCacheFactory($config, $cache); + + $cacheConfig = new \Doctrine\ORM\Cache\CacheConfiguration(); + $cache = $this->getSharedSecondLevelCacheDriverImpl(); + $factory = new DefaultCacheFactory($cacheConfig->getRegionsConfiguration(), $cache); $this->secondLevelCacheFactory = $factory; - $config->setSecondLevelCacheEnabled(); - $config->setSecondLevelCacheFactory($factory); + $cacheConfig->setCacheFactory($factory); + $config->setSecondLevelCacheEnabled(true); + $config->setSecondLevelCacheConfiguration($cacheConfig); } if ($conn === null) { From 1438a59c0018d211ae618ebe713a4b8949344bf0 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Thu, 3 Oct 2013 23:02:42 -0400 Subject: [PATCH 3/9] Fix persister query cache invalidation --- lib/Doctrine/ORM/Cache.php | 4 +- lib/Doctrine/ORM/Cache/CacheFactory.php | 7 + .../ORM/Cache/DefaultCacheFactory.php | 37 ++++- lib/Doctrine/ORM/Cache/DefaultQueryCache.php | 52 +++++- .../ORM/Cache/Logging/CacheLoggerChain.php | 156 ++++++++++++++++++ .../Cache/Logging/StatisticsCacheLogger.php | 24 +++ .../Persister/AbstractEntityPersister.php | 32 +++- ...onStrictReadWriteCachedEntityPersister.php | 22 ++- .../ReadWriteCachedEntityPersister.php | 10 ++ lib/Doctrine/ORM/Cache/QueryCacheKey.php | 2 +- .../ORM/Cache/Region/DefaultRegion.php | 19 +-- .../ORM/Cache/Region/UpdateTimestampCache.php | 42 +++++ .../ORM/Cache/TimestampCacheEntry.php | 51 ++++++ lib/Doctrine/ORM/Cache/TimestampCacheKey.php | 38 +++++ lib/Doctrine/ORM/Cache/TimestampRegion.php | 39 +++++ lib/Doctrine/ORM/Mapping/Cache.php | 2 +- .../Tests/Mocks/TimestampRegionMock.php | 14 ++ .../Tests/ORM/Cache/CacheLoggerChainTest.php | 118 +++++++++++++ .../Tests/ORM/Cache/DefaultQueryCacheTest.php | 5 + .../ORM/Cache/StatisticsCacheLoggerTest.php | 134 +++++++++++++++ .../SecondLevelCacheConcurrentTest.php | 5 + .../SecondLevelCacheQueryCacheTest.php | 18 +- .../SecondLevelCacheRepositoryTest.php | 128 +++++++++++++- 23 files changed, 909 insertions(+), 50 deletions(-) create mode 100644 lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php create mode 100644 lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php create mode 100644 lib/Doctrine/ORM/Cache/TimestampCacheEntry.php create mode 100644 lib/Doctrine/ORM/Cache/TimestampCacheKey.php create mode 100644 lib/Doctrine/ORM/Cache/TimestampRegion.php create mode 100644 tests/Doctrine/Tests/Mocks/TimestampRegionMock.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php diff --git a/lib/Doctrine/ORM/Cache.php b/lib/Doctrine/ORM/Cache.php index fe0df9702..068591253 100644 --- a/lib/Doctrine/ORM/Cache.php +++ b/lib/Doctrine/ORM/Cache.php @@ -30,7 +30,9 @@ use Doctrine\ORM\EntityManagerInterface; */ interface Cache { - const DEFAULT_QUERY_REGION_NAME = 'query.cache.region'; + const DEFAULT_QUERY_REGION_NAME = 'query_cache_region'; + + const DEFAULT_TIMESTAMP_REGION_NAME = 'timestamp_cache_region'; /** * May read items from the cache, but will not add items. diff --git a/lib/Doctrine/ORM/Cache/CacheFactory.php b/lib/Doctrine/ORM/Cache/CacheFactory.php index 77bcb24a6..01ffa45c9 100644 --- a/lib/Doctrine/ORM/Cache/CacheFactory.php +++ b/lib/Doctrine/ORM/Cache/CacheFactory.php @@ -92,4 +92,11 @@ interface CacheFactory * @return \Doctrine\ORM\Cache\Region The cache region. */ public function getRegion(array $cache); + + /** + * Build timestamp cache region + * + * @return \Doctrine\ORM\Cache\TimestampRegion The timestamp region. + */ + public function getTimestampRegion(); } diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 360a97210..24898bb56 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -22,13 +22,15 @@ namespace Doctrine\ORM\Cache; use Doctrine\ORM\Cache; use Doctrine\ORM\Cache\Region; +use Doctrine\ORM\Cache\TimestampRegion; + use Doctrine\ORM\Cache\RegionsConfiguration; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Cache\Region\DefaultRegion; use Doctrine\ORM\Cache\Region\FileLockRegion; +use Doctrine\ORM\Cache\Region\UpdateTimestampCache; use Doctrine\Common\Cache\Cache as CacheDriver; - use Doctrine\ORM\Persisters\EntityPersister; use Doctrine\ORM\Persisters\CollectionPersister; use Doctrine\ORM\Cache\Persister\ReadOnlyCachedEntityPersister; @@ -54,6 +56,11 @@ class DefaultCacheFactory implements CacheFactory */ private $regionsConfig; + /** + * @var \Doctrine\ORM\Cache\TimestampRegion + */ + private $timestampRegion; + /** * @var array */ @@ -98,6 +105,15 @@ class DefaultCacheFactory implements CacheFactory $this->regions[$region->getName()] = $region; } + /** + * @param \Doctrine\ORM\Cache\TimestampRegion $region + */ + public function setTimestampRegion(TimestampRegion $region) + { + $this->timestampRegion = $region; + } + + /** * {@inheritdoc} */ @@ -180,9 +196,7 @@ class DefaultCacheFactory implements CacheFactory return $this->regions[$cache['region']]; } - $region = new DefaultRegion($cache['region'], clone $this->cache, array( - 'lifetime' => $this->regionsConfig->getLifetime($cache['region']) - )); + $region = new DefaultRegion($cache['region'], clone $this->cache, $this->regionsConfig->getLifetime($cache['region'])); if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { @@ -199,4 +213,19 @@ class DefaultCacheFactory implements CacheFactory return $this->regions[$cache['region']] = $region; } + + /** + * {@inheritdoc} + */ + public function getTimestampRegion() + { + if ($this->timestampRegion === null) { + $name = Cache::DEFAULT_TIMESTAMP_REGION_NAME; + $lifetime = $this->regionsConfig->getLifetime($name); + + $this->timestampRegion = new UpdateTimestampCache($name, clone $this->cache, $lifetime); + } + + return $this->timestampRegion; + } } diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php index 3ff1dd06b..c1648fb7b 100644 --- a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -61,6 +61,11 @@ class DefaultQueryCache implements QueryCache */ private $validator; + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + protected $cacheLogger; + /** * @var array */ @@ -72,12 +77,13 @@ class DefaultQueryCache implements QueryCache */ public function __construct(EntityManagerInterface $em, Region $region) { - $this->em = $em; - $this->region = $region; - $this->uow = $em->getUnitOfWork(); - $this->validator = $em->getConfiguration() - ->getSecondLevelCacheConfiguration() - ->getQueryValidator(); + $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration(); + + $this->em = $em; + $this->region = $region; + $this->uow = $em->getUnitOfWork(); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->validator = $cacheConfig->getQueryValidator(); } /** @@ -106,14 +112,24 @@ class DefaultQueryCache implements QueryCache $hasRelation = ( ! empty($rsm->relationMap)); $persister = $this->uow->getEntityPersister($entityName); $region = $persister->getCacheRegion(); + $regionName = $region->getName(); // @TODO - move to cache hydration componente foreach ($entry->result as $index => $entry) { - if (($entityEntry = $region->get(new EntityCacheKey($entityName, $entry['identifier']))) === null) { + if (($entityEntry = $region->get($entityKey = new EntityCacheKey($entityName, $entry['identifier']))) === null) { + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheMiss($regionName, $entityKey); + } + return null; } + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheHit($regionName, $entityKey); + } + if ( ! $hasRelation) { $result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->data, self::$hints); @@ -129,12 +145,21 @@ class DefaultQueryCache implements QueryCache if ($assoc['type'] & ClassMetadata::TO_ONE) { - if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) { + if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) { + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey); + } + return null; } $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints); + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey); + } + continue; } @@ -147,13 +172,22 @@ class DefaultQueryCache implements QueryCache foreach ($assoc['list'] as $assocIndex => $assocId) { - if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) { + if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) { + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey); + } + return null; } $element = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints); $collection->hydrateSet($assocIndex, $element); + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey); + } } $data[$name] = $collection; diff --git a/lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php b/lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php new file mode 100644 index 000000000..694b35ca5 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php @@ -0,0 +1,156 @@ +. + */ + +namespace Doctrine\ORM\Cache\Logging; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\QueryCacheKey; + +/** + * Cache logger chain + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CacheLoggerChain implements CacheLogger +{ + /** + * @var array<\Doctrine\ORM\Cache\Logging\CacheLogger> + */ + private $loggers = array(); + + /** + * @param string $name + * @param \Doctrine\ORM\Cache\Logging\CacheLogger $logger + */ + public function setLogger($name, CacheLogger $logger) + { + $this->loggers[$name] = $logger; + } + + /** + * @param string $name + * + * @return \Doctrine\ORM\Cache\Logging\CacheLogger|null + */ + public function getLogger($name) + { + return isset($this->loggers[$name]) ? $this->loggers[$name] : null; + } + + /** + * @return array<\Doctrine\ORM\Cache\Logging\CacheLogger> + */ + public function getLoggers() + { + return $this->loggers; + } + + /** + * {@inheritdoc} + */ + public function collectionCacheHit($regionName, CollectionCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->collectionCacheHit($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function collectionCacheMiss($regionName, CollectionCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->collectionCacheMiss($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function collectionCachePut($regionName, CollectionCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->collectionCachePut($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function entityCacheHit($regionName, EntityCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->entityCacheHit($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function entityCacheMiss($regionName, EntityCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->entityCacheMiss($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function entityCachePut($regionName, EntityCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->entityCachePut($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function queryCacheHit($regionName, QueryCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->queryCacheHit($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function queryCacheMiss($regionName, QueryCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->queryCacheMiss($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function queryCachePut($regionName, QueryCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->queryCachePut($regionName, $key); + } + } +} diff --git a/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php b/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php index 283bd4512..2fbba40be 100644 --- a/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php +++ b/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php @@ -173,6 +173,30 @@ class StatisticsCacheLogger implements CacheLogger return isset($this->cachePutCountMap[$regionName]) ? $this->cachePutCountMap[$regionName] : 0; } + /** + * @return array + */ + public function getRegionsMiss() + { + return $this->cacheMissCountMap; + } + + /** + * @return array + */ + public function getRegionsHit() + { + return $this->cacheHitCountMap; + } + + /** + * @return array + */ + public function getRegionsPut() + { + return $this->cachePutCountMap; + } + /** * Clear region statistics * diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php index ad113b609..4eeef07e7 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -24,6 +24,7 @@ use Doctrine\ORM\Cache; use Doctrine\ORM\Cache\Region; use Doctrine\ORM\Cache\EntityCacheKey; use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\TimestampCacheKey; use Doctrine\ORM\Cache\QueryCacheKey; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\PersistentCollection; @@ -69,6 +70,16 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ protected $region; + /** + * @var \Doctrine\ORM\Cache\TimestampRegion + */ + protected $timestampRegion; + + /** + * @var \Doctrine\ORM\Cache\TimestampCacheKey + */ + protected $timestampKey; + /** * @var \Doctrine\ORM\Cache\EntityHydrator */ @@ -109,7 +120,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->timestampRegion = $cacheFactory->getTimestampRegion(); $this->hydrator = $cacheFactory->buildEntityHydrator($em, $class); + $this->timestampKey = new TimestampCacheKey($this->class->getTableName()); } /** @@ -218,13 +231,20 @@ abstract class AbstractEntityPersister implements CachedEntityPersister /** * Generates a string of currently query * + * @param array $query + * @param string $criteria + * @param array $orderBy + * @param integer $limit + * @param integer $offset + * @param integer $timestamp + * * @return string */ - protected function getHash($query, $criteria, array $orderBy = null, $limit = null, $offset = null) + protected function getHash($query, $criteria, array $orderBy = null, $limit = null, $offset = null, $timestamp = null) { - list($params) = $this->expandParameters($criteria); + list($params) = $this->persister->expandParameters($criteria); - return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset); + return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset . $timestamp); } /** @@ -288,8 +308,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister } //handle only EntityRepository#findOneBy + $timestamp = $this->timestampRegion->get($this->timestampKey); $query = $this->persister->getSelectSQL($criteria, null, 0, $limit, 0, $orderBy); - $hash = $this->getHash($query, $criteria); + $hash = $this->getHash($query, $criteria, null, null, null, $timestamp ? $timestamp->time : null); $rsm = $this->getResultSetMapping(); $querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL); $queryCache = $this->cache->getQueryCache($this->regionName); @@ -326,8 +347,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null) { + $timestamp = $this->timestampRegion->get($this->timestampKey); $query = $this->persister->getSelectSQL($criteria, null, 0, $limit, $offset, $orderBy); - $hash = $this->getHash($query, $criteria); + $hash = $this->getHash($query, $criteria, null, null, null, $timestamp ? $timestamp->time : null); $rsm = $this->getResultSetMapping(); $querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL); $queryCache = $this->cache->getQueryCache($this->regionName); diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php index f6420ca21..a9a9a37e9 100644 --- a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php @@ -37,6 +37,8 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister */ public function afterTransactionComplete() { + $isChanged = false; + if (isset($this->queuedCache['insert'])) { foreach ($this->queuedCache['insert'] as $entity) { @@ -47,9 +49,10 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister $class = $this->metadataFactory->getMetadataFor($className); } - $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); - $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); - $cached = $this->region->put($key, $entry); + $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + $isChanged = $isChanged ?: $cached; if ($this->cacheLogger && $cached) { $this->cacheLogger->entityCachePut($this->regionName, $key); @@ -67,9 +70,10 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister $class = $this->metadataFactory->getMetadataFor($className); } - $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); - $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); - $cached = $this->region->put($key, $entry); + $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + $isChanged = $isChanged ?: $cached; if ($this->cacheLogger && $cached) { $this->cacheLogger->entityCachePut($this->regionName, $key); @@ -80,9 +84,15 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister if (isset($this->queuedCache['delete'])) { foreach ($this->queuedCache['delete'] as $key) { $this->region->evict($key); + + $isChanged = true; } } + if ($isChanged) { + $this->timestampRegion->update($this->timestampKey); + } + $this->queuedCache = array(); } diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php index e49c22541..d77aabf9e 100644 --- a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php @@ -56,18 +56,28 @@ class ReadWriteCachedEntityPersister extends AbstractEntityPersister */ public function afterTransactionComplete() { + $isChanged = true; + if (isset($this->queuedCache['update'])) { foreach ($this->queuedCache['update'] as $item) { $this->region->evict($item['key']); + + $isChanged = true; } } if (isset($this->queuedCache['delete'])) { foreach ($this->queuedCache['delete'] as $item) { $this->region->evict($item['key']); + + $isChanged = true; } } + if ($isChanged) { + $this->timestampRegion->update($this->timestampKey); + } + $this->queuedCache = array(); } diff --git a/lib/Doctrine/ORM/Cache/QueryCacheKey.php b/lib/Doctrine/ORM/Cache/QueryCacheKey.php index 36126582a..31a3d15d8 100644 --- a/lib/Doctrine/ORM/Cache/QueryCacheKey.php +++ b/lib/Doctrine/ORM/Cache/QueryCacheKey.php @@ -43,7 +43,7 @@ class QueryCacheKey extends CacheKey * @param integer $lifetime Query lifetime * @param integer $cacheMode Query cache mode */ - public function __construct($hash, $lifetime, $cacheMode = 3) + public function __construct($hash, $lifetime = 0, $cacheMode = 3) { $this->hash = $hash; $this->lifetime = $lifetime; diff --git a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php index 0b62c5b81..b5735e9f6 100644 --- a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -39,33 +39,30 @@ class DefaultRegion implements Region /** * @var \Doctrine\Common\Cache\CacheProvider */ - private $cache; + protected $cache; /** * @var string */ - private $name; + protected $name; /** * @var integer */ - private $lifetime = 0; + protected $lifetime = 0; /** * @param string $name * @param \Doctrine\Common\Cache\CacheProvider $cache - * @param array $configuration + * @param integer $lifetime */ - public function __construct($name, CacheProvider $cache, array $configuration = array()) + public function __construct($name, CacheProvider $cache, $lifetime = 0) { - $this->name = $name; - $this->cache = $cache; + $this->name = $name; + $this->cache = $cache; + $this->lifetime = $lifetime; $this->cache->setNamespace($this->name); - - if (isset($configuration['lifetime']) && $configuration['lifetime'] > 0) { - $this->lifetime = (integer) $configuration['lifetime']; - } } /** diff --git a/lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php b/lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php new file mode 100644 index 000000000..dfdf9062a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php @@ -0,0 +1,42 @@ +. + */ + +namespace Doctrine\ORM\Cache\Region; + +use Doctrine\ORM\Cache\TimestampCacheEntry; +use Doctrine\ORM\Cache\TimestampRegion; +use Doctrine\ORM\Cache\CacheKey; + +/** + * Tracks the timestamps of the most recent updates to particular keys. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class UpdateTimestampCache extends DefaultRegion implements TimestampRegion +{ + /** + * {@inheritdoc} + */ + public function update(CacheKey $key) + { + $this->put($key, new TimestampCacheEntry); + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php new file mode 100644 index 000000000..3ad05c96a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php @@ -0,0 +1,51 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Timestamp cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class TimestampCacheEntry implements CacheEntry +{ + /** + * @var integer + */ + public $time; + + /** + * @param array $result + */ + public function __construct($time = null) + { + $this->time = $time ?: microtime(true); + } + + /** + * @param array $values + */ + public static function __set_state(array $values) + { + return new self($values['time']); + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheKey.php b/lib/Doctrine/ORM/Cache/TimestampCacheKey.php new file mode 100644 index 000000000..2ae65d065 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampCacheKey.php @@ -0,0 +1,38 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * A key that identifies a timestamped space. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class TimestampCacheKey extends CacheKey +{ + /** + * @param string $space Result cache id + */ + public function __construct($space) + { + $this->hash = $space; + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampRegion.php b/lib/Doctrine/ORM/Cache/TimestampRegion.php new file mode 100644 index 000000000..8065a9411 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampRegion.php @@ -0,0 +1,39 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Defines the contract for a cache region which will specifically be used to store entity "update timestamps". + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface TimestampRegion extends Region +{ + /** + * Update an specific key into the cache region. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to lock. + * + * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. + */ + public function update(CacheKey $key); +} diff --git a/lib/Doctrine/ORM/Mapping/Cache.php b/lib/Doctrine/ORM/Mapping/Cache.php index 560a9c4eb..2bb8bb321 100644 --- a/lib/Doctrine/ORM/Mapping/Cache.php +++ b/lib/Doctrine/ORM/Mapping/Cache.php @@ -26,7 +26,7 @@ namespace Doctrine\ORM\Mapping; * @since 2.5 * * @Annotation - * @Target("CLASS") + * @Target({"CLASS","PROPERTY"}) */ final class Cache implements Annotation { diff --git a/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php new file mode 100644 index 000000000..ade513060 --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php @@ -0,0 +1,14 @@ +calls[__FUNCTION__][] = array('key' => $key); + } +} diff --git a/tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php b/tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php new file mode 100644 index 000000000..8f8c65c0f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php @@ -0,0 +1,118 @@ +logger = new CacheLoggerChain(); + $this->mock = $this->getMock('Doctrine\ORM\Cache\Logging\CacheLogger'); + } + + public function testGetAndSetLogger() + { + $this->assertEmpty($this->logger->getLoggers()); + + $this->assertNull($this->logger->getLogger('mock')); + + $this->logger->setLogger('mock', $this->mock); + + $this->assertSame($this->mock, $this->logger->getLogger('mock')); + $this->assertEquals(array('mock' => $this->mock), $this->logger->getLoggers()); + } + + public function testEntityCacheChain() + { + $name = 'my_entity_region'; + $key = new EntityCacheKey(State::CLASSNAME, array('id' => 1)); + + $this->logger->setLogger('mock', $this->mock); + + $this->mock->expects($this->once()) + ->method('entityCacheHit') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('entityCachePut') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('entityCacheMiss') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->logger->entityCacheHit($name, $key); + $this->logger->entityCachePut($name, $key); + $this->logger->entityCacheMiss($name, $key); + } + + public function testCollectionCacheChain() + { + $name = 'my_collection_region'; + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id' => 1)); + + $this->logger->setLogger('mock', $this->mock); + + $this->mock->expects($this->once()) + ->method('collectionCacheHit') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('collectionCachePut') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('collectionCacheMiss') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->logger->collectionCacheHit($name, $key); + $this->logger->collectionCachePut($name, $key); + $this->logger->collectionCacheMiss($name, $key); + } + + public function testQueryCacheChain() + { + $name = 'my_query_region'; + $key = new QueryCacheKey('my_query_hash'); + + $this->logger->setLogger('mock', $this->mock); + + $this->mock->expects($this->once()) + ->method('queryCacheHit') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('queryCachePut') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('queryCacheMiss') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->logger->queryCacheHit($name, $key); + $this->logger->queryCachePut($name, $key); + $this->logger->queryCacheMiss($name, $key); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php index fd58fedc2..fed80adc0 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -525,4 +525,9 @@ class CacheFactoryDefaultQueryCacheTest extends \Doctrine\ORM\Cache\DefaultCache { return $this->region; } + + public function getTimestampRegion() + { + return new \Doctrine\Tests\Mocks\TimestampRegionMock(); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php b/tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php new file mode 100644 index 000000000..16c91729b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php @@ -0,0 +1,134 @@ +logger = new StatisticsCacheLogger(); + } + + public function testEntityCache() + { + $name = 'my_entity_region'; + $key = new EntityCacheKey(State::CLASSNAME, array('id' => 1)); + + $this->logger->entityCacheHit($name, $key); + $this->logger->entityCachePut($name, $key); + $this->logger->entityCacheMiss($name, $key); + + $this->assertEquals(1, $this->logger->getHitCount()); + $this->assertEquals(1, $this->logger->getPutCount()); + $this->assertEquals(1, $this->logger->getMissCount()); + $this->assertEquals(1, $this->logger->getRegionHitCount($name)); + $this->assertEquals(1, $this->logger->getRegionPutCount($name)); + $this->assertEquals(1, $this->logger->getRegionMissCount($name)); + } + + public function testCollectionCache() + { + $name = 'my_collection_region'; + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id' => 1)); + + $this->logger->collectionCacheHit($name, $key); + $this->logger->collectionCachePut($name, $key); + $this->logger->collectionCacheMiss($name, $key); + + $this->assertEquals(1, $this->logger->getHitCount()); + $this->assertEquals(1, $this->logger->getPutCount()); + $this->assertEquals(1, $this->logger->getMissCount()); + $this->assertEquals(1, $this->logger->getRegionHitCount($name)); + $this->assertEquals(1, $this->logger->getRegionPutCount($name)); + $this->assertEquals(1, $this->logger->getRegionMissCount($name)); + } + + public function testQueryCache() + { + $name = 'my_query_region'; + $key = new QueryCacheKey('my_query_hash'); + + $this->logger->queryCacheHit($name, $key); + $this->logger->queryCachePut($name, $key); + $this->logger->queryCacheMiss($name, $key); + + $this->assertEquals(1, $this->logger->getHitCount()); + $this->assertEquals(1, $this->logger->getPutCount()); + $this->assertEquals(1, $this->logger->getMissCount()); + $this->assertEquals(1, $this->logger->getRegionHitCount($name)); + $this->assertEquals(1, $this->logger->getRegionPutCount($name)); + $this->assertEquals(1, $this->logger->getRegionMissCount($name)); + } + + public function testMultipleCaches() + { + $coolRegion = 'my_collection_region'; + $entityRegion = 'my_entity_region'; + $queryRegion = 'my_query_region'; + + $coolKey = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id' => 1)); + $entityKey = new EntityCacheKey(State::CLASSNAME, array('id' => 1)); + $queryKey = new QueryCacheKey('my_query_hash'); + + $this->logger->queryCacheHit($queryRegion, $queryKey); + $this->logger->queryCachePut($queryRegion, $queryKey); + $this->logger->queryCacheMiss($queryRegion, $queryKey); + + $this->logger->entityCacheHit($entityRegion, $entityKey); + $this->logger->entityCachePut($entityRegion, $entityKey); + $this->logger->entityCacheMiss($entityRegion, $entityKey); + + $this->logger->collectionCacheHit($coolRegion, $coolKey); + $this->logger->collectionCachePut($coolRegion, $coolKey); + $this->logger->collectionCacheMiss($coolRegion, $coolKey); + + $this->assertEquals(3, $this->logger->getHitCount()); + $this->assertEquals(3, $this->logger->getPutCount()); + $this->assertEquals(3, $this->logger->getMissCount()); + + $this->assertEquals(1, $this->logger->getRegionHitCount($queryRegion)); + $this->assertEquals(1, $this->logger->getRegionPutCount($queryRegion)); + $this->assertEquals(1, $this->logger->getRegionMissCount($queryRegion)); + + $this->assertEquals(1, $this->logger->getRegionHitCount($coolRegion)); + $this->assertEquals(1, $this->logger->getRegionPutCount($coolRegion)); + $this->assertEquals(1, $this->logger->getRegionMissCount($coolRegion)); + + $this->assertEquals(1, $this->logger->getRegionHitCount($entityRegion)); + $this->assertEquals(1, $this->logger->getRegionPutCount($entityRegion)); + $this->assertEquals(1, $this->logger->getRegionMissCount($entityRegion)); + + $miss = $this->logger->getRegionsMiss(); + $hit = $this->logger->getRegionsHit(); + $put = $this->logger->getRegionsPut(); + + $this->assertArrayHasKey($coolRegion, $miss); + $this->assertArrayHasKey($queryRegion, $miss); + $this->assertArrayHasKey($entityRegion, $miss); + + $this->assertArrayHasKey($coolRegion, $put); + $this->assertArrayHasKey($queryRegion, $put); + $this->assertArrayHasKey($entityRegion, $put); + + $this->assertArrayHasKey($coolRegion, $hit); + $this->assertArrayHasKey($queryRegion, $hit); + $this->assertArrayHasKey($entityRegion, $hit); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php index 4297e147a..298687b23 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php @@ -138,4 +138,9 @@ class CacheFactorySecondLevelCacheConcurrentTest extends \Doctrine\ORM\Cache\Def return $mock; } + + public function getTimestampRegion() + { + return new \Doctrine\Tests\Mocks\TimestampRegionMock(); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php index b7ebca196..c326128cb 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -53,7 +53,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -70,7 +70,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -256,7 +256,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -273,7 +273,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -357,7 +357,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(5, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); @@ -639,7 +639,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -656,7 +656,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -804,7 +804,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertNotEmpty($result3); $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount('foo_region')); @@ -818,7 +818,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertNotEmpty($result3); $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); - $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(6, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount('bar_region')); diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php index ca546bb10..71f7d952f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php @@ -3,6 +3,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\Cache\Country; +use Doctrine\Tests\Models\Cache\State; /** * @group DDC-2183 @@ -60,13 +61,61 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheAbstractTest $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); $this->assertInstanceOf(Country::CLASSNAME, $countries[1]); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); } + public function testRepositoryCacheFindAllInvalidation() + { + $this->loadFixturesCountries(); + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $repository = $this->_em->getRepository(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(2, $repository->findAll()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $queryCount = $this->getCurrentQueryCount(); + $countries = $repository->findAll(); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertCount(2, $countries); + $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); + $this->assertInstanceOf(Country::CLASSNAME, $countries[1]); + + $country = new Country('foo'); + + $this->_em->persist($country); + $this->_em->flush(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(3, $repository->findAll()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $country = $repository->find($country->getId()); + + $this->_em->remove($country); + $this->_em->flush(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(2, $repository->findAll()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + public function testRepositoryCacheFindBy() { $this->loadFixturesCountries(); @@ -91,7 +140,7 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheAbstractTest $this->assertCount(1, $countries); $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); @@ -120,9 +169,82 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheAbstractTest $this->assertInstanceOf(Country::CLASSNAME, $country); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); } + + public function testRepositoryCacheFindAllToOneAssociation() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + + $this->evictRegions(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + // load from database + $repository = $this->_em->getRepository(State::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(4, $entities); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + + // load from cache + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(4, $entities); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[1]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + + // invalidate cache + $this->_em->persist(new State('foo', $this->_em->find(Country::CLASSNAME, $this->countries[0]->getId()))); + $this->_em->flush(); + $this->_em->clear(); + + // load from database + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(5, $entities); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[1]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + + // load from cache + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(5, $entities); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[1]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + } } \ No newline at end of file From 61bff7d5f63490c2ac0fd882e3cec88c2673e9b0 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Mon, 7 Oct 2013 18:53:32 -0400 Subject: [PATCH 4/9] Fix CS and update docs --- docs/en/reference/annotations-reference.rst | 12 ++ docs/en/reference/second-level-cache.rst | 128 +++++++++++++++--- lib/Doctrine/ORM/AbstractQuery.php | 50 ++++--- lib/Doctrine/ORM/Cache.php | 4 +- lib/Doctrine/ORM/Cache/CacheConfiguration.php | 6 +- lib/Doctrine/ORM/Cache/CacheEntry.php | 4 + lib/Doctrine/ORM/Cache/CacheFactory.php | 2 + lib/Doctrine/ORM/Cache/CollectionHydrator.php | 4 +- lib/Doctrine/ORM/Cache/DefaultCache.php | 14 +- .../ORM/Cache/DefaultCacheFactory.php | 13 +- lib/Doctrine/ORM/Cache/DefaultQueryCache.php | 22 ++- lib/Doctrine/ORM/Cache/EntityHydrator.php | 2 +- .../Persister/AbstractCollectionPersister.php | 5 +- .../Persister/AbstractEntityPersister.php | 40 +++--- ...rictReadWriteCachedCollectionPersister.php | 1 - ...onStrictReadWriteCachedEntityPersister.php | 2 - .../ReadWriteCachedCollectionPersister.php | 5 - .../ReadWriteCachedEntityPersister.php | 5 - lib/Doctrine/ORM/Cache/QueryCache.php | 12 +- lib/Doctrine/ORM/Cache/QueryCacheKey.php | 4 +- .../ORM/Cache/Region/DefaultRegion.php | 10 +- .../ORM/Cache/Region/FileLockRegion.php | 16 +-- .../ORM/Cache/RegionsConfiguration.php | 16 +-- .../ORM/Cache/TimestampCacheEntry.php | 4 +- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 32 +++-- .../ORM/Mapping/Driver/YamlDriver.php | 12 +- lib/Doctrine/ORM/ORMException.php | 2 +- .../ORM/Persisters/CollectionPersister.php | 2 - .../ORM/Persisters/EntityPersister.php | 1 - lib/Doctrine/ORM/Query.php | 2 +- lib/Doctrine/ORM/Query/ResultSetMapping.php | 8 ++ lib/Doctrine/ORM/Query/SqlWalker.php | 2 + lib/Doctrine/ORM/UnitOfWork.php | 10 +- .../EventListener/CacheMetadataListener.php | 2 +- .../Doctrine/Tests/Mocks/CacheRegionMock.php | 4 +- .../Tests/Mocks/ConcurrentRegionMock.php | 2 +- .../ORM/Cache/DefaultCacheFactoryTest.php | 2 +- .../Tests/ORM/Cache/DefaultQueryCacheTest.php | 28 +++- .../Tests/ORM/Cache/FileLockRegionTest.php | 1 - .../SecondLevelCacheAbstractTest.php | 2 - ...condLevelCacheCompositePrimaryKeyTest.php} | 3 +- ...econdLevelCacheExtraLazyCollectionTest.php | 1 - ...condLevelCacheJoinTableInheritanceTest.php | 1 - .../SecondLevelCacheOneToManyTest.php | 1 - .../SecondLevelCacheQueryCacheTest.php | 23 ++++ .../ORM/Performance/SecondLevelCacheTest.php | 2 - tests/Doctrine/Tests/OrmTestCase.php | 1 - 47 files changed, 338 insertions(+), 187 deletions(-) rename tests/Doctrine/Tests/ORM/Functional/{SecondLevelCacheCompositPrimaryKeyTest.php => SecondLevelCacheCompositePrimaryKeyTest.php} (98%) diff --git a/docs/en/reference/annotations-reference.rst b/docs/en/reference/annotations-reference.rst index 2ebc5c1f8..62869297f 100644 --- a/docs/en/reference/annotations-reference.rst +++ b/docs/en/reference/annotations-reference.rst @@ -35,6 +35,7 @@ Index - :ref:`@Column ` - :ref:`@ColumnResult ` +- :ref:`@Cache ` - :ref:`@ChangeTrackingPolicy ` - :ref:`@DiscriminatorColumn ` - :ref:`@DiscriminatorMap ` @@ -152,6 +153,17 @@ Required attributes: - **name**: The name of a column in the SELECT clause of a SQL query +.. _annref_cache: + +@Cache +~~~~~~~~~~~~~~ +Add caching strategy to a root entity or a collection. + +Optional attributes: + +- **usage**: One of ``READ_ONLY``, ``READ_READ_WRITE`` or ``NONSTRICT_READ_WRITE``, By default this is ``READ_ONLY``. +- **region**: An specific region name + .. _annref_changetrackingpolicy: @ChangeTrackingPolicy diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst index 7139a1c38..193a563f1 100644 --- a/docs/en/reference/second-level-cache.rst +++ b/docs/en/reference/second-level-cache.rst @@ -155,7 +155,7 @@ Defines contract for concurrently managed data region. * * @return \Doctrine\ORM\Cache\Lock A lock instance or NULL if the lock already exists. */ - public function readLock(CacheKey $key); + public function lock(CacheKey $key); /** * Attempts to read unlock the mapping for the given key. @@ -163,9 +163,31 @@ Defines contract for concurrently managed data region. * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to unlock. * @param \Doctrine\ORM\Cache\Lock $lock The lock previously obtained from readLock */ - public function readUnlock(CacheKey $key, Lock $lock); + public function unlock(CacheKey $key, Lock $lock); } +``Doctrine\ORM\Cache\TimestampRegion`` + +Tracks the timestamps of the most recent updates to particular entity. + +.. code-block:: php + + setSecondLevelCacheEnabled(); //Cache factory - $config->setSecondLevelCacheFactory($factory); + $config->getSecondLevelCacheConfiguration() + ->setCacheFactory($factory); Cache Factory @@ -290,22 +313,22 @@ It allows you to provide a specific implementation of the following components : public function buildQueryCache(EntityManagerInterface $em, $regionName = null); /** - * Build an entity hidrator + * Build an entity hydrator * * @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. + * @return \Doctrine\ORM\Cache\EntityHydrator The built entity hydrator. */ public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata); /** - * Build a collection hidrator + * Build a collection hydrator * * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. * @param array $mapping The association mapping. * - * @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hidrator. + * @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hydrator. */ public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping); @@ -317,6 +340,13 @@ It allows you to provide a specific implementation of the following components : * @return \Doctrine\ORM\Cache\Region The cache region. */ public function getRegion(array $cache); + + /** + * Build timestamp cache region + * + * @return \Doctrine\ORM\Cache\TimestampRegion The timestamp region. + */ + public function getTimestampRegion(); } Region Lifetime @@ -328,11 +358,14 @@ To specify a default lifetime for all regions or specify a different lifetime fo getSecondLevelCacheConfiguration(); + $regionConfig = $cacheConfig->getRegionsConfiguration(); //Cache Region lifetime - $config->setSecondLevelCacheRegionLifetime('my_entity_region', 3600); - $config->setSecondLevelCacheDefaultRegionLifetime(7200); + $regionConfig->setLifetime('my_entity_region', 3600); + $regionConfig->setDefaultLifetime(7200); Cache Log @@ -344,12 +377,14 @@ By providing a cache logger you should be able to get information about all cach .. code-block:: php setSecondLevelCacheLogger($logger); + $config->setSecondLevelCacheEnabled(true); + $config->getSecondLevelCacheConfiguration() + ->setCacheLogger($logger); // Collect cache statistics @@ -456,8 +491,8 @@ Entity cache definition ----------------------- * Entity cache configuration allows you to define the caching strategy and region for an entity. - * ``usage`` Specifies the caching strategy: ``READ_ONLY``, ``NONSTRICT_READ_WRITE``, ``READ_WRITE`` - * ``region`` Specifies the name of the second level cache region. + * ``usage`` Specifies the caching strategy: ``READ_ONLY``, ``NONSTRICT_READ_WRITE``, ``READ_WRITE``. see :ref:`reference-second-level-cache-mode` + * ``region`` Optional value that specifies the name of the second level cache region. .. configuration-block:: @@ -729,6 +764,57 @@ The query cache stores the results of the query but as identifiers, entity value ->setCacheable(true) ->getResult(); +Cache mode +~~~~~~~~~~ + +The Cache Mode controls how a particular query interacts with the second-level cache: + +* ``Cache::MODE_GET`` - May read items from the cache, but will not add items. +* ``Cache::MODE_PUT`` - Will never read items from the cache, but will add items to the cache as it reads them from the database. +* ``Cache::MODE_NORMAL`` - May read items from the cache, and add items to the cache. +* ``Cache::MODE_REFRESH`` - The query will never read items from the cache, but will refresh items to the cache as it reads them from the database. + +.. code-block:: php + + createQuery('SELECT c FROM Country c ORDER BY c.name') + ->setCacheMode(Cache::MODE_GET) + ->setCacheable(true) + ->getResult(); + +.. note:: + + The the default query cache mode is ```Cache::MODE_NORMAL``` + + +Using the repository query cache +--------------------- + +As well as ``Query Cache`` all persister queries store only identifier values for an individual query. +All persister use a single timestamps cache region keeps track of the last update for each persister, +When a query is loaded from cache, the timestamp region is checked for the last update for that persister. +Using the last update timestamps as part of the query key invalidate the cache key when an update occurs. + +.. code-block:: php + + getRepository('Entity\Country')->findAll(); + + // load from query and entities from cache.. + $entities = $em->getRepository('Country')->findAll(); + + // update the timestamp cache region for Country + $em->persist(new Country('zombieland')); + $em->flush(); + $em->clear(); + + // Reload the query from database. + $entities = $em->getRepository('Country')->findAll(); Cache API --------- @@ -785,6 +871,14 @@ Composite primary key private $target; } + // Supported + /** @var $article Article */ + $article = $this->_em->find("Article", 1); + + // Supported + /** @var $article Article */ + $article = $this->_em->find("Article", $article); + // Supported $id = array('source' => 1, 'target' => 2); $reference = $this->_em->find("Reference", $id); diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index cfc3e22dd..67606a0f9 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM; use Doctrine\Common\Util\ClassUtils; use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\DBAL\Types\Type; +use Doctrine\ORM\Query\Parameter; use Doctrine\ORM\Cache\QueryCacheKey; use Doctrine\DBAL\Cache\QueryCacheProfile; @@ -127,7 +127,7 @@ abstract class AbstractQuery * * @var boolean */ - protected $cacheable; + protected $cacheable = false; /** * Second level cache region name. @@ -216,7 +216,7 @@ abstract class AbstractQuery } /** - * @return boolean TRUE if the query cache and second level cache are anabled, FALSE otherwise. + * @return boolean TRUE if the query cache and second level cache are enabled, FALSE otherwise. */ protected function isCacheEnabled() { @@ -340,7 +340,7 @@ abstract class AbstractQuery $parameterCollection = new ArrayCollection(); foreach ($parameters as $key => $value) { - $parameter = new Query\Parameter($key, $value); + $parameter = new Parameter($key, $value); $parameterCollection->add($parameter); } @@ -381,7 +381,7 @@ abstract class AbstractQuery return $this; } - $parameter = new Query\Parameter($key, $value, $type); + $parameter = new Parameter($key, $value, $type); $this->parameters->add($parameter); @@ -395,10 +395,14 @@ abstract class AbstractQuery * * @return array * - * @throws ORMInvalidArgumentException + * @throws \Doctrine\ORM\ORMInvalidArgumentException */ public function processParameterValue($value) { + if (is_scalar($value)) { + return $value; + } + if (is_array($value)) { foreach ($value as $key => $paramValue) { $paramValue = $this->processParameterValue($paramValue); @@ -889,7 +893,7 @@ abstract class AbstractQuery $this->setParameters($parameters); } - $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $rsm = $this->getResultSetMapping(); $stmt = $this->_doExecute(); return $this->_em->newHydrator($this->_hydrationMode)->iterate($stmt, $rsm, $this->_hints); @@ -962,7 +966,7 @@ abstract class AbstractQuery return $stmt; } - $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $rsm = $this->getResultSetMapping(); $data = $this->_em->newHydrator($this->_hydrationMode)->hydrateAll($stmt, $rsm, $this->_hints); $setCacheEntry($data); @@ -980,13 +984,12 @@ abstract class AbstractQuery */ private function executeUsingQueryCache($parameters = null, $hydrationMode = null) { - $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $rsm = $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); + $result = $queryCache->get($querykey, $rsm, $this->_hints); if ($result !== null) { - if ($this->cacheLogger) { $this->cacheLogger->queryCacheHit($queryCache->getRegion()->getName(), $querykey); } @@ -995,14 +998,14 @@ abstract class AbstractQuery } $result = $this->executeIgnoreQueryCache($parameters, $hydrationMode); - $cached = $queryCache->put($querykey, $rsm, $result); + $cached = $queryCache->put($querykey, $rsm, $result, $this->_hints); if ($this->cacheLogger) { $this->cacheLogger->queryCacheMiss($queryCache->getRegion()->getName(), $querykey); - } - if ($this->cacheLogger && $cached) { - $this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $querykey); + if ($cached) { + $this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $querykey); + } } return $result; @@ -1089,15 +1092,18 @@ abstract class AbstractQuery */ protected function getHash() { - $hints = $this->getHints(); + $self = $this; $query = $this->getSQL(); - $params = array(); + $hints = $this->getHints(); + $params = array_map(function(Parameter $parameter) use ($self) { + // Small optimization + // Does not invoke processParameterValue for scalar values + if (is_scalar($value = $parameter->getValue())) { + return $value; + } - foreach ($this->parameters as $parameter) { - $value = $parameter->getValue(); - - $params[$parameter->getName()] = is_scalar($value) ? $value : $this->processParameterValue($value); - } + return $self->processParameterValue($value); + }, $this->parameters->getValues()); ksort($hints); diff --git a/lib/Doctrine/ORM/Cache.php b/lib/Doctrine/ORM/Cache.php index 068591253..6cd8b4814 100644 --- a/lib/Doctrine/ORM/Cache.php +++ b/lib/Doctrine/ORM/Cache.php @@ -51,7 +51,7 @@ interface Cache const MODE_NORMAL = 3; /** - * The session will never read items from the cache, + * The query 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; @@ -59,7 +59,7 @@ interface Cache /** * Construct * - * @param \Doctrine\ORMEntityManagerInterface $em + * @param \Doctrine\ORM\EntityManagerInterface $em */ public function __construct(EntityManagerInterface $em); diff --git a/lib/Doctrine/ORM/Cache/CacheConfiguration.php b/lib/Doctrine/ORM/Cache/CacheConfiguration.php index 78540f3f7..42a1ba3ba 100644 --- a/lib/Doctrine/ORM/Cache/CacheConfiguration.php +++ b/lib/Doctrine/ORM/Cache/CacheConfiguration.php @@ -98,7 +98,7 @@ class CacheConfiguration */ public function getRegionsConfiguration() { - if ($this->regionsConfig == null) { + if ($this->regionsConfig === null) { $this->regionsConfig = new RegionsConfiguration(); } @@ -118,7 +118,7 @@ class CacheConfiguration */ public function getQueryValidator() { - if ($this->queryValidator == null) { + if ($this->queryValidator === null) { $this->queryValidator = new TimestampQueryCacheValidator(); } @@ -136,7 +136,7 @@ class CacheConfiguration /** * @param string $className * - * @throws ORMException If not is a \Doctrine\ORM\Cache + * @throws \Doctrine\ORM\ORMException If is not a \Doctrine\ORM\Cache */ public function setCacheClassName($className) { diff --git a/lib/Doctrine/ORM/Cache/CacheEntry.php b/lib/Doctrine/ORM/Cache/CacheEntry.php index cdcb6c0fb..c34b0ff9b 100644 --- a/lib/Doctrine/ORM/Cache/CacheEntry.php +++ b/lib/Doctrine/ORM/Cache/CacheEntry.php @@ -23,6 +23,10 @@ namespace Doctrine\ORM\Cache; /** * Cache entry interface * + * IMPORTANT NOTE: + * + * Fields of classes that implement CacheEntry are public for performance reason. + * * @since 2.5 * @author Fabio B. Silva */ diff --git a/lib/Doctrine/ORM/Cache/CacheFactory.php b/lib/Doctrine/ORM/Cache/CacheFactory.php index 01ffa45c9..b1ce7f8e3 100644 --- a/lib/Doctrine/ORM/Cache/CacheFactory.php +++ b/lib/Doctrine/ORM/Cache/CacheFactory.php @@ -27,6 +27,8 @@ use Doctrine\ORM\Persisters\CollectionPersister; use Doctrine\ORM\Persisters\EntityPersister; /** + * Contract for building second level cache regions components. + * * @since 2.5 * @author Fabio B. Silva */ diff --git a/lib/Doctrine/ORM/Cache/CollectionHydrator.php b/lib/Doctrine/ORM/Cache/CollectionHydrator.php index 9b3141dd5..05f0d35a1 100644 --- a/lib/Doctrine/ORM/Cache/CollectionHydrator.php +++ b/lib/Doctrine/ORM/Cache/CollectionHydrator.php @@ -26,7 +26,7 @@ use Doctrine\ORM\Cache\CollectionCacheKey; use Doctrine\ORM\Cache\CollectionCacheEntry; /** - * Hidrator cache entry for collections + * Hydrator cache entry for collections * * @since 2.5 * @author Fabio B. Silva @@ -46,7 +46,7 @@ interface CollectionHydrator * @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. + * @param \Doctrine\ORM\PersistentCollection $collection The collection to load the cache into. * * @return array */ diff --git a/lib/Doctrine/ORM/Cache/DefaultCache.php b/lib/Doctrine/ORM/Cache/DefaultCache.php index 1e76653d9..cd4c52ca0 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultCache.php @@ -22,10 +22,8 @@ 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; @@ -111,13 +109,12 @@ class DefaultCache implements Cache { $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); + return $persister->getCacheRegion()->contains($this->buildEntityCacheKey($metadata, $identifier)); } /** @@ -127,13 +124,12 @@ class DefaultCache implements Cache { $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); + $persister->getCacheRegion()->evict($this->buildEntityCacheKey($metadata, $identifier)); } /** @@ -176,13 +172,12 @@ class DefaultCache implements Cache { $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); + return $persister->getCacheRegion()->contains($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier)); } /** @@ -192,13 +187,12 @@ class DefaultCache implements Cache { $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); + $persister->getCacheRegion()->evict($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier)); } /** diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 24898bb56..5781f3936 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -20,17 +20,17 @@ namespace Doctrine\ORM\Cache; +use Doctrine\Common\Cache\CacheProvider; + use Doctrine\ORM\Cache; use Doctrine\ORM\Cache\Region; use Doctrine\ORM\Cache\TimestampRegion; - use Doctrine\ORM\Cache\RegionsConfiguration; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Cache\Region\DefaultRegion; use Doctrine\ORM\Cache\Region\FileLockRegion; use Doctrine\ORM\Cache\Region\UpdateTimestampCache; -use Doctrine\Common\Cache\Cache as CacheDriver; use Doctrine\ORM\Persisters\EntityPersister; use Doctrine\ORM\Persisters\CollectionPersister; use Doctrine\ORM\Cache\Persister\ReadOnlyCachedEntityPersister; @@ -47,7 +47,7 @@ use Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedCollectionPersister; class DefaultCacheFactory implements CacheFactory { /** - * @var \Doctrine\Common\Cache\Cache + * @var \Doctrine\Common\Cache\CacheProvider */ private $cache; @@ -73,9 +73,9 @@ class DefaultCacheFactory implements CacheFactory /** * @param \Doctrine\ORM\Cache\RegionsConfiguration $cacheConfig - * @param \Doctrine\Common\Cache\Cache $cache + * @param \Doctrine\Common\Cache\CacheProvider $cache */ - public function __construct(RegionsConfiguration $cacheConfig, CacheDriver $cache) + public function __construct(RegionsConfiguration $cacheConfig, CacheProvider $cache) { $this->cache = $cache; $this->regionsConfig = $cacheConfig; @@ -113,7 +113,6 @@ class DefaultCacheFactory implements CacheFactory $this->timestampRegion = $region; } - /** * {@inheritdoc} */ @@ -202,7 +201,7 @@ class DefaultCacheFactory implements CacheFactory if ( ! $this->fileLockRegionDirectory) { throw new \RuntimeException( - 'To use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' . + 'If you want 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(). ' ); } diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php index c1648fb7b..479bfbfea 100644 --- a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -89,7 +89,7 @@ class DefaultQueryCache implements QueryCache /** * {@inheritdoc} */ - public function get(QueryCacheKey $key, ResultSetMapping $rsm) + public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = array()) { if ( ! ($key->cacheMode & Cache::MODE_GET)) { return null; @@ -108,13 +108,13 @@ class DefaultQueryCache implements QueryCache } $result = array(); - $entityName = reset($rsm->aliasMap); //@TODO find root entity + $entityName = reset($rsm->aliasMap); $hasRelation = ( ! empty($rsm->relationMap)); $persister = $this->uow->getEntityPersister($entityName); $region = $persister->getCacheRegion(); $regionName = $region->getName(); - // @TODO - move to cache hydration componente + // @TODO - move to cache hydration component foreach ($entry->result as $index => $entry) { if (($entityEntry = $region->get($entityKey = new EntityCacheKey($entityName, $entry['identifier']))) === null) { @@ -204,10 +204,18 @@ class DefaultQueryCache implements QueryCache /** * {@inheritdoc} */ - public function put(QueryCacheKey $key, ResultSetMapping $rsm, array $result) + public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = array()) { if ($rsm->scalarMappings) { - throw new CacheException("Second level cache does not suport scalar results."); + throw new CacheException("Second level cache does not support scalar results."); + } + + if (count($rsm->entityMappings) > 1) { + throw new CacheException("Second level cache does not support multiple root entities."); + } + + if ( ! $rsm->isSelect) { + throw new CacheException("Second-level cache query supports only select statements."); } if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) { @@ -219,7 +227,7 @@ class DefaultQueryCache implements QueryCache } $data = array(); - $entityName = reset($rsm->aliasMap); //@TODO find root entity + $entityName = reset($rsm->aliasMap); $hasRelation = ( ! empty($rsm->relationMap)); $metadata = $this->em->getClassMetadata($entityName); $persister = $this->uow->getEntityPersister($entityName); @@ -246,7 +254,7 @@ class DefaultQueryCache implements QueryCache continue; } - // @TODO - move to cache hydration componente + // @TODO - move to cache hydration components foreach ($rsm->relationMap as $name) { $assoc = $metadata->associationMappings[$name]; diff --git a/lib/Doctrine/ORM/Cache/EntityHydrator.php b/lib/Doctrine/ORM/Cache/EntityHydrator.php index 3e382cd75..569561101 100644 --- a/lib/Doctrine/ORM/Cache/EntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/EntityHydrator.php @@ -25,7 +25,7 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Cache\EntityCacheEntry; /** - * Hidrator cache entry for entities + * Hydrator cache entry for entities * * @since 2.5 * @author Fabio B. Silva diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php index ca56a1bcc..baacbd4c2 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php @@ -145,7 +145,6 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister */ public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key) { - if (($cache = $this->region->get($key)) === null) { return null; } @@ -164,7 +163,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister { $targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName); $targetRegion = $targetPersister->getCacheRegion(); - $targetHidrator = $targetPersister->getEntityHydrator(); + $targetHydrator = $targetPersister->getEntityHydrator(); $entry = $this->hydrator->buildCacheEntry($this->targetEntity, $key, $elements); foreach ($entry->identifiers as $index => $identifier) { @@ -182,7 +181,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister } $entity = $elements[$index]; - $entityEntry = $targetHidrator->buildCacheEntry($class, $entityKey, $entity); + $entityEntry = $targetHydrator->buildCacheEntry($class, $entityKey, $entity); $targetRegion->put($entityKey, $entityEntry); } diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php index 4eeef07e7..4f78c7354 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -179,7 +179,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister 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)) { @@ -302,7 +301,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ 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); } @@ -317,7 +315,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $result = $queryCache->get($querykey, $rsm); if ($result !== null) { - if ($this->cacheLogger) { $this->cacheLogger->queryCacheHit($this->regionName, $querykey); } @@ -331,12 +328,14 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $cached = $queryCache->put($querykey, $rsm, array($result)); - if ($this->cacheLogger && $result) { - $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); - } + if ($this->cacheLogger) { + if ($result) { + $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); + } - if ($this->cacheLogger && $cached) { - $this->cacheLogger->queryCachePut($this->regionName, $querykey); + if ($cached) { + $this->cacheLogger->queryCachePut($this->regionName, $querykey); + } } return $result; @@ -356,7 +355,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $result = $queryCache->get($querykey, $rsm); if ($result !== null) { - if ($this->cacheLogger) { $this->cacheLogger->queryCacheHit($this->regionName, $querykey); } @@ -367,12 +365,14 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $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) { + if ($result) { + $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); + } - if ($this->cacheLogger && $cached) { - $this->cacheLogger->queryCachePut($this->regionName, $querykey); + if ($cached) { + $this->cacheLogger->queryCachePut($this->regionName, $querykey); + } } return $result; @@ -388,13 +388,11 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $class = $this->class; if ($cacheEntry !== null) { - if ($cacheEntry->class !== $this->class->name) { $class = $this->metadataFactory->getMetadataFor($cacheEntry->class); } if (($entity = $this->hydrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity)) !== null) { - if ($this->cacheLogger) { $this->cacheLogger->entityCacheHit($this->regionName, $cacheKey); } @@ -419,11 +417,11 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $cacheEntry = $this->hydrator->buildCacheEntry($class, $cacheKey, $entity); $cached = $this->region->put($cacheKey, $cacheEntry); - if ($this->cacheLogger && $cached) { - $this->cacheLogger->entityCachePut($this->regionName, $cacheKey); - } - if ($this->cacheLogger) { + if ($cached) { + $this->cacheLogger->entityCachePut($this->regionName, $cacheKey); + } + $this->cacheLogger->entityCacheMiss($this->regionName, $cacheKey); } @@ -453,7 +451,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $list = $persister->loadCollectionCache($coll, $key); if ($list !== null) { - if ($this->cacheLogger) { $this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key); } @@ -489,7 +486,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $list = $persister->loadCollectionCache($coll, $key); if ($list !== null) { - if ($this->cacheLogger) { $this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key); } diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php index 619a520b3..6f7d75596 100644 --- a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php @@ -87,7 +87,6 @@ class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPers // 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; diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php index a9a9a37e9..3d6004e96 100644 --- a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php @@ -41,7 +41,6 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister if (isset($this->queuedCache['insert'])) { foreach ($this->queuedCache['insert'] as $entity) { - $class = $this->class; $className = ClassUtils::getClass($entity); @@ -62,7 +61,6 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister if (isset($this->queuedCache['update'])) { foreach ($this->queuedCache['update'] as $entity) { - $class = $this->class; $className = ClassUtils::getClass($entity); diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php index 573257bdd..967ccf69a 100644 --- a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php @@ -33,11 +33,6 @@ use Doctrine\ORM\PersistentCollection; */ 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. diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php index d77aabf9e..1b3af3ab7 100644 --- a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php @@ -35,11 +35,6 @@ use Doctrine\ORM\Cache\EntityCacheKey; */ 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. diff --git a/lib/Doctrine/ORM/Cache/QueryCache.php b/lib/Doctrine/ORM/Cache/QueryCache.php index 7bdc0d337..5eb202281 100644 --- a/lib/Doctrine/ORM/Cache/QueryCache.php +++ b/lib/Doctrine/ORM/Cache/QueryCache.php @@ -39,19 +39,21 @@ interface QueryCache /** * @param \Doctrine\ORM\Cache\QueryCacheKey $key * @param \Doctrine\ORM\Query\ResultSetMapping $rsm - * @param array $result + * @param mixed $result + * @param array $hints * * @return boolean */ - public function put(QueryCacheKey $key, ResultSetMapping $rsm, array $result); + public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = array()); /** - * @param \Doctrine\ORM\Cache\QueryCacheKey $key - * @param \Doctrine\ORM\Query\ResultSetMapping $rsm + * @param \Doctrine\ORM\Cache\QueryCacheKey $key + * @param \Doctrine\ORM\Query\ResultSetMapping $rsm + * @param array $hints * * @return void */ - public function get(QueryCacheKey $key, ResultSetMapping $rsm); + public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = array()); /** * @return \Doctrine\ORM\Cache\Region diff --git a/lib/Doctrine/ORM/Cache/QueryCacheKey.php b/lib/Doctrine/ORM/Cache/QueryCacheKey.php index 31a3d15d8..7001cc332 100644 --- a/lib/Doctrine/ORM/Cache/QueryCacheKey.php +++ b/lib/Doctrine/ORM/Cache/QueryCacheKey.php @@ -20,6 +20,8 @@ namespace Doctrine\ORM\Cache; +use Doctrine\ORM\Cache; + /** * A key that identifies a particular query. * @@ -43,7 +45,7 @@ class QueryCacheKey extends CacheKey * @param integer $lifetime Query lifetime * @param integer $cacheMode Query cache mode */ - public function __construct($hash, $lifetime = 0, $cacheMode = 3) + public function __construct($hash, $lifetime = 0, $cacheMode = Cache::MODE_NORMAL) { $this->hash = $hash; $this->lifetime = $lifetime; diff --git a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php index b5735e9f6..b4b5b1b1d 100644 --- a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -34,8 +34,6 @@ use Doctrine\Common\Cache\CacheProvider; */ class DefaultRegion implements Region { - const ENTRY_KEY = '_entry_'; - /** * @var \Doctrine\Common\Cache\CacheProvider */ @@ -86,7 +84,7 @@ class DefaultRegion implements Region */ public function contains(CacheKey $key) { - return $this->cache->contains($this->name . self::ENTRY_KEY . $key->hash); + return $this->cache->contains($this->name . '_' . $key->hash); } /** @@ -94,7 +92,7 @@ class DefaultRegion implements Region */ public function get(CacheKey $key) { - return $this->cache->fetch($this->name . self::ENTRY_KEY . $key->hash) ?: null; + return $this->cache->fetch($this->name . '_' . $key->hash) ?: null; } /** @@ -102,7 +100,7 @@ class DefaultRegion implements Region */ public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) { - return $this->cache->save($this->name . self::ENTRY_KEY . $key->hash, $entry, $this->lifetime); + return $this->cache->save($this->name . '_' . $key->hash, $entry, $this->lifetime); } /** @@ -110,7 +108,7 @@ class DefaultRegion implements Region */ public function evict(CacheKey $key) { - return $this->cache->delete($this->name . self::ENTRY_KEY . $key->hash); + return $this->cache->delete($this->name . '_' . $key->hash); } /** diff --git a/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php index 680f1940b..956e7634f 100644 --- a/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php @@ -77,9 +77,9 @@ class FileLockRegion implements ConcurrentRegion * param \Doctrine\ORM\Cache\CacheKey $key * param \Doctrine\ORM\Cache\Lock $lock * - * return boolean + * @return boolean */ - private function isLoked(CacheKey $key, Lock $lock = null) + private function isLocked(CacheKey $key, Lock $lock = null) { $filename = $this->getLockFileName($key); @@ -153,7 +153,7 @@ class FileLockRegion implements ConcurrentRegion */ public function contains(CacheKey $key) { - if ($this->isLoked($key)) { + if ($this->isLocked($key)) { return false; } @@ -165,7 +165,7 @@ class FileLockRegion implements ConcurrentRegion */ public function get(CacheKey $key) { - if ($this->isLoked($key)) { + if ($this->isLocked($key)) { return null; } @@ -177,7 +177,7 @@ class FileLockRegion implements ConcurrentRegion */ public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) { - if ($this->isLoked($key, $lock)) { + if ($this->isLocked($key, $lock)) { return false; } @@ -189,7 +189,7 @@ class FileLockRegion implements ConcurrentRegion */ public function evict(CacheKey $key) { - if ($this->isLoked($key)) { + if ($this->isLocked($key)) { @unlink($this->getLockFileName($key)); } @@ -213,7 +213,7 @@ class FileLockRegion implements ConcurrentRegion */ public function lock(CacheKey $key) { - if ($this->isLoked($key)) { + if ($this->isLocked($key)) { return null; } @@ -232,7 +232,7 @@ class FileLockRegion implements ConcurrentRegion */ public function unlock(CacheKey $key, Lock $lock) { - if ($this->isLoked($key, $lock)) { + if ($this->isLocked($key, $lock)) { return false; } diff --git a/lib/Doctrine/ORM/Cache/RegionsConfiguration.php b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php index 58bcbdef9..1a847943b 100644 --- a/lib/Doctrine/ORM/Cache/RegionsConfiguration.php +++ b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php @@ -97,11 +97,9 @@ class RegionsConfiguration */ public function getLifetime($regionName) { - if (isset($this->lifetimes[$regionName])) { - return $this->lifetimes[$regionName]; - } - - return $this->defaultLifetime; + return isset($this->lifetimes[$regionName]) + ? $this->lifetimes[$regionName] + : $this->defaultLifetime; } /** @@ -120,11 +118,9 @@ class RegionsConfiguration */ public function getLockLifetime($regionName) { - if (isset($this->lockLifetimes[$regionName])) { - return $this->lockLifetimes[$regionName]; - } - - return $this->defaultLockLifetime; + return isset($this->lockLifetimes[$regionName]) + ? $this->lockLifetimes[$regionName] + : $this->defaultLockLifetime; } /** diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php index 3ad05c96a..f7617b3bb 100644 --- a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php +++ b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php @@ -29,12 +29,12 @@ namespace Doctrine\ORM\Cache; class TimestampCacheEntry implements CacheEntry { /** - * @var integer + * @var float */ public $time; /** - * @param array $result + * @param float $time */ public function __construct($time = null) { diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index 778f44d84..c261723f7 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -82,8 +82,8 @@ class XmlDriver extends FileDriver $metadata->setPrimaryTable($table); // Evaluate second level cache - if (isset($xmlRoot->{'cache'})) { - $metadata->enableCache($this->cacheToArray($xmlRoot->{'cache'})); + if (isset($xmlRoot->cache)) { + $metadata->enableCache($this->cacheToArray($xmlRoot->cache)); } // Evaluate named queries @@ -356,8 +356,8 @@ class XmlDriver extends FileDriver $metadata->mapOneToOne($mapping); // Evaluate second level cache - if (isset($oneToOneElement->{'cache'})) { - $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement->{'cache'})); + if (isset($oneToOneElement->cache)) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement->cache)); } } } @@ -400,8 +400,8 @@ class XmlDriver extends FileDriver $metadata->mapOneToMany($mapping); // Evaluate second level cache - if (isset($oneToManyElement->{'cache'})) { - $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement->{'cache'})); + if (isset($oneToManyElement->cache)) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement->cache)); } } } @@ -445,8 +445,8 @@ class XmlDriver extends FileDriver $metadata->mapManyToOne($mapping); // Evaluate second level cache - if (isset($manyToOneElement->{'cache'})) { - $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement->{'cache'})); + if (isset($manyToOneElement->cache)) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement->cache)); } } } @@ -515,8 +515,8 @@ class XmlDriver extends FileDriver $metadata->mapManyToMany($mapping); // Evaluate second level cache - if (isset($manyToManyElement->{'cache'})) { - $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement->{'cache'})); + if (isset($manyToManyElement->cache)) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement->cache)); } } } @@ -733,8 +733,16 @@ class XmlDriver extends FileDriver */ 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; + $region = isset($cacheMapping['region']) ? (string) $cacheMapping['region'] : null; + $usage = isset($cacheMapping['usage']) ? strtoupper($cacheMapping['usage']) : null; + + if ($usage && ! defined('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage)) { + throw new \InvalidArgumentException(sprintf('Invalid cache usage "%s"', $usage)); + } + + if ($usage) { + $usage = constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage); + } return array( 'usage' => $usage, diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 80b2d6a2a..e5eac7e4f 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -738,8 +738,16 @@ class YamlDriver extends FileDriver */ 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; + $region = isset($cacheMapping['region']) ? (string) $cacheMapping['region'] : null; + $usage = isset($cacheMapping['usage']) ? strtoupper($cacheMapping['usage']) : null; + + if ($usage && ! defined('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage)) { + throw new \InvalidArgumentException(sprintf('Invalid cache usage "%s"', $usage)); + } + + if ($usage) { + $usage = constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage); + } return array( 'usage' => $usage, diff --git a/lib/Doctrine/ORM/ORMException.php b/lib/Doctrine/ORM/ORMException.php index 8349c4f6d..b9a0b9cec 100644 --- a/lib/Doctrine/ORM/ORMException.php +++ b/lib/Doctrine/ORM/ORMException.php @@ -109,7 +109,7 @@ class ORMException extends Exception * * @return \Doctrine\ORM\ORMInvalidArgumentException */ - static public function unexpectedAssociationValue($class, $association, $given, $expected) + public static 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)); } diff --git a/lib/Doctrine/ORM/Persisters/CollectionPersister.php b/lib/Doctrine/ORM/Persisters/CollectionPersister.php index f8d194442..f99d57805 100644 --- a/lib/Doctrine/ORM/Persisters/CollectionPersister.php +++ b/lib/Doctrine/ORM/Persisters/CollectionPersister.php @@ -73,8 +73,6 @@ interface CollectionPersister * @param \Doctrine\ORM\PersistentCollection $collection * * @return integer - * - * @throws \BadMethodCallException */ public function count(PersistentCollection $collection); diff --git a/lib/Doctrine/ORM/Persisters/EntityPersister.php b/lib/Doctrine/ORM/Persisters/EntityPersister.php index a27a59d14..fde9d5540 100644 --- a/lib/Doctrine/ORM/Persisters/EntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/EntityPersister.php @@ -22,7 +22,6 @@ 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. diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index de8411df0..1722d58a3 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -309,7 +309,7 @@ final class Query extends AbstractQuery foreach ($this->parameters as $parameter) { $key = $parameter->getName(); $value = $parameter->getValue(); - $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $rsm = $this->getResultSetMapping(); if ( ! isset($paramMappings[$key])) { throw QueryException::unknownParameter($key); diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index f4d11cf44..07b896695 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -43,6 +43,14 @@ class ResultSetMapping */ public $isMixed = false; + /** + * Whether the result is a select statement. + * + * @ignore + * @var boolean + */ + public $isSelect = true; + /** * Maps alias names to class names. * diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 0852664ec..cdb1bb24f 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -573,6 +573,7 @@ class SqlWalker implements TreeWalker public function walkUpdateStatement(AST\UpdateStatement $AST) { $this->useSqlTableAliases = false; + $this->rsm->isSelect = false; return $this->walkUpdateClause($AST->updateClause) . $this->walkWhereClause($AST->whereClause); @@ -584,6 +585,7 @@ class SqlWalker implements TreeWalker public function walkDeleteStatement(AST\DeleteStatement $AST) { $this->useSqlTableAliases = false; + $this->rsm->isSelect = false; return $this->walkDeleteClause($AST->deleteClause) . $this->walkWhereClause($AST->whereClause); diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index e0909d4b6..023f742ab 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -2539,7 +2539,7 @@ class UnitOfWork implements PropertyChangedListener $overrideLocalValues = isset($hints[Query::HINT_REFRESH]); // If only a specific entity is set to refresh, check that it's the one - if(isset($hints[Query::HINT_REFRESH_ENTITY])) { + if (isset($hints[Query::HINT_REFRESH_ENTITY])) { $overrideLocalValues = $hints[Query::HINT_REFRESH_ENTITY] === $entity; } @@ -3255,13 +3255,13 @@ class UnitOfWork implements PropertyChangedListener } foreach ($this->persisters as $persister) { - if($persister instanceof CachedPersister) { + if ($persister instanceof CachedPersister) { $persister->afterTransactionComplete(); } } foreach ($this->collectionPersisters as $persister) { - if($persister instanceof CachedPersister) { + if ($persister instanceof CachedPersister) { $persister->afterTransactionComplete(); } } @@ -3277,13 +3277,13 @@ class UnitOfWork implements PropertyChangedListener } foreach ($this->persisters as $persister) { - if($persister instanceof CachedPersister) { + if ($persister instanceof CachedPersister) { $persister->afterTransactionRolledBack(); } } foreach ($this->collectionPersisters as $persister) { - if($persister instanceof CachedPersister) { + if ($persister instanceof CachedPersister) { $persister->afterTransactionRolledBack(); } } diff --git a/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php index 9f5f4cc39..243f9b63e 100644 --- a/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php +++ b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php @@ -17,7 +17,7 @@ class CacheMetadataListener 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE ); - /* @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */ + /** @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */ if (strstr($metadata->name, 'Doctrine\Tests\Models\Cache')) { return; } diff --git a/tests/Doctrine/Tests/Mocks/CacheRegionMock.php b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php index 06a40c2eb..a4ef52ca3 100644 --- a/tests/Doctrine/Tests/Mocks/CacheRegionMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php @@ -18,13 +18,13 @@ class CacheRegionMock implements Region $this->returns[$method][] = $value; } - public function getReturn($method, $datault) + public function getReturn($method, $default) { if (isset($this->returns[$method]) && ! empty($this->returns[$method])) { return array_shift($this->returns[$method]); } - return $datault; + return $default; } public function getName() diff --git a/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php index a84672bea..e299f2a70 100644 --- a/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php +++ b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php @@ -31,7 +31,7 @@ class ConcurrentRegionMock implements ConcurrentRegion if (isset($this->exceptions[$method]) && ! empty($this->exceptions[$method])) { $exception = array_shift($this->exceptions[$method]); - if($exception != null) { + if ($exception != null) { throw $exception; } } diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php index fa7cb219a..f597a5134 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -253,7 +253,7 @@ class DefaultCacheFactoryTest extends OrmTestCase /** * @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 + * @expectedExceptionMessage If you want 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() { diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php index fed80adc0..dfe385813 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -236,7 +236,7 @@ class DefaultQueryCacheTest extends OrmTestCase $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); - $result = $this->queryCache->get($key, $rsm, $entry); + $result = $this->queryCache->get($key, $rsm); $this->assertCount(2, $result); $this->assertInstanceOf(Country::CLASSNAME, $result[0]); @@ -349,7 +349,7 @@ class DefaultQueryCacheTest extends OrmTestCase $this->region->addReturn('get', $entry); - $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + $this->assertNull($this->queryCache->get($key, $rsm)); } public function testIgnoreCacheNonPutMode() @@ -394,7 +394,7 @@ class DefaultQueryCacheTest extends OrmTestCase $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); - $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + $this->assertNull($this->queryCache->get($key, $rsm)); } public function testGetShouldIgnoreNonQueryCacheEntryResult() @@ -417,7 +417,7 @@ class DefaultQueryCacheTest extends OrmTestCase $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); - $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + $this->assertNull($this->queryCache->get($key, $rsm)); } public function testGetShouldIgnoreMissingEntityQueryCacheEntry() @@ -434,7 +434,7 @@ class DefaultQueryCacheTest extends OrmTestCase $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); - $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + $this->assertNull($this->queryCache->get($key, $rsm)); } /** @@ -463,7 +463,7 @@ class DefaultQueryCacheTest extends OrmTestCase /** * @expectedException Doctrine\ORM\Cache\CacheException - * @expectedExceptionMessage Second level cache does not suport scalar results. + * @expectedExceptionMessage Second level cache does not support scalar results. */ public function testScalarResultException() { @@ -476,6 +476,22 @@ class DefaultQueryCacheTest extends OrmTestCase $this->queryCache->put($key, $rsm, $result); } + /** + * @expectedException Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Second level cache does not support multiple root entities. + */ + public function testSuportMultipleRootEntitiesException() + { + $result = array(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + + $rsm->addEntityResult('Doctrine\Tests\Models\Cache\City', 'e1'); + $rsm->addEntityResult('Doctrine\Tests\Models\Cache\State', 'e2'); + + $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. diff --git a/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php b/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php index 94cd3985d..e699bb39f 100644 --- a/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php @@ -8,7 +8,6 @@ use Doctrine\ORM\Cache\ConcurrentRegion; use Doctrine\Tests\Mocks\CacheEntryMock; use Doctrine\Tests\Mocks\CacheKeyMock; use Doctrine\ORM\Cache\CacheKey; -use Doctrine\ORM\Cache\Lock; use RecursiveIteratorIterator; use RecursiveDirectoryIterator; diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php index fa8e20b53..593073e8d 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php @@ -18,8 +18,6 @@ use Doctrine\Tests\Models\Cache\Bar; use Doctrine\Tests\Models\Cache\AttractionContactInfo; use Doctrine\Tests\Models\Cache\AttractionLocationInfo; -require_once __DIR__ . '/../../TestInit.php'; - /** * @group DDC-2183 */ diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositePrimaryKeyTest.php similarity index 98% rename from tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php rename to tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositePrimaryKeyTest.php index 5c58b0aaa..f5c105225 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositePrimaryKeyTest.php @@ -2,14 +2,13 @@ namespace Doctrine\Tests\ORM\Functional; - use Doctrine\Tests\Models\Cache\City; use Doctrine\Tests\Models\Cache\Flight; /** * @group DDC-2183 */ -class SecondLevelCacheCompositPrimaryKeyTest extends SecondLevelCacheAbstractTest +class SecondLevelCacheCompositePrimaryKeyTest extends SecondLevelCacheAbstractTest { public function testPutAndLoadCompositPrimaryKeyEntities() { diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php index b96c41e1f..366f846f8 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php @@ -2,7 +2,6 @@ namespace Doctrine\Tests\ORM\Functional; - use Doctrine\Tests\Models\Cache\City; use Doctrine\Tests\Models\Cache\State; use Doctrine\Tests\Models\Cache\Travel; diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php index b063cd319..e81b08b96 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php @@ -7,7 +7,6 @@ use Doctrine\Tests\Models\Cache\AttractionInfo; use Doctrine\Tests\Models\Cache\AttractionContactInfo; use Doctrine\Tests\Models\Cache\AttractionLocationInfo; - /** * @group DDC-2183 */ diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php index 931ba88ba..c00820c91 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php @@ -2,7 +2,6 @@ namespace Doctrine\Tests\ORM\Functional; - use Doctrine\Tests\Models\Cache\City; use Doctrine\Tests\Models\Cache\State; use Doctrine\Tests\Models\Cache\Travel; diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php index c326128cb..70dbf3cd3 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -9,6 +9,7 @@ use Doctrine\Tests\Models\Cache\City; use Doctrine\ORM\Cache\QueryCacheKey; use Doctrine\ORM\Cache\EntityCacheKey; use Doctrine\ORM\Cache\EntityCacheEntry; +use Doctrine\ORM\Query; use Doctrine\ORM\Cache; /** @@ -840,4 +841,26 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest ->setCacheable(true) ->getResult(); } + + /** + * @expectedException \Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Second-level cache query supports only select statements. + */ + public function testNonCacheableQueryDeleteStatementException() + { + $this->_em->createQuery('DELETE Doctrine\Tests\Models\Cache\Country u WHERE u.id = 4') + ->setCacheable(true) + ->getResult(); + } + + /** + * @expectedException \Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Second-level cache query supports only select statements. + */ + public function testNonCacheableQueryUpdateStatementException() + { + $this->_em->createQuery('UPDATE Doctrine\Tests\Models\Cache\Country u SET u.name = NULL WHERE u.id = 4') + ->setCacheable(true) + ->getResult(); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php b/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php index 3e628acbd..61b9c7ce7 100644 --- a/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php @@ -9,8 +9,6 @@ use Doctrine\Tests\Models\Cache\State; use Doctrine\Tests\Models\Cache\City; use Doctrine\ORM\EntityManagerInterface; -require_once __DIR__ . '/../../TestInit.php'; - /** * @group DDC-2183 * @group performance diff --git a/tests/Doctrine/Tests/OrmTestCase.php b/tests/Doctrine/Tests/OrmTestCase.php index 8546fae03..fc713805a 100644 --- a/tests/Doctrine/Tests/OrmTestCase.php +++ b/tests/Doctrine/Tests/OrmTestCase.php @@ -10,7 +10,6 @@ use Doctrine\ORM\Cache\DefaultCacheFactory; */ abstract class OrmTestCase extends DoctrineTestCase { - /** * The metadata cache that is shared between all ORM tests (except functional tests). * From d135e402bbaeb06f51010836af711c8f4b6618e3 Mon Sep 17 00:00:00 2001 From: fabios Date: Tue, 15 Oct 2013 17:55:10 -0400 Subject: [PATCH 5/9] handle update/delete queries --- docs/en/reference/second-level-cache.rst | 63 +++++++++++++++---- lib/Doctrine/ORM/AbstractQuery.php | 12 +++- lib/Doctrine/ORM/Query.php | 28 +++++++++ .../SecondLevelCacheQueryCacheTest.php | 36 ++++++++++- 4 files changed, 123 insertions(+), 16 deletions(-) diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst index 193a563f1..ea7c20ac3 100644 --- a/docs/en/reference/second-level-cache.rst +++ b/docs/en/reference/second-level-cache.rst @@ -829,13 +829,13 @@ However, you can use the cache API to check / invalidate cache entries. /* var $cache \Doctrine\ORM\Cache */ $cache = $em->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->containsEntity('Entity\State', 1) // Check if the cache exists + $cache->evictEntity('Entity\State', 1); // Remove an entity from cache + $cache->evictEntityRegion('Entity\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 + $cache->containsCollection('Entity\State', 'cities', 1); // Check if the cache exists + $cache->evictCollection('Entity\State', 'cities', 1); // Remove an entity collection from cache + $cache->evictCollectionRegion('Entity\State', 'cities'); // Remove all collections from cache Limitations ----------- @@ -843,10 +843,8 @@ 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. +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 @@ -895,4 +893,47 @@ A ``Doctrine\\ORM\\Cache\\ConcurrentRegion`` is designed to store concurrently m By default, Doctrine provides a 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 +for more details about how to implement a cache region please see :ref:`reference-second-level-cache-regions` + + +DELETE / UPDATE queries +~~~~~~~~~~~~~~~~~~~~~~~ + +DQL UPDATE / DELETE statements are ported directly into a database and bypass the second-level cache, +Entities that are already cached will NOT be invalidated. +However the cached data could be evicted using the cache API or an special query hint. + + +Execute the ``UPDATE`` and invalidate ``all cache entries`` using ``Query::HINT_CACHE_EVICT`` + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->setHint(Query::HINT_CACHE_EVICT, true) + ->execute(); + + +Execute the ``UPDATE`` and invalidate ``all cache entries`` using the cache API + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->execute(); + // Invoke Cache API + $em->getCache()->evictEntityRegion('Entity\Country'); + + +Execute the ``UPDATE`` and invalidate ``a specific cache entry`` using the cache API + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->execute(); + // Invoke Cache API + $em->getCache()->evictEntity('Entity\Country', 1); \ No newline at end of file diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 67606a0f9..d7ed6fd17 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -129,6 +129,11 @@ abstract class AbstractQuery */ protected $cacheable = false; + /** + * @var boolean + */ + protected $hasCache = false; + /** * Second level cache region name. * @@ -162,9 +167,10 @@ abstract class AbstractQuery { $this->_em = $em; $this->parameters = new ArrayCollection(); + $this->hasCache = $this->_em->getConfiguration()->isSecondLevelCacheEnabled(); - if ($this->_em->getConfiguration()->isSecondLevelCacheEnabled()) { - $this->cacheLogger = $em->getConfiguration() + if ($this->hasCache) { + $this->cacheLogger = $em->getConfiguration() ->getSecondLevelCacheConfiguration() ->getCacheLogger(); } @@ -220,7 +226,7 @@ abstract class AbstractQuery */ protected function isCacheEnabled() { - return $this->cacheable && $this->_em->getConfiguration()->isSecondLevelCacheEnabled(); + return $this->cacheable && $this->hasCache; } /** diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 1722d58a3..7be9dbd5b 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -64,6 +64,11 @@ final class Query extends AbstractQuery */ const HINT_CACHE_ENABLED = 'doctrine.cache.enabled'; + /** + * @var string + */ + const HINT_CACHE_EVICT = 'doctrine.cache.evict'; + /** * Internal hint: is set to the proxy entity that is currently triggered for loading * @@ -287,11 +292,34 @@ final class Query extends AbstractQuery throw QueryException::invalidParameterNumber(); } + // evict all cache for the entity region + if ($this->hasCache && isset($this->_hints[self::HINT_CACHE_EVICT]) && $this->_hints[self::HINT_CACHE_EVICT]) { + $this->evictEntityCacheRegion(); + } + list($sqlParams, $types) = $this->processParameterMappings($paramMappings); return $executor->execute($this->_em->getConnection(), $sqlParams, $types); } + /** + * Evict entity cache region + */ + private function evictEntityCacheRegion() + { + $AST = $this->getAST(); + + if ($AST instanceof \Doctrine\ORM\Query\AST\SelectStatement) { + throw new QueryException('The hint "HINT_CACHE_EVICT" is not valid for select statements.'); + } + + $className = ($AST instanceof \Doctrine\ORM\Query\AST\DeleteStatement) + ? $AST->deleteClause->abstractSchemaName + : $AST->updateClause->abstractSchemaName; + + $this->_em->getCache()->evictEntityRegion($className); + } + /** * Processes query parameter mappings. * diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php index 70dbf3cd3..77c54ad7f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -827,6 +827,38 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount('bar_region')); } + public function testHintClearEntityRegionUpdateStatement() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->assertTrue($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[1]->getId())); + + $this->_em->createQuery('DELETE Doctrine\Tests\Models\Cache\Country u WHERE u.id = 4') + ->setHint(Query::HINT_CACHE_EVICT, true) + ->execute(); + + $this->assertFalse($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[1]->getId())); + } + + public function testHintClearEntityRegionDeleteStatement() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->assertTrue($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[1]->getId())); + + $this->_em->createQuery("UPDATE Doctrine\Tests\Models\Cache\Country u SET u.name = 'foo' WHERE u.id = 1") + ->setHint(Query::HINT_CACHE_EVICT, true) + ->execute(); + + $this->assertFalse($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[1]->getId())); + } + /** * @expectedException \Doctrine\ORM\Cache\CacheException * @expectedExceptionMessage Second level cache does not support partial entities. @@ -848,7 +880,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest */ public function testNonCacheableQueryDeleteStatementException() { - $this->_em->createQuery('DELETE Doctrine\Tests\Models\Cache\Country u WHERE u.id = 4') + $this->_em->createQuery("DELETE Doctrine\Tests\Models\Cache\Country u WHERE u.id = 4") ->setCacheable(true) ->getResult(); } @@ -859,7 +891,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest */ public function testNonCacheableQueryUpdateStatementException() { - $this->_em->createQuery('UPDATE Doctrine\Tests\Models\Cache\Country u SET u.name = NULL WHERE u.id = 4') + $this->_em->createQuery("UPDATE Doctrine\Tests\Models\Cache\Country u SET u.name = 'foo' WHERE u.id = 4") ->setCacheable(true) ->getResult(); } From fb982777830e44e294308f714c11c708ffdbd750 Mon Sep 17 00:00:00 2001 From: fabios Date: Thu, 17 Oct 2013 12:11:56 -0400 Subject: [PATCH 6/9] Fix docs --- docs/en/reference/second-level-cache.rst | 69 +++++++++++-------- lib/Doctrine/ORM/AbstractQuery.php | 12 ++-- lib/Doctrine/ORM/Cache.php | 6 +- lib/Doctrine/ORM/Cache/CacheConfiguration.php | 8 +-- lib/Doctrine/ORM/Cache/DefaultCache.php | 2 +- .../ORM/Cache/DefaultCacheFactory.php | 12 ++-- lib/Doctrine/ORM/Cache/Region.php | 2 +- .../ORM/Cache/RegionsConfiguration.php | 12 ++-- lib/Doctrine/ORM/Cache/TimestampRegion.php | 2 +- lib/Doctrine/ORM/Mapping/Cache.php | 2 +- .../ORM/Mapping/ClassMetadataInfo.php | 4 +- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 2 + .../ORM/Mapping/Driver/YamlDriver.php | 2 + .../EventListener/CacheMetadataListener.php | 2 +- tests/Doctrine/Tests/Mocks/CacheEntryMock.php | 2 +- tests/Doctrine/Tests/Mocks/CacheKeyMock.php | 3 +- .../ORM/Cache/DefaultCacheFactoryTest.php | 2 +- 17 files changed, 80 insertions(+), 64 deletions(-) diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst index ea7c20ac3..ecf682b08 100644 --- a/docs/en/reference/second-level-cache.rst +++ b/docs/en/reference/second-level-cache.rst @@ -2,7 +2,7 @@ The Second Level Cache ====================== The Second Level Cache is designed to reduce 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. +It sits between your application and the database to avoid the number of database hits as much as possible. When 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. @@ -22,6 +22,9 @@ Each entity class, collection association and query has its region, where values 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. +Notice that when caching collection and queries only identifiers are stored. +The entity values will be stored in its own region + Something like below for an entity region : .. code-block:: php @@ -60,8 +63,7 @@ A query region might be something like : .. note:: - Notice that when caching collection and queries only identifiers are stored. - The entity values will be stored in its own region + The following data structures represents now the cache will looks like, this is not actual cached data. .. _reference-second-level-cache-regions: @@ -80,6 +82,11 @@ It allows you to provide your own cache implementation that might take advantage If you want to support locking for ``READ_WRITE`` strategies you should implement ``ConcurrentRegion``; ``CacheRegion`` otherwise. +Cache region +~~~~~~~~~~~~ + +Defines a contract for accessing a particular region. + ``Doctrine\ORM\Cache\Region`` Defines a contract for accessing a particular cache region. @@ -111,7 +118,7 @@ Defines a contract for accessing a particular cache region. * * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to be retrieved. * - * @return \Doctrine\ORM\Cache\CacheEntry The cached entry or NULL + * @return \Doctrine\ORM\Cache\CacheEntry|null The cached entry or NULL */ public function get(CacheKey $key); @@ -138,6 +145,14 @@ Defines a contract for accessing a particular cache region. } +Concurrent cache region +~~~~~~~~~~~~~~~~~~~~~~~ + +A ``Doctrine\ORM\Cache\ConcurrentRegion`` is designed to store concurrently managed data region. +By default, Doctrine provides a 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. + ``Doctrine\ORM\Cache\ConcurrentRegion`` Defines contract for concurrently managed data region. @@ -166,6 +181,9 @@ Defines contract for concurrently managed data region. public function unlock(CacheKey $key, Lock $lock); } +Cache region +~~~~~~~~~~~~ + ``Doctrine\ORM\Cache\TimestampRegion`` Tracks the timestamps of the most recent updates to particular entity. @@ -179,7 +197,7 @@ Tracks the timestamps of the most recent updates to particular entity. /** * Update an specific key into the cache region. * - * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to lock. + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to update the timestamp. * * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. */ @@ -364,8 +382,8 @@ To specify a default lifetime for all regions or specify a different lifetime fo $regionConfig = $cacheConfig->getRegionsConfiguration(); //Cache Region lifetime - $regionConfig->setLifetime('my_entity_region', 3600); - $regionConfig->setDefaultLifetime(7200); + $regionConfig->setLifetime('my_entity_region', 3600); // Time to live for a specific region; In seconds + $regionConfig->setDefaultLifetime(7200); // Default time to live; In seconds Cache Log @@ -672,7 +690,7 @@ Basic entity cache $em->clear(); // Clear entity manager - $country = $em->find('Country', 1); // Retrieve item from cache + $country1 = $em->find('Country', 1); // Retrieve item from cache $country->setName("New Name"); $em->persist($state); @@ -680,7 +698,8 @@ Basic entity cache $em->clear(); // Clear entity manager - $country = $em->find('Country', 1); // Retrieve item from cache + $country2 = $em->find('Country', 1); // Retrieve item from cache + // Notice that $country1 and $country2 are not the same instance. Association cache @@ -759,6 +778,8 @@ The query cache stores the results of the query but as identifiers, entity value ->setCacheable(true) ->getResult(); + $em->clear() + // 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) @@ -806,15 +827,16 @@ Using the last update timestamps as part of the query key invalidate the cache k $entities = $em->getRepository('Entity\Country')->findAll(); // load from query and entities from cache.. - $entities = $em->getRepository('Country')->findAll(); + $entities = $em->getRepository('Entity\Country')->findAll(); // update the timestamp cache region for Country $em->persist(new Country('zombieland')); $em->flush(); $em->clear(); - // Reload the query from database. - $entities = $em->getRepository('Country')->findAll(); + // Reload from database. + // At this point the query cache key if not logger valid, the select goes straight + $entities = $em->getRepository('Entity\Country')->findAll(); Cache API --------- @@ -843,8 +865,9 @@ Limitations Composite primary key ~~~~~~~~~~~~~~~~~~~~~ -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. +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. +For performance reasons the cache API does not extract from composite primary key. .. code-block:: php @@ -871,29 +894,19 @@ the cached entity should always be retrieved using the association identifier. // Supported /** @var $article Article */ - $article = $this->_em->find("Article", 1); + $article = $em->find('Article', 1); // Supported /** @var $article Article */ - $article = $this->_em->find("Article", $article); + $article = $em->find('Article', $article); // Supported $id = array('source' => 1, 'target' => 2); - $reference = $this->_em->find("Reference", $id); + $reference = $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. -By default, Doctrine provides a 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` + $reference = $em->find('Reference', $id); DELETE / UPDATE queries diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index d7ed6fd17..5d77b4579 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -137,19 +137,19 @@ abstract class AbstractQuery /** * Second level cache region name. * - * @var string + * @var string|null */ protected $cacheRegion; /** * Second level query cache mode. * - * @var integer + * @var integer|null */ protected $cacheMode; /** - * @var \Doctrine\ORM\Cache\Logging\CacheLogger + * @var \Doctrine\ORM\Cache\Logging\CacheLogger|null */ protected $cacheLogger; @@ -206,7 +206,7 @@ abstract class AbstractQuery */ public function setCacheRegion($cacheRegion) { - $this->cacheRegion = $cacheRegion; + $this->cacheRegion = (string) $cacheRegion; return $this; } @@ -245,7 +245,7 @@ abstract class AbstractQuery */ public function setLifetime($lifetime) { - $this->lifetime = $lifetime; + $this->lifetime = (integer) $lifetime; return $this; } @@ -264,7 +264,7 @@ abstract class AbstractQuery */ public function setCacheMode($cacheMode) { - $this->cacheMode = $cacheMode; + $this->cacheMode = (integer) $cacheMode; return $this; } diff --git a/lib/Doctrine/ORM/Cache.php b/lib/Doctrine/ORM/Cache.php index 6cd8b4814..37cc9a075 100644 --- a/lib/Doctrine/ORM/Cache.php +++ b/lib/Doctrine/ORM/Cache.php @@ -165,7 +165,7 @@ interface Cache /** * 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. + * @param string|null $regionName The cache name associated to the queries being cached. */ public function evictQueryRegion($regionName = null); @@ -179,9 +179,9 @@ interface Cache /** * 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. + * @param string|null $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/CacheConfiguration.php b/lib/Doctrine/ORM/Cache/CacheConfiguration.php index 42a1ba3ba..9d3654a04 100644 --- a/lib/Doctrine/ORM/Cache/CacheConfiguration.php +++ b/lib/Doctrine/ORM/Cache/CacheConfiguration.php @@ -35,22 +35,22 @@ use Doctrine\ORM\Cache\TimestampQueryCacheValidator; class CacheConfiguration { /** - * @var \Doctrine\ORM\Cache\CacheFactory + * @var \Doctrine\ORM\Cache\CacheFactory|null */ private $cacheFactory; /** - * @var \Doctrine\ORM\Cache\RegionsConfiguration + * @var \Doctrine\ORM\Cache\RegionsConfiguration|null */ private $regionsConfig; /** - * @var \Doctrine\ORM\Cache\Logging\CacheLogger + * @var \Doctrine\ORM\Cache\Logging\CacheLogger|null */ private $cacheLogger; /** - * @var \Doctrine\ORM\Cache\QueryCacheValidator + * @var \Doctrine\ORM\Cache\QueryCacheValidator|null */ private $queryValidator; diff --git a/lib/Doctrine/ORM/Cache/DefaultCache.php b/lib/Doctrine/ORM/Cache/DefaultCache.php index cd4c52ca0..78a22a80f 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultCache.php @@ -51,7 +51,7 @@ class DefaultCache implements Cache private $cacheFactory; /** - * @var array<\Doctrine\ORM\Cache\QueryCache> + * @var \Doctrine\ORM\Cache\QueryCache[] */ private $queryCaches = array(); diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 5781f3936..1add374eb 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -57,17 +57,17 @@ class DefaultCacheFactory implements CacheFactory private $regionsConfig; /** - * @var \Doctrine\ORM\Cache\TimestampRegion + * @var \Doctrine\ORM\Cache\TimestampRegion|null */ private $timestampRegion; /** - * @var array + * @var \Doctrine\ORM\Cache\Region[] */ - private $regions; + private $regions = array(); /** - * @var string + * @var string|null */ private $fileLockRegionDirectory; @@ -86,7 +86,7 @@ class DefaultCacheFactory implements CacheFactory */ public function setFileLockRegionDirectory($fileLockRegionDirectory) { - $this->fileLockRegionDirectory = $fileLockRegionDirectory; + $this->fileLockRegionDirectory = (string) $fileLockRegionDirectory; } /** @@ -200,7 +200,7 @@ class DefaultCacheFactory implements CacheFactory if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { if ( ! $this->fileLockRegionDirectory) { - throw new \RuntimeException( + throw new \LogicException( 'If you want 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(). ' ); diff --git a/lib/Doctrine/ORM/Cache/Region.php b/lib/Doctrine/ORM/Cache/Region.php index 16609400f..91879f45a 100644 --- a/lib/Doctrine/ORM/Cache/Region.php +++ b/lib/Doctrine/ORM/Cache/Region.php @@ -51,7 +51,7 @@ interface Region * * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to be retrieved. * - * @return \Doctrine\ORM\Cache\CacheEntry The cached entry or NULL + * @return \Doctrine\ORM\Cache\CacheEntry|null The cached entry or NULL * * @throws \Doctrine\ORM\Cache\CacheException Indicates a problem accessing the item or region. */ diff --git a/lib/Doctrine/ORM/Cache/RegionsConfiguration.php b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php index 1a847943b..0d060636c 100644 --- a/lib/Doctrine/ORM/Cache/RegionsConfiguration.php +++ b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php @@ -31,12 +31,12 @@ class RegionsConfiguration /** * @var array */ - private $lifetimes; + private $lifetimes = array(); /** * @var array */ - private $lockLifetimes; + private $lockLifetimes = array(); /** * @var integer @@ -54,8 +54,8 @@ class RegionsConfiguration */ public function __construct($defaultLifetime = 3600, $defaultLockLifetime = 60) { - $this->defaultLifetime = $defaultLifetime; - $this->defaultLockLifetime = $defaultLockLifetime; + $this->defaultLifetime = (integer) $defaultLifetime; + $this->defaultLockLifetime = (integer) $defaultLockLifetime; } /** @@ -71,7 +71,7 @@ class RegionsConfiguration */ public function setDefaultLifetime($defaultLifetime) { - $this->defaultLifetime = $defaultLifetime; + $this->defaultLifetime = (integer) $defaultLifetime; } /** @@ -87,7 +87,7 @@ class RegionsConfiguration */ public function setDefaultLockLifetime($defaultLockLifetime) { - $this->defaultLockLifetime = $defaultLockLifetime; + $this->defaultLockLifetime = (integer) $defaultLockLifetime; } /** diff --git a/lib/Doctrine/ORM/Cache/TimestampRegion.php b/lib/Doctrine/ORM/Cache/TimestampRegion.php index 8065a9411..9e0c25ca6 100644 --- a/lib/Doctrine/ORM/Cache/TimestampRegion.php +++ b/lib/Doctrine/ORM/Cache/TimestampRegion.php @@ -31,7 +31,7 @@ interface TimestampRegion extends Region /** * Update an specific key into the cache region. * - * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to lock. + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to update the timestamp. * * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. */ diff --git a/lib/Doctrine/ORM/Mapping/Cache.php b/lib/Doctrine/ORM/Mapping/Cache.php index 2bb8bb321..3226b6031 100644 --- a/lib/Doctrine/ORM/Mapping/Cache.php +++ b/lib/Doctrine/ORM/Mapping/Cache.php @@ -41,4 +41,4 @@ final class Cache implements Annotation * @var string Cache region name. */ public $region; -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 6337326d4..057c3857f 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -594,7 +594,7 @@ class ClassMetadataInfo implements ClassMetadata /** * @var array */ - public $cache; + public $cache = null; /** * The ReflectionClass instance of the mapped class. @@ -875,7 +875,7 @@ class ClassMetadataInfo implements ClassMetadata } if ($this->cache) { - $serialized[] = "cache"; + $serialized[] = 'cache'; } return $serialized; diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index c261723f7..2c8f1afcd 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -727,6 +727,8 @@ class XmlDriver extends FileDriver } /** + * Parse / Normalize the cache configuration + * * @param SimpleXMLElement $cacheMapping * * @return array diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index e5eac7e4f..a6880b018 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -732,6 +732,8 @@ class YamlDriver extends FileDriver } /** + * Parse / Normalize the cache configuration + * * @param array $cacheMapping * * @return array diff --git a/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php index 243f9b63e..f9e2616c6 100644 --- a/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php +++ b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php @@ -32,4 +32,4 @@ class CacheMetadataListener $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 index 702295cda..902f092e0 100644 --- a/tests/Doctrine/Tests/Mocks/CacheEntryMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php @@ -7,4 +7,4 @@ use Doctrine\ORM\Cache\CacheEntry; class CacheEntryMock extends \ArrayObject implements CacheEntry { -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/Mocks/CacheKeyMock.php b/tests/Doctrine/Tests/Mocks/CacheKeyMock.php index 078cd15eb..e427c2f41 100644 --- a/tests/Doctrine/Tests/Mocks/CacheKeyMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheKeyMock.php @@ -6,9 +6,8 @@ use Doctrine\ORM\Cache\CacheKey; class CacheKeyMock extends CacheKey { - function __construct($hash) { $this->hash = $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 index f597a5134..30cc3ca76 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -252,7 +252,7 @@ class DefaultCacheFactoryTest extends OrmTestCase } /** - * @expectedException RuntimeException + * @expectedException LogicException * @expectedExceptionMessage If you want 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() From 71903c28a8d19d792ca17728aa588694e5337a9b Mon Sep 17 00:00:00 2001 From: fabios Date: Wed, 23 Oct 2013 15:46:45 -0400 Subject: [PATCH 7/9] Region cache clear commands --- .../ORM/Cache/Region/DefaultRegion.php | 2 +- .../ClearCache/CollectionRegionCommand.php | 134 ++++++++++++++++++ .../ClearCache/EntityRegionCommand.php | 132 +++++++++++++++++ .../Command/ClearCache/QueryRegionCommand.php | 124 ++++++++++++++++ tests/Doctrine/Tests/Mocks/CacheEntryMock.php | 3 + tests/Doctrine/Tests/Mocks/CacheKeyMock.php | 8 ++ .../Doctrine/Tests/Mocks/CacheRegionMock.php | 42 +++++- .../Tests/Mocks/ConcurrentRegionMock.php | 54 ++++++- .../Tests/Mocks/TimestampRegionMock.php | 5 + .../ClearCacheCollectionRegionCommandTest.php | 94 ++++++++++++ .../ClearCacheEntityRegionCommandTest.php | 91 ++++++++++++ .../ClearCacheQueryRegionCommandTest.php | 90 ++++++++++++ tools/sandbox/bootstrap.php | 14 ++ tools/sandbox/doctrine.php | 3 + 14 files changed, 792 insertions(+), 4 deletions(-) create mode 100644 lib/Doctrine/ORM/Tools/Console/Command/ClearCache/CollectionRegionCommand.php create mode 100644 lib/Doctrine/ORM/Tools/Console/Command/ClearCache/EntityRegionCommand.php create mode 100644 lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryRegionCommand.php create mode 100644 tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheCollectionRegionCommandTest.php create mode 100644 tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheEntityRegionCommandTest.php create mode 100644 tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheQueryRegionCommandTest.php diff --git a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php index b4b5b1b1d..ab518ff65 100644 --- a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -72,7 +72,7 @@ class DefaultRegion implements Region } /** - * @return \Doctrine\Common\Cache\Cache + * @return \Doctrine\Common\Cache\CacheProvider */ public function getCache() { diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/CollectionRegionCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/CollectionRegionCommand.php new file mode 100644 index 000000000..80df37a64 --- /dev/null +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/CollectionRegionCommand.php @@ -0,0 +1,134 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\ClearCache; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Doctrine\ORM\Cache\Region\DefaultRegion; +use Doctrine\ORM\Cache; + +/** + * Command to clear a collection cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CollectionRegionCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('orm:clear-cache:region:collection') + ->setDescription('Clear a second-level cache collection region.') + ->addArgument('owner-class', InputArgument::OPTIONAL, 'The owner entity name.') + ->addArgument('association', InputArgument::OPTIONAL, 'The association collection name.') + ->addArgument('owner-id', InputArgument::OPTIONAL, 'The owner identifier.') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE,'If defined, all cache entries will be flushed.'); + + + $this->setHelp(<<%command.name% command is meant to clear a second-level cache collection regions for an associated Entity Manager. +It is possible to delete/invalidate all collection region, a specific collection region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for an collection region this command would do the work: + +%command.name% 'Entities\MyEntity' 'collectionName' + +To invalidate a specific entry you should use : + +%command.name% 'Entities\MyEntity' 'collectionName' 1 + +If you want to invalidate all entries for the all collection regions: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider for an collection region use this command: + +%command.name% 'Entities\MyEntity' 'collectionName' --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $em = $this->getHelper('em')->getEntityManager(); + $ownerClass = $input->getArgument('owner-class'); + $assoc = $input->getArgument('association'); + $ownerId = $input->getArgument('owner-id'); + $cache = $em->getCache(); + + if ( ! $cache instanceof Cache) { + throw new \InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if ( (! $ownerClass || ! $assoc) && ! $input->getOption('all')) { + throw new \InvalidArgumentException('Missing arguments "--owner-class" "--association"'); + } + + if ($input->getOption('flush')) { + $collectionRegion = $cache->getCollectionCacheRegion($ownerClass, $assoc); + + if ( ! $collectionRegion instanceof DefaultRegion) { + throw new \InvalidArgumentException(sprintf( + 'The option "--flush" expects a "Doctrine\ORM\Cache\Region\DefaultRegion", but got "%s".', + is_object($collectionRegion) ? get_class($collectionRegion) : gettype($collectionRegion) + )); + } + + $collectionRegion->getCache()->flushAll(); + + $output->writeln(sprintf('Flushing cache provider configured for "%s#%s"', $ownerClass, $assoc)); + + return; + } + + if ($input->getOption('all')) { + $output->writeln('Clearing all second-level cache collection regions'); + + $cache->evictEntityRegions(); + + return; + } + + if ($ownerId) { + $output->writeln(sprintf('Clearing second-level cache entry for collection "%s#%s" owner entity identified by "%s"', $ownerClass, $assoc, $ownerId)); + $cache->evictCollection($ownerClass, $assoc, $ownerId); + + return; + } + + $output->writeln(sprintf('Clearing second-level cache for collection "%s#%s"', $ownerClass, $assoc)); + $cache->evictCollectionRegion($ownerClass, $assoc); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/EntityRegionCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/EntityRegionCommand.php new file mode 100644 index 000000000..21f6e9de9 --- /dev/null +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/EntityRegionCommand.php @@ -0,0 +1,132 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\ClearCache; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Doctrine\ORM\Cache\Region\DefaultRegion; +use Doctrine\ORM\Cache; + +/** + * Command to clear a entity cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class EntityRegionCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('orm:clear-cache:region:entity') + ->setDescription('Clear a second-level cache entity region.') + ->addArgument('entity-class', InputArgument::OPTIONAL, 'The entity name.') + ->addArgument('entity-id', InputArgument::OPTIONAL, 'The entity identifier.') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE,'If defined, all cache entries will be flushed.'); + + + $this->setHelp(<<%command.name% command is meant to clear a second-level cache entity region for an associated Entity Manager. +It is possible to delete/invalidate all entity region, a specific entity region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for an entity region this command would do the work: + +%command.name% 'Entities\MyEntity' + +To invalidate a specific entry you should use : + +%command.name% 'Entities\MyEntity' 1 + +If you want to invalidate all entries for the all entity regions: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider for an entity region use this command: + +%command.name% 'Entities\MyEntity' --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $em = $this->getHelper('em')->getEntityManager(); + $entityClass = $input->getArgument('entity-class'); + $entityId = $input->getArgument('entity-id'); + $cache = $em->getCache(); + + if ( ! $cache instanceof Cache) { + throw new \InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if ( ! $entityClass && ! $input->getOption('all')) { + throw new \InvalidArgumentException('Invalid argument "--entity-class"'); + } + + if ($input->getOption('flush')) { + $entityRegion = $cache->getEntityCacheRegion($entityClass); + + if ( ! $entityRegion instanceof DefaultRegion) { + throw new \InvalidArgumentException(sprintf( + 'The option "--flush" expects a "Doctrine\ORM\Cache\Region\DefaultRegion", but got "%s".', + is_object($entityRegion) ? get_class($entityRegion) : gettype($entityRegion) + )); + } + + $entityRegion->getCache()->flushAll(); + + $output->writeln(sprintf('Flushing cache provider configured for entity named "%s"', $entityClass)); + + return; + } + + if ($input->getOption('all')) { + $output->writeln('Clearing all second-level cache entity regions'); + + $cache->evictEntityRegions(); + + return; + } + + if ($entityId) { + $output->writeln(sprintf('Clearing second-level cache entry for entity "%s" identified by "%s"', $entityClass, $entityId)); + $cache->evictEntity($entityClass, $entityId); + + return; + } + + $output->writeln(sprintf('Clearing second-level cache for entity "%s"', $entityClass)); + $cache->evictEntityRegion($entityClass); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryRegionCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryRegionCommand.php new file mode 100644 index 000000000..b5d75d3b3 --- /dev/null +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryRegionCommand.php @@ -0,0 +1,124 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\ClearCache; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Doctrine\ORM\Cache\Region\DefaultRegion; +use Doctrine\ORM\Cache; + +/** + * Command to clear a query cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class QueryRegionCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('orm:clear-cache:region:query') + ->setDescription('Clear a second-level cache query region.') + ->addArgument('region-name', InputArgument::OPTIONAL, 'The query region to clear.') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all query regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE,'If defined, all cache entries will be flushed.'); + + + $this->setHelp(<<%command.name% command is meant to clear a second-level cache query region for an associated Entity Manager. +It is possible to delete/invalidate all query region, a specific query region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for the default query region this command would do the work: + +%command.name% + +To invalidate entries for a specific query region you should use : + +%command.name% my_region_name + +If you want to invalidate all entries for the all query region: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider use this command: + +%command.name% my_region_name --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $em = $this->getHelper('em')->getEntityManager(); + $name = $input->getArgument('region-name'); + $cache = $em->getCache(); + + if ($name === null) { + $name = Cache::DEFAULT_QUERY_REGION_NAME; + } + + if ( ! $cache instanceof Cache) { + throw new \InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if ($input->getOption('flush')) { + $queryCache = $cache->getQueryCache($name); + $queryRegion = $queryCache->getRegion(); + + if ( ! $queryRegion instanceof DefaultRegion) { + throw new \InvalidArgumentException(sprintf( + 'The option "--flush" expects a "Doctrine\ORM\Cache\Region\DefaultRegion", but got "%s".', + is_object($queryRegion) ? get_class($queryRegion) : gettype($queryRegion) + )); + } + + $queryRegion->getCache()->flushAll(); + + $output->writeln(sprintf('Flushing cache provider configured for second-level cache query region named "%s"', $name)); + + return; + } + + if ($input->getOption('all')) { + $output->writeln('Clearing all second-level cache query regions'); + + $cache->evictQueryRegions(); + + return; + } + + $output->writeln(sprintf('Clearing second-level cache query region named "%s"', $name)); + $cache->evictQueryRegion($name); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Mocks/CacheEntryMock.php b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php index 902f092e0..7053df563 100644 --- a/tests/Doctrine/Tests/Mocks/CacheEntryMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php @@ -4,6 +4,9 @@ namespace Doctrine\Tests\Mocks; use Doctrine\ORM\Cache\CacheEntry; +/** + * Cache entry mock + */ class CacheEntryMock extends \ArrayObject implements CacheEntry { diff --git a/tests/Doctrine/Tests/Mocks/CacheKeyMock.php b/tests/Doctrine/Tests/Mocks/CacheKeyMock.php index e427c2f41..f87bc38b1 100644 --- a/tests/Doctrine/Tests/Mocks/CacheKeyMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheKeyMock.php @@ -4,8 +4,16 @@ namespace Doctrine\Tests\Mocks; use Doctrine\ORM\Cache\CacheKey; +/** + * Cache key mock + * + * Used to store/retrieve entries from a cache region + */ class CacheKeyMock extends CacheKey { + /** + * @param string $hash The string hash that represend this cache key + */ function __construct($hash) { $this->hash = $hash; diff --git a/tests/Doctrine/Tests/Mocks/CacheRegionMock.php b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php index a4ef52ca3..05366379e 100644 --- a/tests/Doctrine/Tests/Mocks/CacheRegionMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php @@ -7,18 +7,35 @@ use Doctrine\ORM\Cache\CacheKey; use Doctrine\ORM\Cache\Lock; use Doctrine\ORM\Cache\Region; +/** + * Cache region mock + */ class CacheRegionMock implements Region { public $calls = array(); public $returns = array(); public $name; - + + /** + * Queue a return value for a specific method invocation + * + * @param string $method + * @param mixed $value + */ public function addReturn($method, $value) { $this->returns[$method][] = $value; } - public function getReturn($method, $default) + /** + * Dequeue a value for a specific method invocation + * + * @param string $method + * @param mixed $default + * + * @return mixed + */ + private function getReturn($method, $default) { if (isset($this->returns[$method]) && ! empty($this->returns[$method])) { return array_shift($this->returns[$method]); @@ -27,6 +44,9 @@ class CacheRegionMock implements Region return $default; } + /** + * {@inheritdoc} + */ public function getName() { $this->calls[__FUNCTION__][] = array(); @@ -34,6 +54,9 @@ class CacheRegionMock implements Region return $this->name; } + /** + * {@inheritdoc} + */ public function contains(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -41,6 +64,9 @@ class CacheRegionMock implements Region return $this->getReturn(__FUNCTION__, false); } + /** + * {@inheritdoc} + */ public function evict(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -48,6 +74,9 @@ class CacheRegionMock implements Region return $this->getReturn(__FUNCTION__, true); } + /** + * {@inheritdoc} + */ public function evictAll() { $this->calls[__FUNCTION__][] = array(); @@ -55,6 +84,9 @@ class CacheRegionMock implements Region return $this->getReturn(__FUNCTION__, true); } + /** + * {@inheritdoc} + */ public function get(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -62,6 +94,9 @@ class CacheRegionMock implements Region return $this->getReturn(__FUNCTION__, null); } + /** + * {@inheritdoc} + */ public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) { $this->calls[__FUNCTION__][] = array('key' => $key, 'entry' => $entry); @@ -69,6 +104,9 @@ class CacheRegionMock implements Region return $this->getReturn(__FUNCTION__, true); } + /** + * {@inheritdoc} + */ public function clear() { $this->calls = array(); diff --git a/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php index e299f2a70..fc488b4b2 100644 --- a/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php +++ b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php @@ -10,22 +10,38 @@ use Doctrine\ORM\Cache\CacheKey; use Doctrine\ORM\Cache\Region; use Doctrine\ORM\Cache\Lock; +/** + * Concurrent region mock + * + * Used to mock a ConcurrentRegion + */ class ConcurrentRegionMock implements ConcurrentRegion { public $calls = array(); public $exceptions = array(); public $locks = array(); - + /** * @var \Doctrine\ORM\Cache\Region */ private $region; + /** + * @param \Doctrine\ORM\Cache\Region $region + */ public function __construct(Region $region) { $this->region = $region; } + /** + * Dequeue an exception for a specific method invocation + * + * @param string $method + * @param mixed $default + * + * @return mixed + */ private function throwException($method) { if (isset($this->exceptions[$method]) && ! empty($this->exceptions[$method])) { @@ -37,16 +53,31 @@ class ConcurrentRegionMock implements ConcurrentRegion } } + /** + * Queue an exception for the next method invocation + * + * @param string $method + * @param \Exception $e + */ public function addException($method, \Exception $e) { $this->exceptions[$method][] = $e; } + /** + * Locks a specific cache entry + * + * @param \Doctrine\ORM\Cache\CacheKey $key + * @param \Doctrine\ORM\Cache\Lock $lock + */ public function setLock(CacheKey $key, Lock $lock) { $this->locks[$key->hash] = $lock; } + /** + * {@inheritdoc} + */ public function contains(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -60,6 +91,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->contains($key); } + /** + * {@inheritdoc} + */ public function evict(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -69,6 +103,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->evict($key); } + /** + * {@inheritdoc} + */ public function evictAll() { $this->calls[__FUNCTION__][] = array(); @@ -78,6 +115,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->evictAll(); } + /** + * {@inheritdoc} + */ public function get(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -91,6 +131,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->get($key); } + /** + * {@inheritdoc} + */ public function getName() { $this->calls[__FUNCTION__][] = array(); @@ -100,6 +143,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->getName(); } + /** + * {@inheritdoc} + */ public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) { $this->calls[__FUNCTION__][] = array('key' => $key, 'entry' => $entry); @@ -118,6 +164,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->put($key, $entry); } + /** + * {@inheritdoc} + */ public function lock(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -131,6 +180,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->locks[$key->hash] = Lock::createLockRead(); } + /** + * {@inheritdoc} + */ public function unlock(CacheKey $key, Lock $lock) { $this->calls[__FUNCTION__][] = array('key' => $key, 'lock' => $lock); diff --git a/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php index ade513060..b55c7c5e3 100644 --- a/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php +++ b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php @@ -5,6 +5,11 @@ namespace Doctrine\Tests\Mocks; use Doctrine\ORM\Cache\TimestampRegion; use Doctrine\ORM\Cache\CacheKey; +/** + * Timestamp region mock + * + * Used to mock a TimestampRegion + */ class TimestampRegionMock extends CacheRegionMock implements TimestampRegion { public function update(CacheKey $key) diff --git a/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheCollectionRegionCommandTest.php b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheCollectionRegionCommandTest.php new file mode 100644 index 000000000..de1e1772c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheCollectionRegionCommandTest.php @@ -0,0 +1,94 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->application = new Application(); + $this->command = new CollectionRegionCommand(); + + $this->application->setHelperSet(new HelperSet(array( + 'em' => new EntityManagerHelper($this->_em) + ))); + + $this->application->add($this->command); + } + + public function testClearAllRegion() + { + $command = $this->application->find('orm:clear-cache:region:collection'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + '--all' => true, + )); + + $this->assertEquals('Clearing all second-level cache collection regions' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearByOwnerEntityClassName() + { + $command = $this->application->find('orm:clear-cache:region:collection'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'owner-class' => 'Doctrine\Tests\Models\Cache\State', + 'association' => 'cities', + )); + + $this->assertEquals('Clearing second-level cache for collection "Doctrine\Tests\Models\Cache\State#cities"' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearCacheEntryName() + { + $command = $this->application->find('orm:clear-cache:region:collection'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'owner-class' => 'Doctrine\Tests\Models\Cache\State', + 'association' => 'cities', + 'owner-id' => 1, + )); + + $this->assertEquals('Clearing second-level cache entry for collection "Doctrine\Tests\Models\Cache\State#cities" owner entity identified by "1"' . PHP_EOL, $tester->getDisplay()); + } + + public function testFlushRegionName() + { + $command = $this->application->find('orm:clear-cache:region:collection'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'owner-class' => 'Doctrine\Tests\Models\Cache\State', + 'association' => 'cities', + '--flush' => true, + )); + + $this->assertEquals('Flushing cache provider configured for "Doctrine\Tests\Models\Cache\State#cities"' . PHP_EOL, $tester->getDisplay()); + } +} diff --git a/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheEntityRegionCommandTest.php b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheEntityRegionCommandTest.php new file mode 100644 index 000000000..7e9e7c23a --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheEntityRegionCommandTest.php @@ -0,0 +1,91 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->application = new Application(); + $this->command = new EntityRegionCommand(); + + $this->application->setHelperSet(new HelperSet(array( + 'em' => new EntityManagerHelper($this->_em) + ))); + + $this->application->add($this->command); + } + + public function testClearAllRegion() + { + $command = $this->application->find('orm:clear-cache:region:entity'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + '--all' => true, + )); + + $this->assertEquals('Clearing all second-level cache entity regions' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearByEntityClassName() + { + $command = $this->application->find('orm:clear-cache:region:entity'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'entity-class' => 'Doctrine\Tests\Models\Cache\Country', + )); + + $this->assertEquals('Clearing second-level cache for entity "Doctrine\Tests\Models\Cache\Country"' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearCacheEntryName() + { + $command = $this->application->find('orm:clear-cache:region:entity'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'entity-class' => 'Doctrine\Tests\Models\Cache\Country', + 'entity-id' => 1, + )); + + $this->assertEquals('Clearing second-level cache entry for entity "Doctrine\Tests\Models\Cache\Country" identified by "1"' . PHP_EOL, $tester->getDisplay()); + } + + public function testFlushRegionName() + { + $command = $this->application->find('orm:clear-cache:region:entity'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'entity-class' => 'Doctrine\Tests\Models\Cache\Country', + '--flush' => true, + )); + + $this->assertEquals('Flushing cache provider configured for entity named "Doctrine\Tests\Models\Cache\Country"' . PHP_EOL, $tester->getDisplay()); + } +} diff --git a/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheQueryRegionCommandTest.php b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheQueryRegionCommandTest.php new file mode 100644 index 000000000..130bec8db --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheQueryRegionCommandTest.php @@ -0,0 +1,90 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->application = new Application(); + $this->command = new QueryRegionCommand(); + + $this->application->setHelperSet(new HelperSet(array( + 'em' => new EntityManagerHelper($this->_em) + ))); + + $this->application->add($this->command); + } + + public function testClearAllRegion() + { + $command = $this->application->find('orm:clear-cache:region:query'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + '--all' => true, + )); + + $this->assertEquals('Clearing all second-level cache query regions' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearDefaultRegionName() + { + $command = $this->application->find('orm:clear-cache:region:query'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'region-name' => null, + )); + + $this->assertEquals('Clearing second-level cache query region named "query_cache_region"' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearByRegionName() + { + $command = $this->application->find('orm:clear-cache:region:query'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'region-name' => 'my_region', + )); + + $this->assertEquals('Clearing second-level cache query region named "my_region"' . PHP_EOL, $tester->getDisplay()); + } + + public function testFlushRegionName() + { + $command = $this->application->find('orm:clear-cache:region:query'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'region-name' => 'my_region', + '--flush' => true, + )); + + $this->assertEquals('Flushing cache provider configured for second-level cache query region named "my_region"' . PHP_EOL, $tester->getDisplay()); + } +} diff --git a/tools/sandbox/bootstrap.php b/tools/sandbox/bootstrap.php index 5b97b9412..1e7d8ba00 100644 --- a/tools/sandbox/bootstrap.php +++ b/tools/sandbox/bootstrap.php @@ -38,4 +38,18 @@ $connectionOptions = array( 'path' => 'database.sqlite' ); +// Enable second-level cache +$cacheConfig = new \Doctrine\ORM\Cache\CacheConfiguration(); +$cacheDriver = $debug ? new Cache\ArrayCache : new Cache\ApcCache; +$cacheLogger = new \Doctrine\ORM\Cache\Logging\StatisticsCacheLogger(); +$factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($cacheConfig->getRegionsConfiguration(), $cacheDriver); + +if ($debug) { + $cacheConfig->setCacheLogger($cacheLogger); +} + +$cacheConfig->setCacheFactory($factory); +$config->setSecondLevelCacheEnabled(true); +$config->setSecondLevelCacheConfiguration($cacheConfig); + return EntityManager::create($connectionOptions, $config); \ No newline at end of file diff --git a/tools/sandbox/doctrine.php b/tools/sandbox/doctrine.php index 6cadae0d4..052968690 100644 --- a/tools/sandbox/doctrine.php +++ b/tools/sandbox/doctrine.php @@ -16,6 +16,9 @@ $cli->addCommands(array( new \Doctrine\DBAL\Tools\Console\Command\ImportCommand(), // ORM Commands + new \Doctrine\ORM\Tools\Console\Command\ClearCache\QueryRegionCommand(), + new \Doctrine\ORM\Tools\Console\Command\ClearCache\EntityRegionCommand(), + new \Doctrine\ORM\Tools\Console\Command\ClearCache\CollectionRegionCommand(), new \Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand(), new \Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand(), new \Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand(), From 0a66a2bc0933c2609ba2365c01d803695be194ca Mon Sep 17 00:00:00 2001 From: fabios Date: Mon, 2 Dec 2013 16:56:16 -0500 Subject: [PATCH 8/9] CS/Doc Fixes --- docs/en/reference/second-level-cache.rst | 335 +++--------------- lib/Doctrine/ORM/AbstractQuery.php | 10 +- lib/Doctrine/ORM/Cache/CacheKey.php | 4 +- .../ORM/Cache/CollectionCacheEntry.php | 12 +- lib/Doctrine/ORM/Cache/CollectionCacheKey.php | 16 +- .../ORM/Cache/DefaultCacheFactory.php | 15 +- .../ORM/Cache/DefaultEntityHydrator.php | 1 - lib/Doctrine/ORM/Cache/EntityCacheEntry.php | 14 +- lib/Doctrine/ORM/Cache/EntityCacheKey.php | 8 +- lib/Doctrine/ORM/Cache/QueryCacheEntry.php | 8 +- lib/Doctrine/ORM/Cache/QueryCacheKey.php | 10 +- .../ORM/Cache/Region/DefaultRegion.php | 4 +- .../ORM/Cache/TimestampCacheEntry.php | 10 +- lib/Doctrine/ORM/Cache/TimestampCacheKey.php | 2 +- .../ORM/Mapping/Driver/AnnotationDriver.php | 9 +- lib/Doctrine/ORM/Query.php | 2 +- .../ORM/Cache/DefaultCacheFactoryTest.php | 2 +- 17 files changed, 138 insertions(+), 324 deletions(-) diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst index ecf682b08..e0965b429 100644 --- a/docs/en/reference/second-level-cache.rst +++ b/docs/en/reference/second-level-cache.rst @@ -71,7 +71,7 @@ A query region might be something like : Cache Regions ------------- -``Doctrine\ORM\Cache\Region\DefaultRegion`` Its the default implementation. +``Doctrine\ORM\Cache\Region\DefaultRegion`` It's 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`` @@ -91,59 +91,7 @@ Defines a contract for accessing a particular region. Defines a contract for accessing a particular cache region. -.. code-block:: php - - `_. Concurrent cache region ~~~~~~~~~~~~~~~~~~~~~~~ @@ -157,29 +105,7 @@ If you want to use an ``READ_WRITE`` cache, you should consider providing your o Defines contract for concurrently managed data region. -.. code-block:: php - - `_. Cache region ~~~~~~~~~~~~ @@ -188,21 +114,7 @@ Cache region Tracks the timestamps of the most recent updates to particular entity. -.. code-block:: php - - `_. .. _reference-second-level-cache-mode: @@ -218,7 +130,7 @@ Caching mode * ``NONSTRICT_READ_WRITE`` - * Read Write Cache doesn’t employ any locks but can do reads, inserts , updates and deletes. + * Read Write Cache doesn’t employ any locks but can do reads, inserts, updates and deletes. * Good if the application needs to update data rarely. @@ -292,80 +204,7 @@ It allows you to provide a specific implementation of the following components : * ``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 - - `_. Region Lifetime ~~~~~~~~~~~~~~~ @@ -428,81 +267,7 @@ By providing a cache logger you should be able to get information about all cach If you want to get more information you should implement ``\Doctrine\ORM\Cache\Logging\CacheLogger``. and collect all information you want. - .. code-block:: php - - `_. Entity cache definition @@ -810,6 +575,47 @@ The Cache Mode controls how a particular query interacts with the second-level c The the default query cache mode is ```Cache::MODE_NORMAL``` +DELETE / UPDATE queries +~~~~~~~~~~~~~~~~~~~~~~~ + +DQL UPDATE / DELETE statements are ported directly into a database and bypass the second-level cache, +Entities that are already cached will NOT be invalidated. +However the cached data could be evicted using the cache API or an special query hint. + + +Execute the ``UPDATE`` and invalidate ``all cache entries`` using ``Query::HINT_CACHE_EVICT`` + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->setHint(Query::HINT_CACHE_EVICT, true) + ->execute(); + + +Execute the ``UPDATE`` and invalidate ``all cache entries`` using the cache API + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->execute(); + // Invoke Cache API + $em->getCache()->evictEntityRegion('Entity\Country'); + + +Execute the ``UPDATE`` and invalidate ``a specific cache entry`` using the cache API + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->execute(); + // Invoke Cache API + $em->getCache()->evictEntity('Entity\Country', 1); Using the repository query cache --------------------- @@ -908,45 +714,12 @@ For performance reasons the cache API does not extract from composite primary ke $id = array('source' => new Article(1), 'target' => new Article(2)); $reference = $em->find('Reference', $id); - -DELETE / UPDATE queries +Distribute environments ~~~~~~~~~~~~~~~~~~~~~~~ -DQL UPDATE / DELETE statements are ported directly into a database and bypass the second-level cache, -Entities that are already cached will NOT be invalidated. -However the cached data could be evicted using the cache API or an special query hint. +Some cache driver are not meant to be used in a distribute environment +Load-balancer for distributing workloads across multiple computing resources +should be used in conjunction with distributed caching system such as memcached, redis, riak ... - -Execute the ``UPDATE`` and invalidate ``all cache entries`` using ``Query::HINT_CACHE_EVICT`` - -.. code-block:: php - - _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") - ->setHint(Query::HINT_CACHE_EVICT, true) - ->execute(); - - -Execute the ``UPDATE`` and invalidate ``all cache entries`` using the cache API - -.. code-block:: php - - _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") - ->execute(); - // Invoke Cache API - $em->getCache()->evictEntityRegion('Entity\Country'); - - -Execute the ``UPDATE`` and invalidate ``a specific cache entry`` using the cache API - -.. code-block:: php - - _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") - ->execute(); - // Invoke Cache API - $em->getCache()->evictEntity('Entity\Country', 1); \ No newline at end of file +Caches should be used with care when using a load-balancer if you don't share the cache. +While using APC or any file based cache update occurred in a specific machine would not reflect to the cache in other machines. diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 5d77b4579..3ac4cf840 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -346,9 +346,7 @@ abstract class AbstractQuery $parameterCollection = new ArrayCollection(); foreach ($parameters as $key => $value) { - $parameter = new Parameter($key, $value); - - $parameterCollection->add($parameter); + $parameterCollection->add(new Parameter($key, $value)); } $parameters = $parameterCollection; @@ -387,9 +385,7 @@ abstract class AbstractQuery return $this; } - $parameter = new Parameter($key, $value, $type); - - $this->parameters->add($parameter); + $this->parameters->add(new Parameter($key, $value, $type)); return $this; } @@ -453,7 +449,7 @@ abstract class AbstractQuery * * @return \Doctrine\ORM\Query\ResultSetMapping */ - public function getResultSetMapping() + protected function getResultSetMapping() { return $this->_resultSetMapping; } diff --git a/lib/Doctrine/ORM/Cache/CacheKey.php b/lib/Doctrine/ORM/Cache/CacheKey.php index ede203091..1641c9900 100644 --- a/lib/Doctrine/ORM/Cache/CacheKey.php +++ b/lib/Doctrine/ORM/Cache/CacheKey.php @@ -21,7 +21,7 @@ namespace Doctrine\ORM\Cache; /** - * Defines entity / collection key to be stored in the cache region. + * Defines entity / collection / query key to be stored in the cache region. * Allows multiple roles to be stored in the same cache region. * * @since 2.5 @@ -30,6 +30,8 @@ namespace Doctrine\ORM\Cache; abstract class CacheKey { /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * * @var string Unique identifier */ public $hash; diff --git a/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php b/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php index 4bb329a7e..58c8757e0 100644 --- a/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php +++ b/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php @@ -29,12 +29,14 @@ namespace Doctrine\ORM\Cache; class CollectionCacheEntry implements CacheEntry { /** - * @var array + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array The list of entity identifiers hold by the collection */ public $identifiers; /** - * @param array $identifiers + * @param array $identifiers List of entity identifiers hold by the collection */ public function __construct(array $identifiers) { @@ -42,7 +44,11 @@ class CollectionCacheEntry implements CacheEntry } /** - * @param array $values + * Creates a new CollectionCacheEntry + * + * This method allow Doctrine\Common\Cache\PhpFileCache compatibility + * + * @param array $values array containing property values */ public static function __set_state(array $values) { diff --git a/lib/Doctrine/ORM/Cache/CollectionCacheKey.php b/lib/Doctrine/ORM/Cache/CollectionCacheKey.php index 184fa9bf2..6b6314550 100644 --- a/lib/Doctrine/ORM/Cache/CollectionCacheKey.php +++ b/lib/Doctrine/ORM/Cache/CollectionCacheKey.php @@ -29,17 +29,23 @@ namespace Doctrine\ORM\Cache; class CollectionCacheKey extends CacheKey { /** - * @var array + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array The owner entity identifier */ public $ownerIdentifier; /** - * @var string + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The owner entity class */ public $entityClass; /** - * @var string + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The association name */ public $association; @@ -52,9 +58,9 @@ class CollectionCacheKey extends CacheKey { ksort($ownerIdentifier); - $this->entityClass = $entityClass; - $this->association = $association; $this->ownerIdentifier = $ownerIdentifier; + $this->entityClass = (string) $entityClass; + $this->association = (string) $association; $this->hash = str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association; } } diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 1add374eb..92ec80c5c 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -164,10 +164,15 @@ class DefaultCacheFactory implements CacheFactory */ 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 - ))); + return new DefaultQueryCache( + $em, + $this->getRegion( + array( + 'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME, + 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE + ) + ) + ); } /** @@ -201,7 +206,7 @@ class DefaultCacheFactory implements CacheFactory if ( ! $this->fileLockRegionDirectory) { throw new \LogicException( - 'If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' . + 'If you what 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(). ' ); } diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index f022ae59f..57a28e9ab 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -123,7 +123,6 @@ class DefaultEntityHydrator implements EntityHydrator } foreach ($metadata->associationMappings as $name => $assoc) { - if ( ! isset($assoc['cache']) || ! isset($data[$name])) { continue; } diff --git a/lib/Doctrine/ORM/Cache/EntityCacheEntry.php b/lib/Doctrine/ORM/Cache/EntityCacheEntry.php index 73ce222b3..03d3c1f24 100644 --- a/lib/Doctrine/ORM/Cache/EntityCacheEntry.php +++ b/lib/Doctrine/ORM/Cache/EntityCacheEntry.php @@ -29,12 +29,16 @@ namespace Doctrine\ORM\Cache; class EntityCacheEntry implements CacheEntry { /** - * @var array + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array The entity map data */ public $data; /** - * @var string + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The entity class name */ public $class; @@ -49,7 +53,11 @@ class EntityCacheEntry implements CacheEntry } /** - * @param array $values + * Creates a new EntityCacheEntry + * + * This method allow Doctrine\Common\Cache\PhpFileCache compatibility + * + * @param array $values array containing property values */ public static function __set_state(array $values) { diff --git a/lib/Doctrine/ORM/Cache/EntityCacheKey.php b/lib/Doctrine/ORM/Cache/EntityCacheKey.php index 7729691a6..281e610fa 100644 --- a/lib/Doctrine/ORM/Cache/EntityCacheKey.php +++ b/lib/Doctrine/ORM/Cache/EntityCacheKey.php @@ -29,12 +29,16 @@ namespace Doctrine\ORM\Cache; class EntityCacheKey extends CacheKey { /** - * @var array + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array The entity identifier */ public $identifier; /** - * @var string + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The entity class name */ public $entityClass; diff --git a/lib/Doctrine/ORM/Cache/QueryCacheEntry.php b/lib/Doctrine/ORM/Cache/QueryCacheEntry.php index 46e0d603f..1ba61bcf0 100644 --- a/lib/Doctrine/ORM/Cache/QueryCacheEntry.php +++ b/lib/Doctrine/ORM/Cache/QueryCacheEntry.php @@ -29,12 +29,16 @@ namespace Doctrine\ORM\Cache; class QueryCacheEntry implements CacheEntry { /** - * @var array + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array List of entity identifiers */ public $result; /** - * @var integer + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var integer Time creation of this cache entry */ public $time; diff --git a/lib/Doctrine/ORM/Cache/QueryCacheKey.php b/lib/Doctrine/ORM/Cache/QueryCacheKey.php index 7001cc332..9a7d2b7bc 100644 --- a/lib/Doctrine/ORM/Cache/QueryCacheKey.php +++ b/lib/Doctrine/ORM/Cache/QueryCacheKey.php @@ -23,7 +23,7 @@ namespace Doctrine\ORM\Cache; use Doctrine\ORM\Cache; /** - * A key that identifies a particular query. + * A cache key that identifies a particular query. * * @since 2.5 * @author Fabio B. Silva @@ -31,12 +31,16 @@ use Doctrine\ORM\Cache; class QueryCacheKey extends CacheKey { /** - * @var integer + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var integer Cache key lifetime */ public $lifetime; /** - * @var integer + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var integer Cache mode (Doctrine\ORM\Cache::MODE_*) */ public $cacheMode; diff --git a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php index ab518ff65..42e1a3471 100644 --- a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -56,9 +56,9 @@ class DefaultRegion implements Region */ public function __construct($name, CacheProvider $cache, $lifetime = 0) { - $this->name = $name; $this->cache = $cache; - $this->lifetime = $lifetime; + $this->name = (string) $name; + $this->lifetime = (integer) $lifetime; $this->cache->setNamespace($this->name); } diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php index f7617b3bb..9d15c84b3 100644 --- a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php +++ b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php @@ -29,6 +29,8 @@ namespace Doctrine\ORM\Cache; class TimestampCacheEntry implements CacheEntry { /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * * @var float */ public $time; @@ -38,11 +40,15 @@ class TimestampCacheEntry implements CacheEntry */ public function __construct($time = null) { - $this->time = $time ?: microtime(true); + $this->time = $time ? (float)$time : microtime(true); } /** - * @param array $values + * Creates a new TimestampCacheEntry + * + * This method allow Doctrine\Common\Cache\PhpFileCache compatibility + * + * @param array $values array containing property values */ public static function __set_state(array $values) { diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheKey.php b/lib/Doctrine/ORM/Cache/TimestampCacheKey.php index 2ae65d065..dfa72274b 100644 --- a/lib/Doctrine/ORM/Cache/TimestampCacheKey.php +++ b/lib/Doctrine/ORM/Cache/TimestampCacheKey.php @@ -33,6 +33,6 @@ class TimestampCacheKey extends CacheKey */ public function __construct($space) { - $this->hash = $space; + $this->hash = (string) $space; } } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 359f26322..a0f99ad9b 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -131,11 +131,12 @@ class AnnotationDriver extends AbstractAnnotationDriver // Evaluate @Cache annotation if (isset($classAnnotations['Doctrine\ORM\Mapping\Cache'])) { $cacheAnnot = $classAnnotations['Doctrine\ORM\Mapping\Cache']; + $cacheMap = array( + 'region' => $cacheAnnot->region, + 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage), + ); - $metadata->enableCache(array( - 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage), - 'region' => $cacheAnnot->region, - )); + $metadata->enableCache($cacheMap); } // Evaluate NamedNativeQueries annotation diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 7be9dbd5b..71ddb6a3c 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -215,7 +215,7 @@ final class Query extends AbstractQuery /** * {@inheritdoc} */ - public function getResultSetMapping() + protected function getResultSetMapping() { // parse query or load from cache if ($this->_resultSetMapping === null) { diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php index 30cc3ca76..5e72fd0d6 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -253,7 +253,7 @@ class DefaultCacheFactoryTest extends OrmTestCase /** * @expectedException LogicException - * @expectedExceptionMessage If you want 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 + * @expectedExceptionMessage If you what 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() { From 4e0e1b80610f50e12a1564d2fb5fb7754c7c2558 Mon Sep 17 00:00:00 2001 From: fabios Date: Mon, 16 Dec 2013 15:55:54 -0500 Subject: [PATCH 9/9] CS/Typo Fixes --- lib/Doctrine/ORM/Cache/DefaultCacheFactory.php | 4 ++-- lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php | 2 +- .../Cache/Persister/AbstractCollectionPersister.php | 8 ++++---- .../NonStrictReadWriteCachedCollectionPersister.php | 2 +- .../Persister/ReadWriteCachedCollectionPersister.php | 8 ++++---- lib/Doctrine/ORM/Cache/QueryCache.php | 2 +- lib/Doctrine/ORM/Cache/QueryCacheValidator.php | 2 +- lib/Doctrine/ORM/Cache/Region/FileLockRegion.php | 10 +++++----- lib/Doctrine/ORM/EntityManagerInterface.php | 2 +- lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php | 3 ++- .../Tests/ORM/Cache/DefaultCacheFactoryTest.php | 2 +- tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php | 2 +- .../Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php | 4 ++-- .../Persister/AbstractCollectionPersisterTest.php | 2 +- .../Persister/ReadWriteCachedEntityPersisterTest.php | 2 +- 15 files changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 92ec80c5c..118fd30b1 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -102,7 +102,7 @@ class DefaultCacheFactory implements CacheFactory */ public function setRegion(Region $region) { - $this->regions[$region->getName()] = $region; + $this->regions[$region->getName()] = $region; } /** @@ -110,7 +110,7 @@ class DefaultCacheFactory implements CacheFactory */ public function setTimestampRegion(TimestampRegion $region) { - $this->timestampRegion = $region; + $this->timestampRegion = $region; } /** diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index 57a28e9ab..0420ed562 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -94,7 +94,7 @@ class DefaultEntityHydrator implements EntityHydrator : $data[$name]; // @TODO - fix it ! - // hande UnitOfWork#createEntity hash generation + // handle UnitOfWork#createEntity hash generation if ( ! is_array($targetId)) { $data[reset($assoc['joinColumnFieldNames'])] = $targetId; diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php index baacbd4c2..41bf1d914 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php @@ -90,10 +90,10 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister 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. + * @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 $association The association mapping. */ public function __construct(CollectionPersister $persister, Region $region, EntityManagerInterface $em, array $association) { diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php index 6f7d75596..8c682762a 100644 --- a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php @@ -85,7 +85,7 @@ class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPers $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); - // Invalidate non initialized collections OR odered collection + // Invalidate non initialized collections OR ordered collection if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) { $this->persister->update($collection); diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php index 967ccf69a..be11531d9 100644 --- a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php @@ -34,10 +34,10 @@ use Doctrine\ORM\PersistentCollection; class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister { /** - * @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. + * @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 $association The association mapping. */ public function __construct(CollectionPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, array $association) { diff --git a/lib/Doctrine/ORM/Cache/QueryCache.php b/lib/Doctrine/ORM/Cache/QueryCache.php index 5eb202281..dd5ef3bf5 100644 --- a/lib/Doctrine/ORM/Cache/QueryCache.php +++ b/lib/Doctrine/ORM/Cache/QueryCache.php @@ -51,7 +51,7 @@ interface QueryCache * @param \Doctrine\ORM\Query\ResultSetMapping $rsm * @param array $hints * - * @return void + * @return array|null */ public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = array()); diff --git a/lib/Doctrine/ORM/Cache/QueryCacheValidator.php b/lib/Doctrine/ORM/Cache/QueryCacheValidator.php index b4dfa79aa..dba7b0abe 100644 --- a/lib/Doctrine/ORM/Cache/QueryCacheValidator.php +++ b/lib/Doctrine/ORM/Cache/QueryCacheValidator.php @@ -33,7 +33,7 @@ interface QueryCacheValidator /** * Checks if the query entry is valid * - * @param \Doctrine\ORM\Cache\QueryCacheEntry $key + * @param \Doctrine\ORM\Cache\QueryCacheKey $key * @param \Doctrine\ORM\Cache\QueryCacheEntry $entry * * @return boolean diff --git a/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php index 956e7634f..7e20d5f73 100644 --- a/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php @@ -29,8 +29,8 @@ use Doctrine\ORM\Cache\ConcurrentRegion; /** * Very naive concurrent region, based on file locks. * - * since 2.5 - * author Fabio B. Silva + * @since 2.5 + * @author Fabio B. Silva */ class FileLockRegion implements ConcurrentRegion { @@ -42,7 +42,7 @@ class FileLockRegion implements ConcurrentRegion private $region; /** - * var string + * @var string */ private $directory; @@ -74,8 +74,8 @@ class FileLockRegion implements ConcurrentRegion } /** - * param \Doctrine\ORM\Cache\CacheKey $key - * param \Doctrine\ORM\Cache\Lock $lock + * @param \Doctrine\ORM\Cache\CacheKey $key + * @param \Doctrine\ORM\Cache\Lock $lock * * @return boolean */ diff --git a/lib/Doctrine/ORM/EntityManagerInterface.php b/lib/Doctrine/ORM/EntityManagerInterface.php index fa8ec902a..280ffb3c4 100644 --- a/lib/Doctrine/ORM/EntityManagerInterface.php +++ b/lib/Doctrine/ORM/EntityManagerInterface.php @@ -31,7 +31,7 @@ use Doctrine\ORM\Query\ResultSetMapping; interface EntityManagerInterface extends ObjectManager { /** - * Returns the cache API for managing the second level cache regions or NULL if the cache is not anabled. + * Returns the cache API for managing the second level cache regions or NULL if the cache is not enabled. * * @return \Doctrine\ORM\Cache|null */ diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 057c3857f..20cf4c2a9 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -1021,7 +1021,8 @@ class ClassMetadataInfo implements ClassMetadata } /** - * @param array $cache + * @param string $fieldName + * @param array $cache * * @return void */ diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php index 5e72fd0d6..9f98a2eaf 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -44,7 +44,7 @@ class DefaultCacheFactoryTest extends OrmTestCase ), $arguments); } - public function testInplementsCacheFactory() + public function testImplementsCacheFactory() { $this->assertInstanceOf('Doctrine\ORM\Cache\CacheFactory', $this->factory); } diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php index eda38cfe7..c33d0839b 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php @@ -245,7 +245,7 @@ class DefaultCacheTest extends OrmTestCase $this->assertSame($fooQueryCache, $this->cache->getQueryCache('foo')); } - public function testToIdentifierArrayShoudLookupForEntityIdentifier() + public function testToIdentifierArrayShouldLookupForEntityIdentifier() { $identifier = 123; $entity = new Country('Foo'); diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php index dfe385813..3af1a3bc4 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -216,7 +216,7 @@ class DefaultQueryCacheTest extends OrmTestCase $this->assertCount(13, $this->region->calls['put']); } - public function testgGetBasicQueryResult() + public function testGetBasicQueryResult() { $rsm = new ResultSetMappingBuilder($this->em); $key = new QueryCacheKey('query.key1', 0); @@ -480,7 +480,7 @@ class DefaultQueryCacheTest extends OrmTestCase * @expectedException Doctrine\ORM\Cache\CacheException * @expectedExceptionMessage Second level cache does not support multiple root entities. */ - public function testSuportMultipleRootEntitiesException() + public function testSupportMultipleRootEntitiesException() { $result = array(); $key = new QueryCacheKey('query.key1', 0); diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php index d0f76deb7..c43fafe3e 100644 --- a/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php @@ -199,7 +199,7 @@ abstract class AbstractCollectionPersisterTest extends OrmTestCase $this->assertEquals(0, $persister->count($collection)); } - public function testInvokEslice() + public function testInvokeSlice() { $entity = new State("Foo"); $persister = $this->createPersisterDefault(); diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php index 1b6ed6183..b95ddaef7 100644 --- a/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php @@ -153,7 +153,7 @@ class ReadWriteCachedEntityPersisterTest extends AbstractEntityPersisterTest $this->assertCount(0, $property->getValue($persister)); } - public function testTransactionlCommitShouldClearQueue() + public function testTransactionCommitShouldClearQueue() { $entity = new Country("Foo"); $lock = Lock::createLockRead();