diff --git a/UPGRADE_TO_2_2 b/UPGRADE_TO_2_2 index 220fb39f9..757413029 100644 --- a/UPGRADE_TO_2_2 +++ b/UPGRADE_TO_2_2 @@ -1,3 +1,17 @@ +# ResultCache implementation rewritten + +The result cache is completly rewritten and now works on the database result level, not inside the ORM AbstractQuery +anymore. This means that for result cached queries the hydration will now always be performed again, regardless of +the hydration mode. Affected areas are: + +1. Fixes the problem that entities coming from the result cache were not registered in the UnitOfWork + leading to problems during EntityManager#flush. Calls to EntityManager#merge are not necessary anymore. +2. Affects the array hydrator which now includes the overhead of hydration compared to caching the final result. + +The API is backwards compatible however most of the getter methods on the `AbstractQuery` object are now +deprecated in favor of calling AbstractQuery#getQueryCacheProfile(). This method returns a `Doctrine\DBAL\Cache\QueryCacheProfile` +instance with access to result cache driver, lifetime and cache key. + # EntityManager#getPartialReference() creates read-only entity Entities returned from EntityManager#getPartialReference() are now marked as read-only if they diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 5d71cf0aa..0f28e71db 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -20,7 +20,8 @@ namespace Doctrine\ORM; use Doctrine\DBAL\Types\Type, - Doctrine\ORM\Query\QueryException; + Doctrine\ORM\Query\QueryException, + Doctrine\DBAL\Cache\QueryCacheProfile; /** * Base contract for ORM queries. Base class for Query and NativeQuery. @@ -91,34 +92,15 @@ abstract class AbstractQuery protected $_hydrationMode = self::HYDRATE_OBJECT; /** - * The locally set cache driver used for caching result sets of this query. - * - * @var CacheDriver + * @param \Doctrine\DBAL\Cache\QueryCacheProfile */ - protected $_resultCacheDriver; - - /** - * Boolean flag for whether or not to cache the results of this query. - * - * @var boolean - */ - protected $_useResultCache; - - /** - * @var string The id to store the result cache entry under. - */ - protected $_resultCacheId; + protected $_queryCacheProfile; /** * @var boolean Boolean value that indicates whether or not expire the result cache. */ protected $_expireResultCache = false; - /** - * @var int Result Cache lifetime. - */ - protected $_resultCacheTTL; - /** * Initializes a new instance of a class derived from AbstractQuery. * @@ -260,7 +242,7 @@ abstract class AbstractQuery } /** - * Defines a cache driver to be used for caching result sets. + * Defines a cache driver to be used for caching result sets and implictly enables caching. * * @param Doctrine\Common\Cache\Cache $driver Cache driver * @return Doctrine\ORM\AbstractQuery @@ -270,9 +252,10 @@ abstract class AbstractQuery if ($resultCacheDriver !== null && ! ($resultCacheDriver instanceof \Doctrine\Common\Cache\Cache)) { throw ORMException::invalidResultCacheDriver(); } - $this->_resultCacheDriver = $resultCacheDriver; - if ($resultCacheDriver) { - $this->_useResultCache = true; + if ($this->_queryCacheProfile) { + $this->_queryCacheProfile = $this->_queryCacheProfile->setResultCacheDriver($resultCacheDriver); + } else { + $this->_queryCacheProfile = new QueryCacheProfile(0, null, $resultCacheDriver); } return $this; } @@ -280,12 +263,13 @@ abstract class AbstractQuery /** * Returns the cache driver used for caching result sets. * + * @deprecated * @return Doctrine\Common\Cache\Cache Cache driver */ public function getResultCacheDriver() { - if ($this->_resultCacheDriver) { - return $this->_resultCacheDriver; + if ($this->_queryCacheProfile && $this->_queryCacheProfile->getResultCacheDriver()) { + return $this->_queryCacheProfile->getResultCacheDriver(); } else { return $this->_em->getConfiguration()->getResultCacheImpl(); } @@ -296,18 +280,17 @@ abstract class AbstractQuery * how long and which ID to use for the cache entry. * * @param boolean $bool - * @param integer $timeToLive + * @param integer $lifetime * @param string $resultCacheId * @return Doctrine\ORM\AbstractQuery This query instance. */ - public function useResultCache($bool, $timeToLive = null, $resultCacheId = null) + public function useResultCache($bool, $lifetime = null, $resultCacheId = null) { - $this->_useResultCache = $bool; - if ($timeToLive) { - $this->setResultCacheLifetime($timeToLive); - } - if ($resultCacheId) { - $this->_resultCacheId = $resultCacheId; + if ($bool) { + $this->setResultCacheLifetime($lifetime); + $this->setResultCacheId($resultCacheId); + } else { + $this->_queryCacheProfile = null; } return $this; } @@ -315,27 +298,33 @@ abstract class AbstractQuery /** * Defines how long the result cache will be active before expire. * - * @param integer $timeToLive How long the cache entry is valid. + * @param integer $lifetime How long the cache entry is valid. * @return Doctrine\ORM\AbstractQuery This query instance. */ - public function setResultCacheLifetime($timeToLive) + public function setResultCacheLifetime($lifetime) { - if ($timeToLive !== null) { - $timeToLive = (int) $timeToLive; + if ($lifetime === null) { + $lifetime = 0; + } else { + $lifetime = (int)$lifetime; + } + if ($this->_queryCacheProfile) { + $this->_queryCacheProfile = $this->_queryCacheProfile->setLifetime($lifetime); + } else { + $this->_queryCacheProfile = new QueryCacheProfile($lifetime); } - - $this->_resultCacheTTL = $timeToLive; return $this; } /** * Retrieves the lifetime of resultset cache. * + * @deprecated * @return integer */ public function getResultCacheLifetime() { - return $this->_resultCacheTTL; + return $this->_queryCacheProfile ? $this->_queryCacheProfile->getLifetime() : 0; } /** @@ -360,6 +349,14 @@ abstract class AbstractQuery return $this->_expireResultCache; } + /** + * @return QueryCacheProfile + */ + public function getQueryCacheProfile() + { + return $this->_queryCacheProfile; + } + /** * Change the default fetch mode of an association for this query. * @@ -584,28 +581,6 @@ abstract class AbstractQuery $this->setParameters($params); } - // Check result cache - if ($this->_useResultCache && $cacheDriver = $this->getResultCacheDriver()) { - list($key, $hash) = $this->getResultCacheId(); - $cached = $this->_expireResultCache ? false : $cacheDriver->fetch($hash); - - if ($cached === false || !isset($cached[$key])) { - // Cache miss. - $stmt = $this->_doExecute(); - - $result = $this->_em->getHydrator($this->_hydrationMode)->hydrateAll( - $stmt, $this->_resultSetMapping, $this->_hints - ); - - $cacheDriver->save($hash, array($key => $result), $this->_resultCacheTTL); - - return $result; - } else { - // Cache hit. - return $cached[$key]; - } - } - $stmt = $this->_doExecute(); if (is_numeric($stmt)) { @@ -627,43 +602,23 @@ abstract class AbstractQuery */ public function setResultCacheId($id) { - $this->_resultCacheId = $id; + if ($this->_queryCacheProfile) { + $this->_queryCacheProfile = $this->_queryCacheProfile->setCacheKey($id); + } else { + $this->_queryCacheProfile = new QueryCacheProfile(0, $id); + } return $this; } /** - * 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 - * automatically generated for you. + * Get the result cache id to use to store the result set cache entry if set. * - * @return array ($key, $hash) + * @deprecated + * @return string */ - protected function getResultCacheId() + public function getResultCacheId() { - if ($this->_resultCacheId) { - return array($this->_resultCacheId, $this->_resultCacheId); - } else { - $params = $this->_params; - foreach ($params AS $key => $value) { - if (is_object($value) && $this->_em->getMetadataFactory()->hasMetadataFor(get_class($value))) { - if ($this->_em->getUnitOfWork()->getEntityState($value) == UnitOfWork::STATE_MANAGED) { - $idValues = $this->_em->getUnitOfWork()->getEntityIdentifier($value); - } else { - $class = $this->_em->getClassMetadata(get_class($value)); - $idValues = $class->getIdentifierValues($value); - } - $params[$key] = $idValues; - } else { - $params[$key] = $value; - } - } - - $sql = $this->getSql(); - ksort($this->_hints); - $key = implode(";", (array)$sql) . var_export($params, true) . - var_export($this->_hints, true)."&hydrationMode=".$this->_hydrationMode; - return array($key, md5($key)); - } + return $this->_queryCacheProfile ? $this->_queryCacheProfile->getCacheKey() : null; } /** diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index ee6496087..eaa4df562 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -209,27 +209,6 @@ class Configuration extends \Doctrine\DBAL\Configuration $this->_attributes['metadataDriverImpl'] : null; } - /** - * Gets the cache driver implementation that is used for query result caching. - * - * @return \Doctrine\Common\Cache\Cache - */ - public function getResultCacheImpl() - { - return isset($this->_attributes['resultCacheImpl']) ? - $this->_attributes['resultCacheImpl'] : null; - } - - /** - * Sets the cache driver implementation that is used for query result caching. - * - * @param \Doctrine\Common\Cache\Cache $cacheImpl - */ - public function setResultCacheImpl(Cache $cacheImpl) - { - $this->_attributes['resultCacheImpl'] = $cacheImpl; - } - /** * Gets the cache driver implementation that is used for the query cache (SQL cache). * diff --git a/lib/Doctrine/ORM/NativeQuery.php b/lib/Doctrine/ORM/NativeQuery.php index 2c0a5ab28..dea223fa3 100644 --- a/lib/Doctrine/ORM/NativeQuery.php +++ b/lib/Doctrine/ORM/NativeQuery.php @@ -57,17 +57,17 @@ final class NativeQuery extends AbstractQuery */ protected function _doExecute() { - $stmt = $this->_em->getConnection()->prepare($this->_sql); $params = $this->_params; - foreach ($params as $key => $value) { - if (isset($this->_paramTypes[$key])) { - $stmt->bindValue($key, $value, $this->_paramTypes[$key]); - } else { - $stmt->bindValue($key, $value); + $types = $this->_paramTypes; + if ($params) { + if (is_int(key($params))) { + ksort($params); + ksort($types); + $params = array_values($params); + $types = array_values($types); } } - $stmt->execute(); - return $stmt; + return $this->_em->getConnection()->executeQuery($this->_sql, $params, $types, $this->_queryCacheProfile); } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index e0bbcec5a..293c64391 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -232,6 +232,9 @@ final class Query extends AbstractQuery protected function _doExecute() { $executor = $this->_parse()->getSqlExecutor(); + if ($this->_queryCacheProfile) { + $executor->setQueryCacheProfile($this->_queryCacheProfile); + } // Prepare parameters $paramMappings = $this->_parserResult->getParameterMappings(); diff --git a/lib/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php b/lib/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php index 7879b0ff2..f44e383b9 100644 --- a/lib/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php +++ b/lib/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php @@ -1,7 +1,5 @@ _sqlStatements; } + public function setQueryCacheProfile(QueryCacheProfile $qcp) + { + $this->queryCacheProfile = $qcp; + } + /** * Executes all sql statements. * * @param Doctrine\DBAL\Connection $conn The database connection that is used to execute the queries. * @param array $params The parameters. + * @param array $types The parameter types. * @return Doctrine\DBAL\Driver\Statement */ - abstract public function execute(Connection $conn, array $params, array $types); + abstract public function execute(Connection $conn, array $params, array $types); } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php b/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php index a6c22cecd..5b07d4d02 100644 --- a/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php +++ b/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php @@ -1,7 +1,5 @@ - * @version $Revision$ * @link www.doctrine-project.org * @since 2.0 */ @@ -41,8 +38,11 @@ class SingleSelectExecutor extends AbstractSqlExecutor $this->_sqlStatements = $sqlWalker->walkSelectStatement($AST); } + /** + * {@inheritDoc} + */ public function execute(Connection $conn, array $params, array $types) { - return $conn->executeQuery($this->_sqlStatements, $params, $types); + return $conn->executeQuery($this->_sqlStatements, $params, $types, $this->queryCacheProfile); } } diff --git a/lib/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php b/lib/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php index 94db13b05..facccb715 100644 --- a/lib/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php +++ b/lib/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php @@ -1,7 +1,5 @@ - * @version $Revision$ * @link www.doctrine-project.org * @since 2.0 * @todo This is exactly the same as SingleSelectExecutor. Unify in SingleStatementExecutor. @@ -45,7 +42,10 @@ class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor $this->_sqlStatements = $sqlWalker->walkDeleteStatement($AST); } } - + + /** + * {@inheritDoc} + */ public function execute(Connection $conn, array $params, array $types) { return $conn->executeUpdate($this->_sqlStatements, $params, $types); diff --git a/tests/Doctrine/Tests/ORM/Functional/ResultCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/ResultCacheTest.php index 235b4c91c..3dcae4fab 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ResultCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ResultCacheTest.php @@ -90,10 +90,10 @@ class ResultCacheTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testUseResultCache() { $cache = new \Doctrine\Common\Cache\ArrayCache(); - $this->_em->getConfiguration()->setResultCacheImpl($cache); $query = $this->_em->createQuery('select ux from Doctrine\Tests\Models\CMS\CmsUser ux'); $query->useResultCache(true); + $query->setResultCacheDriver($cache); $query->setResultCacheId('testing_result_cache_id'); $users = $query->getResult(); @@ -108,11 +108,11 @@ class ResultCacheTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testUseResultCacheParams() { $cache = new \Doctrine\Common\Cache\ArrayCache(); - $this->_em->getConfiguration()->setResultCacheImpl($cache); $sqlCount = count($this->_sqlLoggerStack->queries); $query = $this->_em->createQuery('select ux from Doctrine\Tests\Models\CMS\CmsUser ux WHERE ux.id = ?1'); $query->setParameter(1, 1); + $query->setResultCacheDriver($cache); $query->useResultCache(true); $query->getResult(); @@ -149,10 +149,10 @@ class ResultCacheTest extends \Doctrine\Tests\OrmFunctionalTestCase } /** - * @param $query + * @param string $query * @depends testNativeQueryResultCaching */ - public function testResultCacheDependsOnQueryHints($query) + public function testResultCacheNotDependsOnQueryHints($query) { $cache = $query->getResultCacheDriver(); $cacheCount = $this->getCacheSize($cache); @@ -160,7 +160,7 @@ class ResultCacheTest extends \Doctrine\Tests\OrmFunctionalTestCase $query->setHint('foo', 'bar'); $query->getResult(); - $this->assertEquals($cacheCount + 1, $this->getCacheSize($cache)); + $this->assertEquals($cacheCount, $this->getCacheSize($cache)); } /** @@ -182,7 +182,7 @@ class ResultCacheTest extends \Doctrine\Tests\OrmFunctionalTestCase * @param $query * @depends testNativeQueryResultCaching */ - public function testResultCacheDependsOnHydrationMode($query) + public function testResultCacheNotDependsOnHydrationMode($query) { $cache = $query->getResultCacheDriver(); $cacheCount = $this->getCacheSize($cache); @@ -190,7 +190,7 @@ class ResultCacheTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertNotEquals(\Doctrine\ORM\Query::HYDRATE_ARRAY, $query->getHydrationMode()); $query->getArrayResult(); - $this->assertEquals($cacheCount + 1, $this->getCacheSize($cache)); + $this->assertEquals($cacheCount, $this->getCacheSize($cache)); } /**