From 17cfa66a78e263f968e963f25b07e1e387d0ec5c Mon Sep 17 00:00:00 2001 From: Jaroslav Kuba Date: Wed, 27 Jul 2022 08:47:03 +0200 Subject: [PATCH] Added result caching for QueryBuilder --- docs/en/reference/query-builder.rst | 22 ++++++ src/Query/QueryBuilder.php | 56 +++++++++++--- tests/Query/QueryBuilderTest.php | 111 +++++++++++++++++++++++----- 3 files changed, 162 insertions(+), 27 deletions(-) diff --git a/docs/en/reference/query-builder.rst b/docs/en/reference/query-builder.rst index 7c33fc51587..b15a75f67ac 100644 --- a/docs/en/reference/query-builder.rst +++ b/docs/en/reference/query-builder.rst @@ -369,3 +369,25 @@ in your query as a return value: ->where('email = ' . $queryBuilder->createPositionalParameter($userInputEmail)) ; // SELECT id, name FROM users WHERE email = ? + +Caching +------- + +To use the result cache, it is necessary to call the method +``enableResultCache($cacheProfile)`` and pass a instance of +``Doctrine\DBAL\Cache\QueryCacheProfile`` with a cache lifetime +value in seconds. A cache key can optionally be added if needed. + +.. code-block:: php + + select('id', 'name') + ->from('users') + ->enableResultCache(new QueryCacheProfile(300, 'some-key')) + ; + +.. note:: + + See the :ref:`Caching ` section for more information. diff --git a/src/Query/QueryBuilder.php b/src/Query/QueryBuilder.php index 9d134e82009..b790cdeab46 100644 --- a/src/Query/QueryBuilder.php +++ b/src/Query/QueryBuilder.php @@ -2,6 +2,7 @@ namespace Doctrine\DBAL\Query; +use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception; use Doctrine\DBAL\ParameterType; @@ -139,6 +140,11 @@ class QueryBuilder */ private int $boundCounter = 0; + /** + * The query cache profile used for caching results. + */ + private ?QueryCacheProfile $resultCacheProfile = null; + /** * Initializes a new QueryBuilder. * @@ -227,7 +233,7 @@ public function getState() */ public function fetchAssociative() { - return $this->connection->fetchAssociative($this->getSQL(), $this->params, $this->paramTypes); + return $this->executeQuery()->fetchAssociative(); } /** @@ -240,7 +246,7 @@ public function fetchAssociative() */ public function fetchNumeric() { - return $this->connection->fetchNumeric($this->getSQL(), $this->params, $this->paramTypes); + return $this->executeQuery()->fetchNumeric(); } /** @@ -253,7 +259,7 @@ public function fetchNumeric() */ public function fetchOne() { - return $this->connection->fetchOne($this->getSQL(), $this->params, $this->paramTypes); + return $this->executeQuery()->fetchOne(); } /** @@ -265,7 +271,7 @@ public function fetchOne() */ public function fetchAllNumeric(): array { - return $this->connection->fetchAllNumeric($this->getSQL(), $this->params, $this->paramTypes); + return $this->executeQuery()->fetchAllNumeric(); } /** @@ -277,7 +283,7 @@ public function fetchAllNumeric(): array */ public function fetchAllAssociative(): array { - return $this->connection->fetchAllAssociative($this->getSQL(), $this->params, $this->paramTypes); + return $this->executeQuery()->fetchAllAssociative(); } /** @@ -290,7 +296,7 @@ public function fetchAllAssociative(): array */ public function fetchAllKeyValue(): array { - return $this->connection->fetchAllKeyValue($this->getSQL(), $this->params, $this->paramTypes); + return $this->executeQuery()->fetchAllKeyValue(); } /** @@ -304,7 +310,7 @@ public function fetchAllKeyValue(): array */ public function fetchAllAssociativeIndexed(): array { - return $this->connection->fetchAllAssociativeIndexed($this->getSQL(), $this->params, $this->paramTypes); + return $this->executeQuery()->fetchAllAssociativeIndexed(); } /** @@ -316,7 +322,7 @@ public function fetchAllAssociativeIndexed(): array */ public function fetchFirstColumn(): array { - return $this->connection->fetchFirstColumn($this->getSQL(), $this->params, $this->paramTypes); + return $this->executeQuery()->fetchFirstColumn(); } /** @@ -326,7 +332,12 @@ public function fetchFirstColumn(): array */ public function executeQuery(): Result { - return $this->connection->executeQuery($this->getSQL(), $this->params, $this->paramTypes); + return $this->connection->executeQuery( + $this->getSQL(), + $this->params, + $this->paramTypes, + $this->resultCacheProfile + ); } /** @@ -361,7 +372,7 @@ public function execute() 'QueryBuilder::execute() is deprecated, use QueryBuilder::executeQuery() for SQL queries instead.' ); - return $this->connection->executeQuery($this->getSQL(), $this->params, $this->paramTypes); + return $this->executeQuery(); } Deprecation::trigger( @@ -1588,4 +1599,29 @@ public function __clone() $this->params[$name] = clone $param; } } + + /** + * Enables caching of the results of this query, for given amount of seconds + * and optionally specified witch key to use for the cache entry. + * + * @return $this + */ + public function enableResultCache(QueryCacheProfile $cacheProfile): self + { + $this->resultCacheProfile = $cacheProfile; + + return $this; + } + + /** + * Disables caching of the results of this query. + * + * @return $this + */ + public function disableResultCache(): self + { + $this->resultCacheProfile = null; + + return $this; + } } diff --git a/tests/Query/QueryBuilderTest.php b/tests/Query/QueryBuilderTest.php index 9c8c2be2e34..f73c3e92b03 100644 --- a/tests/Query/QueryBuilderTest.php +++ b/tests/Query/QueryBuilderTest.php @@ -2,6 +2,7 @@ namespace Doctrine\DBAL\Tests\Query; +use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\DBAL\Connection; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; @@ -965,11 +966,16 @@ public function testFetchAssociative( array $parameterTypes, string $expectedSql ): void { - $qb = new QueryBuilder($this->conn); + $qb = new QueryBuilder($this->conn); + $mockedResult = $this->createMock(Result::class); $this->conn->expects(self::once()) + ->method('executeQuery') + ->with($expectedSql, $parameters, $parameterTypes, null) + ->willReturn($mockedResult); + + $mockedResult->expects(self::once()) ->method('fetchAssociative') - ->with($expectedSql, $parameters, $parameterTypes) ->willReturn(['username' => 'jwage', 'password' => 'changeme']); $row = $qb->select($select) @@ -995,11 +1001,16 @@ public function testFetchNumeric( array $parameterTypes, string $expectedSql ): void { - $qb = new QueryBuilder($this->conn); + $qb = new QueryBuilder($this->conn); + $mockedResult = $this->createMock(Result::class); $this->conn->expects(self::once()) + ->method('executeQuery') + ->with($expectedSql, $parameters, $parameterTypes, null) + ->willReturn($mockedResult); + + $mockedResult->expects(self::once()) ->method('fetchNumeric') - ->with($expectedSql, $parameters, $parameterTypes) ->willReturn(['jwage', 'changeme']); $row = $qb->select($select) @@ -1025,11 +1036,16 @@ public function testFetchOne( array $parameterTypes, string $expectedSql ): void { - $qb = new QueryBuilder($this->conn); + $qb = new QueryBuilder($this->conn); + $mockedResult = $this->createMock(Result::class); $this->conn->expects(self::once()) + ->method('executeQuery') + ->with($expectedSql, $parameters, $parameterTypes, null) + ->willReturn($mockedResult); + + $mockedResult->expects(self::once()) ->method('fetchOne') - ->with($expectedSql, $parameters, $parameterTypes) ->willReturn('jwage'); $username = $qb->select($select) @@ -1055,11 +1071,16 @@ public function testFetchAllAssociative( array $parameterTypes, string $expectedSql ): void { - $qb = new QueryBuilder($this->conn); + $qb = new QueryBuilder($this->conn); + $mockedResult = $this->createMock(Result::class); $this->conn->expects(self::once()) + ->method('executeQuery') + ->with($expectedSql, $parameters, $parameterTypes, null) + ->willReturn($mockedResult); + + $mockedResult->expects(self::once()) ->method('fetchAllAssociative') - ->with($expectedSql, $parameters, $parameterTypes) ->willReturn( [ ['username' => 'jwage', 'password' => 'changeme'], @@ -1096,11 +1117,16 @@ public function testFetchAllNumeric( array $parameterTypes, string $expectedSql ): void { - $qb = new QueryBuilder($this->conn); + $qb = new QueryBuilder($this->conn); + $mockedResult = $this->createMock(Result::class); $this->conn->expects(self::once()) + ->method('executeQuery') + ->with($expectedSql, $parameters, $parameterTypes, null) + ->willReturn($mockedResult); + + $mockedResult->expects(self::once()) ->method('fetchAllNumeric') - ->with($expectedSql, $parameters, $parameterTypes) ->willReturn( [ ['jwage', 'changeme'], @@ -1137,11 +1163,16 @@ public function testFetchAllKeyValue( array $parameterTypes, string $expectedSql ): void { - $qb = new QueryBuilder($this->conn); + $qb = new QueryBuilder($this->conn); + $mockedResult = $this->createMock(Result::class); $this->conn->expects(self::once()) + ->method('executeQuery') + ->with($expectedSql, $parameters, $parameterTypes, null) + ->willReturn($mockedResult); + + $mockedResult->expects(self::once()) ->method('fetchAllKeyValue') - ->with($expectedSql, $parameters, $parameterTypes) ->willReturn( [ 'jwage' => 'changeme', @@ -1178,11 +1209,16 @@ public function testFetchAllAssociativeIndexed( array $parameterTypes, string $expectedSql ): void { - $qb = new QueryBuilder($this->conn); + $qb = new QueryBuilder($this->conn); + $mockedResult = $this->createMock(Result::class); $this->conn->expects(self::once()) + ->method('executeQuery') + ->with($expectedSql, $parameters, $parameterTypes, null) + ->willReturn($mockedResult); + + $mockedResult->expects(self::once()) ->method('fetchAllAssociativeIndexed') - ->with($expectedSql, $parameters, $parameterTypes) ->willReturn( [ 1 => [ @@ -1223,11 +1259,16 @@ public function testFetchFirstColumn( array $parameterTypes, string $expectedSql ): void { - $qb = new QueryBuilder($this->conn); + $qb = new QueryBuilder($this->conn); + $mockedResult = $this->createMock(Result::class); $this->conn->expects(self::once()) + ->method('executeQuery') + ->with($expectedSql, $parameters, $parameterTypes, null) + ->willReturn($mockedResult); + + $mockedResult->expects(self::once()) ->method('fetchFirstColumn') - ->with($expectedSql, $parameters, $parameterTypes) ->willReturn( [ 'jwage', @@ -1320,13 +1361,49 @@ public function testExecuteQuery( $this->conn->expects(self::once()) ->method('executeQuery') - ->with($expectedSql, $parameters, $parameterTypes) + ->with($expectedSql, $parameters, $parameterTypes, null) + ->willReturn($mockedResult); + + $results = $qb->select($select) + ->from($from) + ->where($where) + ->setParameters($parameters, $parameterTypes) + ->executeQuery(); + + self::assertSame( + $mockedResult, + $results + ); + } + + /** + * @param list|array $parameters + * @param array|array $parameterTypes + * + * @dataProvider fetchProvider + */ + public function testExecuteQueryWithResultCaching( + string $select, + string $from, + string $where, + array $parameters, + array $parameterTypes, + string $expectedSql + ): void { + $qb = new QueryBuilder($this->conn); + $qcp = new QueryCacheProfile(300); + $mockedResult = $this->createMock(Result::class); + + $this->conn->expects(self::once()) + ->method('executeQuery') + ->with($expectedSql, $parameters, $parameterTypes, $qcp) ->willReturn($mockedResult); $results = $qb->select($select) ->from($from) ->where($where) ->setParameters($parameters, $parameterTypes) + ->enableResultCache($qcp) ->executeQuery(); self::assertSame(