From b0766d60f976ebaaf72e8e1d2fff52c6987eee77 Mon Sep 17 00:00:00 2001 From: Dmitry Bannik Date: Fri, 17 Jan 2025 09:00:32 +0300 Subject: [PATCH] invalidate cache when sql filter changes (#11783) --- src/Cache/CollectionCacheKey.php | 4 ++-- src/Cache/DefaultCache.php | 2 +- .../AbstractCollectionPersister.php | 10 ++++++++-- ...rictReadWriteCachedCollectionPersister.php | 4 ++-- .../ReadWriteCachedCollectionPersister.php | 4 ++-- .../Entity/AbstractEntityPersister.php | 16 ++++++++++----- tests/Tests/ORM/Cache/CacheKeyTest.php | 16 +++++++-------- .../Tests/ORM/Cache/CacheLoggerChainTest.php | 2 +- tests/Tests/ORM/Cache/DefaultCacheTest.php | 2 +- .../Cache/DefaultCollectionHydratorTest.php | 2 +- ...ReadWriteCachedCollectionPersisterTest.php | 20 +++++++++---------- .../ORM/Cache/StatisticsCacheLoggerTest.php | 4 ++-- .../SecondLevelCacheConcurrentTest.php | 2 +- 13 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/Cache/CollectionCacheKey.php b/src/Cache/CollectionCacheKey.php index 56bf8df722c..175a2ce4b60 100644 --- a/src/Cache/CollectionCacheKey.php +++ b/src/Cache/CollectionCacheKey.php @@ -43,7 +43,7 @@ class CollectionCacheKey extends CacheKey * @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) + public function __construct($entityClass, $association, array $ownerIdentifier, string $filterHash) { ksort($ownerIdentifier); @@ -51,6 +51,6 @@ public function __construct($entityClass, $association, array $ownerIdentifier) $this->entityClass = (string) $entityClass; $this->association = (string) $association; - parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association); + parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association . '_' . $filterHash); } } diff --git a/src/Cache/DefaultCache.php b/src/Cache/DefaultCache.php index d318dd007d8..6238a1a2f38 100644 --- a/src/Cache/DefaultCache.php +++ b/src/Cache/DefaultCache.php @@ -282,7 +282,7 @@ private function buildCollectionCacheKey( $ownerIdentifier = $this->toIdentifierArray($metadata, $ownerIdentifier); } - return new CollectionCacheKey($metadata->rootEntityName, $association, $ownerIdentifier); + return new CollectionCacheKey($metadata->rootEntityName, $association, $ownerIdentifier, ''); } /** diff --git a/src/Cache/Persister/Collection/AbstractCollectionPersister.php b/src/Cache/Persister/Collection/AbstractCollectionPersister.php index 42d6ec8519c..18d4d7dab0e 100644 --- a/src/Cache/Persister/Collection/AbstractCollectionPersister.php +++ b/src/Cache/Persister/Collection/AbstractCollectionPersister.php @@ -19,6 +19,7 @@ use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Persisters\Collection\CollectionPersister; use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; +use Doctrine\ORM\Query\FilterCollection; use Doctrine\ORM\UnitOfWork; use function array_values; @@ -55,6 +56,9 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister /** @var string */ protected $regionName; + /** @var FilterCollection */ + protected $filters; + /** @var CollectionHydrator */ protected $hydrator; @@ -76,6 +80,7 @@ public function __construct(CollectionPersister $persister, Region $region, Enti $this->region = $region; $this->persister = $persister; $this->association = $association; + $this->filters = $em->getFilters(); $this->regionName = $region->getName(); $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); @@ -189,7 +194,7 @@ public function containsKey(PersistentCollection $collection, $key) public function count(PersistentCollection $collection) { $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash()); $entry = $this->region->get($key); if ($entry !== null) { @@ -241,7 +246,8 @@ protected function evictCollectionCache(PersistentCollection $collection) $key = new CollectionCacheKey( $this->sourceEntity->rootEntityName, $this->association['fieldName'], - $this->uow->getEntityIdentifier($collection->getOwner()) + $this->uow->getEntityIdentifier($collection->getOwner()), + $this->filters->getHash() ); $this->region->evict($key); diff --git a/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php b/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php index 2441fc9960e..cfe62a2d143 100644 --- a/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php +++ b/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php @@ -45,7 +45,7 @@ public function afterTransactionRolledBack() public function delete(PersistentCollection $collection) { $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash()); $this->persister->delete($collection); @@ -65,7 +65,7 @@ public function update(PersistentCollection $collection) } $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash()); // Invalidate non initialized collections OR ordered collection if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) { diff --git a/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php b/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php index 0ec977695e2..35e7797b39b 100644 --- a/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php +++ b/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php @@ -68,7 +68,7 @@ public function afterTransactionRolledBack() public function delete(PersistentCollection $collection) { $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash()); $lock = $this->region->lock($key); $this->persister->delete($collection); @@ -98,7 +98,7 @@ public function update(PersistentCollection $collection) $this->persister->update($collection); $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash()); $lock = $this->region->lock($key); if ($lock === null) { diff --git a/src/Cache/Persister/Entity/AbstractEntityPersister.php b/src/Cache/Persister/Entity/AbstractEntityPersister.php index 3cdd27885a4..ced175e3c87 100644 --- a/src/Cache/Persister/Entity/AbstractEntityPersister.php +++ b/src/Cache/Persister/Entity/AbstractEntityPersister.php @@ -22,6 +22,7 @@ use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Persisters\Entity\EntityPersister; use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; +use Doctrine\ORM\Query\FilterCollection; use Doctrine\ORM\UnitOfWork; use function array_merge; @@ -62,6 +63,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister /** @var Cache */ protected $cache; + /** @var FilterCollection */ + protected $filters; + /** @var CacheLogger|null */ protected $cacheLogger; @@ -91,6 +95,7 @@ public function __construct(EntityPersister $persister, Region $region, EntityMa $this->region = $region; $this->persister = $persister; $this->cache = $em->getCache(); + $this->filters = $em->getFilters(); $this->regionName = $region->getName(); $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); @@ -261,7 +266,7 @@ protected function getHash($query, $criteria, ?array $orderBy = null, $limit = n ? $this->persister->expandCriteriaParameters($criteria) : $this->persister->expandParameters($criteria); - return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset); + return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset . $this->filters->getHash()); } /** @@ -524,7 +529,7 @@ public function loadManyToManyCollection(array $assoc, $sourceEntity, Persistent } $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = $this->buildCollectionCacheKey($assoc, $ownerId); + $key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash()); $list = $persister->loadCollectionCache($collection, $key); if ($list !== null) { @@ -559,7 +564,7 @@ public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentC } $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = $this->buildCollectionCacheKey($assoc, $ownerId); + $key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash()); $list = $persister->loadCollectionCache($collection, $key); if ($list !== null) { @@ -611,12 +616,13 @@ public function refresh(array $id, $entity, $lockMode = null) * * @return CollectionCacheKey */ - protected function buildCollectionCacheKey(array $association, $ownerId) + protected function buildCollectionCacheKey(array $association, $ownerId, string $filterHash) { return new CollectionCacheKey( $this->metadataFactory->getMetadataFor($association['sourceEntity'])->rootEntityName, $association['fieldName'], - $ownerId + $ownerId, + $filterHash ); } } diff --git a/tests/Tests/ORM/Cache/CacheKeyTest.php b/tests/Tests/ORM/Cache/CacheKeyTest.php index 4dc6c23d0c7..b3b1a3d7f1b 100644 --- a/tests/Tests/ORM/Cache/CacheKeyTest.php +++ b/tests/Tests/ORM/Cache/CacheKeyTest.php @@ -41,32 +41,32 @@ public function testEntityCacheKeyIdentifierOrder(): void public function testCollectionCacheKeyIdentifierType(): void { - $key1 = new CollectionCacheKey('Foo', 'assoc', ['id' => 1]); - $key2 = new CollectionCacheKey('Foo', 'assoc', ['id' => '1']); + $key1 = new CollectionCacheKey('Foo', 'assoc', ['id' => 1], ''); + $key2 = new CollectionCacheKey('Foo', 'assoc', ['id' => '1'], ''); self::assertEquals($key1->hash, $key2->hash); } public function testCollectionCacheKeyIdentifierOrder(): void { - $key1 = new CollectionCacheKey('Foo', 'assoc', ['foo_bar' => 1, 'bar_foo' => 2]); - $key2 = new CollectionCacheKey('Foo', 'assoc', ['bar_foo' => 2, 'foo_bar' => 1]); + $key1 = new CollectionCacheKey('Foo', 'assoc', ['foo_bar' => 1, 'bar_foo' => 2], ''); + $key2 = new CollectionCacheKey('Foo', 'assoc', ['bar_foo' => 2, 'foo_bar' => 1], ''); self::assertEquals($key1->hash, $key2->hash); } public function testCollectionCacheKeyIdentifierCollision(): void { - $key1 = new CollectionCacheKey('Foo', 'assoc', ['id' => 1]); - $key2 = new CollectionCacheKey('Bar', 'assoc', ['id' => 1]); + $key1 = new CollectionCacheKey('Foo', 'assoc', ['id' => 1], ''); + $key2 = new CollectionCacheKey('Bar', 'assoc', ['id' => 1], ''); self::assertNotEquals($key1->hash, $key2->hash); } public function testCollectionCacheKeyAssociationCollision(): void { - $key1 = new CollectionCacheKey('Foo', 'assoc1', ['id' => 1]); - $key2 = new CollectionCacheKey('Foo', 'assoc2', ['id' => 1]); + $key1 = new CollectionCacheKey('Foo', 'assoc1', ['id' => 1], ''); + $key2 = new CollectionCacheKey('Foo', 'assoc2', ['id' => 1], ''); self::assertNotEquals($key1->hash, $key2->hash); } diff --git a/tests/Tests/ORM/Cache/CacheLoggerChainTest.php b/tests/Tests/ORM/Cache/CacheLoggerChainTest.php index d2ddc7f4a11..18f47d67680 100644 --- a/tests/Tests/ORM/Cache/CacheLoggerChainTest.php +++ b/tests/Tests/ORM/Cache/CacheLoggerChainTest.php @@ -69,7 +69,7 @@ public function testEntityCacheChain(): void public function testCollectionCacheChain(): void { $name = 'my_collection_region'; - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $this->logger->setLogger('mock', $this->mock); diff --git a/tests/Tests/ORM/Cache/DefaultCacheTest.php b/tests/Tests/ORM/Cache/DefaultCacheTest.php index 389de9255c8..6df582cce89 100644 --- a/tests/Tests/ORM/Cache/DefaultCacheTest.php +++ b/tests/Tests/ORM/Cache/DefaultCacheTest.php @@ -52,7 +52,7 @@ private function putEntityCacheEntry(string $className, array $identifier, array private function putCollectionCacheEntry(string $className, string $association, array $ownerIdentifier, array $data): void { $metadata = $this->em->getClassMetadata($className); - $cacheKey = new CollectionCacheKey($metadata->name, $association, $ownerIdentifier); + $cacheKey = new CollectionCacheKey($metadata->name, $association, $ownerIdentifier, ''); $cacheEntry = new CollectionCacheEntry($data); $persister = $this->em->getUnitOfWork()->getCollectionPersister($metadata->getAssociationMapping($association)); diff --git a/tests/Tests/ORM/Cache/DefaultCollectionHydratorTest.php b/tests/Tests/ORM/Cache/DefaultCollectionHydratorTest.php index 339f1f8ffb5..52bab761aed 100644 --- a/tests/Tests/ORM/Cache/DefaultCollectionHydratorTest.php +++ b/tests/Tests/ORM/Cache/DefaultCollectionHydratorTest.php @@ -51,7 +51,7 @@ public function testLoadCacheCollection(): void $sourceClass = $this->_em->getClassMetadata(State::class); $targetClass = $this->_em->getClassMetadata(City::class); - $key = new CollectionCacheKey($sourceClass->name, 'cities', ['id' => 21]); + $key = new CollectionCacheKey($sourceClass->name, 'cities', ['id' => 21], ''); $collection = new PersistentCollection($this->_em, $targetClass, new ArrayCollection()); $list = $this->structure->loadCacheEntry($sourceClass, $key, $entry, $collection); diff --git a/tests/Tests/ORM/Cache/Persister/Collection/ReadWriteCachedCollectionPersisterTest.php b/tests/Tests/ORM/Cache/Persister/Collection/ReadWriteCachedCollectionPersisterTest.php index f06845db9f2..127897547f4 100644 --- a/tests/Tests/ORM/Cache/Persister/Collection/ReadWriteCachedCollectionPersisterTest.php +++ b/tests/Tests/ORM/Cache/Persister/Collection/ReadWriteCachedCollectionPersisterTest.php @@ -34,7 +34,7 @@ public function testDeleteShouldLockItem(): void $lock = Lock::createLockRead(); $persister = $this->createPersisterDefault(); $collection = $this->createCollection($entity); - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $this->region->expects(self::once()) ->method('lock') @@ -52,7 +52,7 @@ public function testUpdateShouldLockItem(): void $lock = Lock::createLockRead(); $persister = $this->createPersisterDefault(); $collection = $this->createCollection($entity); - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $this->region->expects(self::once()) ->method('lock') @@ -70,7 +70,7 @@ public function testUpdateTransactionRollBackShouldEvictItem(): void $lock = Lock::createLockRead(); $persister = $this->createPersisterDefault(); $collection = $this->createCollection($entity); - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $this->region->expects(self::once()) ->method('lock') @@ -94,7 +94,7 @@ public function testDeleteTransactionRollBackShouldEvictItem(): void $lock = Lock::createLockRead(); $persister = $this->createPersisterDefault(); $collection = $this->createCollection($entity); - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $this->region->expects(self::once()) ->method('lock') @@ -118,7 +118,7 @@ public function testTransactionRollBackDeleteShouldClearQueue(): void $lock = Lock::createLockRead(); $persister = $this->createPersisterDefault(); $collection = $this->createCollection($entity); - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $property = new ReflectionProperty(ReadWriteCachedCollectionPersister::class, 'queuedCache'); $property->setAccessible(true); @@ -150,7 +150,7 @@ public function testTransactionRollBackUpdateShouldClearQueue(): void $lock = Lock::createLockRead(); $persister = $this->createPersisterDefault(); $collection = $this->createCollection($entity); - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $property = new ReflectionProperty(ReadWriteCachedCollectionPersister::class, 'queuedCache'); $property->setAccessible(true); @@ -182,7 +182,7 @@ public function testTransactionRollCommitDeleteShouldClearQueue(): void $lock = Lock::createLockRead(); $persister = $this->createPersisterDefault(); $collection = $this->createCollection($entity); - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $property = new ReflectionProperty(ReadWriteCachedCollectionPersister::class, 'queuedCache'); $property->setAccessible(true); @@ -214,7 +214,7 @@ public function testTransactionRollCommitUpdateShouldClearQueue(): void $lock = Lock::createLockRead(); $persister = $this->createPersisterDefault(); $collection = $this->createCollection($entity); - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $property = new ReflectionProperty(ReadWriteCachedCollectionPersister::class, 'queuedCache'); $property->setAccessible(true); @@ -245,7 +245,7 @@ public function testDeleteLockFailureShouldIgnoreQueue(): void $entity = new State('Foo'); $persister = $this->createPersisterDefault(); $collection = $this->createCollection($entity); - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $property = new ReflectionProperty(ReadWriteCachedCollectionPersister::class, 'queuedCache'); $property->setAccessible(true); @@ -270,7 +270,7 @@ public function testUpdateLockFailureShouldIgnoreQueue(): void $entity = new State('Foo'); $persister = $this->createPersisterDefault(); $collection = $this->createCollection($entity); - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $property = new ReflectionProperty(ReadWriteCachedCollectionPersister::class, 'queuedCache'); $property->setAccessible(true); diff --git a/tests/Tests/ORM/Cache/StatisticsCacheLoggerTest.php b/tests/Tests/ORM/Cache/StatisticsCacheLoggerTest.php index 185069636bb..a349b0d06a7 100644 --- a/tests/Tests/ORM/Cache/StatisticsCacheLoggerTest.php +++ b/tests/Tests/ORM/Cache/StatisticsCacheLoggerTest.php @@ -44,7 +44,7 @@ public function testEntityCache(): void public function testCollectionCache(): void { $name = 'my_collection_region'; - $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $key = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $this->logger->collectionCacheHit($name, $key); $this->logger->collectionCachePut($name, $key); @@ -81,7 +81,7 @@ public function testMultipleCaches(): void $entityRegion = 'my_entity_region'; $queryRegion = 'my_query_region'; - $coolKey = new CollectionCacheKey(State::class, 'cities', ['id' => 1]); + $coolKey = new CollectionCacheKey(State::class, 'cities', ['id' => 1], ''); $entityKey = new EntityCacheKey(State::class, ['id' => 1]); $queryKey = new QueryCacheKey('my_query_hash'); diff --git a/tests/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php b/tests/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php index 3b315ece59a..06df8083308 100644 --- a/tests/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php +++ b/tests/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php @@ -99,7 +99,7 @@ public function testBasicConcurrentCollectionReadLock(): void $this->secondLevelCacheLogger->clearStats(); $stateId = $this->states[0]->getId(); - $cacheId = new CollectionCacheKey(State::class, 'cities', ['id' => $stateId]); + $cacheId = new CollectionCacheKey(State::class, 'cities', ['id' => $stateId], ''); $region = $this->_em->getCache()->getCollectionCacheRegion(State::class, 'cities'); assert($region instanceof ConcurrentRegionMock);