diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 12fb3f560..9a5ded2c7 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -20,6 +20,7 @@ namespace Doctrine\ORM; use Doctrine\DBAL\LockMode; +use Doctrine\ORM\Query\Exec\AbstractSqlExecutor; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\ParserResult; use Doctrine\ORM\Query\QueryException; @@ -322,9 +323,27 @@ final class Query extends AbstractQuery list($sqlParams, $types) = $this->processParameterMappings($paramMappings); + $this->evictResultSetCache($executor, $sqlParams, $types); + return $executor->execute($this->_em->getConnection(), $sqlParams, $types); } + private function evictResultSetCache(AbstractSqlExecutor $executor, array $sqlParams, array $types) + { + if (null === $this->_queryCacheProfile || ! $this->getExpireResultCache()) { + return; + } + + $cacheDriver = $this->_queryCacheProfile->getResultCacheDriver(); + $statements = (array) $executor->getSqlStatements(); // Type casted since it can either be a string or an array + + foreach ($statements as $statement) { + $cacheKeys = $this->_queryCacheProfile->generateCacheKeys($statement, $sqlParams, $types); + + $cacheDriver->delete(reset($cacheKeys)); + } + } + /** * Evict entity cache region */ diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH2947Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH2947Test.php new file mode 100644 index 000000000..0b21b57f0 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH2947Test.php @@ -0,0 +1,95 @@ +resultCacheImpl = new ArrayCache(); + + parent::setUp(); + + $this->_schemaTool->createSchema([$this->_em->getClassMetadata(GH2947Car::class)]); + } + + public function testIssue() + { + $this->createData(); + $initialQueryCount = $this->getCurrentQueryCount(); + + $query = $this->createQuery(); + self::assertEquals('BMW', (string) $query->getSingleResult()); + self::assertEquals($initialQueryCount + 1, $this->getCurrentQueryCount()); + + $this->updateData(); + self::assertEquals('BMW', (string) $query->getSingleResult()); + self::assertEquals($initialQueryCount + 2, $this->getCurrentQueryCount()); + + $query->expireResultCache(true); + self::assertEquals('Dacia', (string) $query->getSingleResult()); + self::assertEquals($initialQueryCount + 3, $this->getCurrentQueryCount()); + + $query->expireResultCache(false); + self::assertEquals('Dacia', (string) $query->getSingleResult()); + self::assertEquals($initialQueryCount + 3, $this->getCurrentQueryCount()); + } + + private function createQuery() + { + return $this->_em->createQueryBuilder() + ->select('car') + ->from(GH2947Car::class, 'car') + ->getQuery() + ->useResultCache(true, 3600, 'foo-cache-id'); + } + + private function createData() + { + $this->_em->persist(new GH2947Car('BMW')); + $this->_em->flush(); + $this->_em->clear(); + } + + private function updateData() + { + $this->_em->createQueryBuilder() + ->update(GH2947Car::class, 'car') + ->set('car.brand', ':newBrand') + ->where('car.brand = :oldBrand') + ->setParameter('newBrand', 'Dacia') + ->setParameter('oldBrand', 'BMW') + ->getQuery() + ->execute(); + } +} + +/** + * @Entity + * @Table(name="GH2947_car") + */ +class GH2947Car +{ + /** + * @Id + * @Column(type="string", length=25) + * @GeneratedValue(strategy="NONE") + */ + public $brand; + + public function __construct(string $brand) + { + $this->brand = $brand; + } + + public function __toString(): string + { + return $this->brand; + } +} diff --git a/tests/Doctrine/Tests/ORM/Query/QueryTest.php b/tests/Doctrine/Tests/ORM/Query/QueryTest.php index 9b8f24d40..6b0091e9c 100644 --- a/tests/Doctrine/Tests/ORM/Query/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Query/QueryTest.php @@ -243,4 +243,37 @@ class QueryTest extends OrmTestCase $query->setHydrationCacheProfile(null); $this->assertNull($query->getHydrationCacheProfile()); } + + /** + * @group 2947 + */ + public function testResultCacheEviction() + { + $this->_em->getConfiguration()->setResultCacheImpl(new ArrayCache()); + + $query = $this->_em->createQuery("SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u") + ->useResultCache(true); + + /** @var DriverConnectionMock $driverConnectionMock */ + $driverConnectionMock = $this->_em->getConnection() + ->getWrappedConnection(); + + $driverConnectionMock->setStatementMock(new StatementArrayMock([['id_0' => 1]])); + + // Performs the query and sets up the initial cache + self::assertCount(1, $query->getResult()); + + $driverConnectionMock->setStatementMock(new StatementArrayMock([['id_0' => 1], ['id_0' => 2]])); + + // Retrieves cached data since expire flag is false and we have a cached result set + self::assertCount(1, $query->getResult()); + + // Performs the query and caches the result set since expire flag is true + self::assertCount(2, $query->expireResultCache(true)->getResult()); + + $driverConnectionMock->setStatementMock(new StatementArrayMock([['id_0' => 1]])); + + // Retrieves cached data since expire flag is false and we have a cached result set + self::assertCount(2, $query->expireResultCache(false)->getResult()); + } } diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index 2ff2c347c..5716f2a50 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -68,6 +68,13 @@ abstract class OrmFunctionalTestCase extends OrmTestCase */ protected $_usedModelSets = []; + /** + * To be configured by the test that uses result set cache + * + * @var \Doctrine\Common\Cache\Cache|null + */ + protected $resultCacheImpl; + /** * Whether the database schema has already been created. * @@ -699,6 +706,10 @@ abstract class OrmFunctionalTestCase extends OrmTestCase $config->setProxyDir(__DIR__ . '/Proxies'); $config->setProxyNamespace('Doctrine\Tests\Proxies'); + if (null !== $this->resultCacheImpl) { + $config->setResultCacheImpl($this->resultCacheImpl); + } + $enableSecondLevelCache = getenv('ENABLE_SECOND_LEVEL_CACHE'); if ($this->isSecondLevelCacheEnabled || $enableSecondLevelCache) {