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/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 0379ccf56..fab80ff29 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -92,7 +92,7 @@ class BasicEntityPersister /** * The database platform. - * + * * @var Doctrine\DBAL\Platforms\AbstractPlatform */ protected $_platform; @@ -110,12 +110,12 @@ class BasicEntityPersister * @var array */ protected $_queuedInserts = array(); - + /** * ResultSetMapping that is used for all queries. Is generated lazily once per request. - * + * * TODO: Evaluate Caching in combination with the other cached SQL snippets. - * + * * @var Query\ResultSetMapping */ protected $_rsm; @@ -123,7 +123,7 @@ class BasicEntityPersister /** * The map of column names to DBAL mapping types of all prepared columns used * when INSERTing or UPDATEing an entity. - * + * * @var array * @see _prepareInsertData($entity) * @see _prepareUpdateData($entity) @@ -133,7 +133,7 @@ class BasicEntityPersister /** * The INSERT SQL statement used for entities handled by this persister. * This SQL is only generated once per request, if at all. - * + * * @var string */ private $_insertSql; @@ -141,29 +141,29 @@ class BasicEntityPersister /** * The SELECT column list SQL fragment used for querying entities by this persister. * This SQL fragment is only generated once per request, if at all. - * + * * @var string */ protected $_selectColumnListSql; - + /** * The JOIN SQL fragement used to eagerly load all many-to-one and one-to-one * associations configured as FETCH_EAGER, aswell as all inverse one-to-one associations. - * + * * @var string */ protected $_selectJoinSql; /** * Counter for creating unique SQL table and column aliases. - * + * * @var integer */ protected $_sqlAliasCounter = 0; /** * Map from class names (FQCN) to the corresponding generated SQL table aliases. - * + * * @var array */ protected $_sqlTableAliases = array(); @@ -171,7 +171,7 @@ class BasicEntityPersister /** * Initializes a new BasicEntityPersister that uses the given EntityManager * and persists instances of the class described by the given ClassMetadata descriptor. - * + * * @param Doctrine\ORM\EntityManager $em * @param Doctrine\ORM\Mapping\ClassMetadata $class */ @@ -205,7 +205,7 @@ 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 @@ -229,7 +229,7 @@ class BasicEntityPersister if (isset($insertData[$tableName])) { $paramIndex = 1; - + foreach ($insertData[$tableName] as $column => $value) { $stmt->bindValue($paramIndex++, $value, $this->_columnTypes[$column]); } @@ -257,7 +257,7 @@ class BasicEntityPersister /** * Retrieves the default version value which was created - * by the preceding INSERT statement and assigns it back in to the + * by the preceding INSERT statement and assigns it back in to the * entities version field. * * @param object $entity @@ -271,7 +271,7 @@ class BasicEntityPersister /** * Fetch the current version value of a versioned entity. - * + * * @param Doctrine\ORM\Mapping\ClassMetadata $versionedClass * @param mixed $id * @return mixed @@ -280,9 +280,9 @@ class BasicEntityPersister { $versionField = $versionedClass->versionField; $identifier = $versionedClass->getIdentifierColumnNames(); - + $versionFieldColumnName = $versionedClass->getQuotedColumnName($versionField, $this->_platform); - + //FIXME: Order with composite keys might not be correct $sql = 'SELECT ' . $versionFieldColumnName . ' FROM ' . $versionedClass->getQuotedTableName($this->_platform) @@ -299,7 +299,7 @@ class BasicEntityPersister * 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. @@ -310,7 +310,7 @@ class BasicEntityPersister { $updateData = $this->_prepareUpdateData($entity); $tableName = $this->_class->getTableName(); - + if (isset($updateData[$tableName]) && $updateData[$tableName]) { $this->_updateTable( $entity, $this->_class->getQuotedTableName($this->_platform), @@ -338,17 +338,17 @@ class BasicEntityPersister $set = $params = $types = array(); foreach ($updateData as $columnName => $value) { - $set[] = (isset($this->_class->fieldNames[$columnName])) + $set[] = (isset($this->_class->fieldNames[$columnName])) ? $this->_class->getQuotedColumnName($this->_class->fieldNames[$columnName], $this->_platform) . ' = ?' : $columnName . ' = ?'; - + $params[] = $value; $types[] = $this->_columnTypes[$columnName]; } $where = array(); $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity); - + foreach ($this->_class->identifier as $idField) { if (isset($this->_class->associationMappings[$idField])) { $targetMapping = $this->_em->getClassMetadata($this->_class->associationMappings[$idField]['targetEntity']); @@ -366,13 +366,13 @@ class BasicEntityPersister $versionField = $this->_class->versionField; $versionFieldType = $this->_class->fieldMappings[$versionField]['type']; $versionColumn = $this->_class->getQuotedColumnName($versionField, $this->_platform); - + if ($versionFieldType == Type::INTEGER) { $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1'; } else if ($versionFieldType == Type::DATETIME) { $set[] = $versionColumn . ' = CURRENT_TIMESTAMP'; } - + $where[] = $versionColumn; $params[] = $this->_class->reflFields[$versionField]->getValue($entity); $types[] = $this->_class->fieldMappings[$versionField]['type']; @@ -401,18 +401,18 @@ class BasicEntityPersister // @Todo this only covers scenarios with no inheritance or of the same level. Is there something // like self-referential relationship between different levels of an inheritance hierachy? I hope not! $selfReferential = ($mapping['targetEntity'] == $mapping['sourceEntity']); - + if ( ! $mapping['isOwningSide']) { $relatedClass = $this->_em->getClassMetadata($mapping['targetEntity']); $mapping = $relatedClass->associationMappings[$mapping['mappedBy']]; $keys = array_keys($mapping['relationToTargetKeyColumns']); - + if ($selfReferential) { $otherKeys = array_keys($mapping['relationToSourceKeyColumns']); } } else { $keys = array_keys($mapping['relationToSourceKeyColumns']); - + if ($selfReferential) { $otherKeys = array_keys($mapping['relationToTargetKeyColumns']); } @@ -420,13 +420,13 @@ class BasicEntityPersister if ( ! isset($mapping['isOnDeleteCascade'])) { $this->_conn->delete( - $this->_class->getQuotedJoinTableName($mapping, $this->_platform), + $this->_class->getQuotedJoinTableName($mapping, $this->_platform), array_combine($keys, $identifier) ); if ($selfReferential) { $this->_conn->delete( - $this->_class->getQuotedJoinTableName($mapping, $this->_platform), + $this->_class->getQuotedJoinTableName($mapping, $this->_platform), array_combine($otherKeys, $identifier) ); } @@ -458,7 +458,7 @@ class BasicEntityPersister * Prepares the changeset of an entity for database insertion (UPDATE). * * The changeset is obtained from the currently running UnitOfWork. - * + * * During this preparation the array that is passed as the second parameter is filled with * => pairs, grouped by table name. * @@ -493,7 +493,7 @@ class BasicEntityPersister if (isset($this->_class->associationMappings[$field])) { $assoc = $this->_class->associationMappings[$field]; - + // Only owning side of x-1 associations can have a FK column. if ( ! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) { continue; @@ -501,7 +501,7 @@ class BasicEntityPersister if ($newVal !== null) { $oid = spl_object_hash($newVal); - + if (isset($this->_queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) { // The associated entity $newVal is not yet persisted, so we must // set $newVal = null, in order to insert a null value and schedule an @@ -528,7 +528,7 @@ class BasicEntityPersister } else { $result[$owningTable][$sourceColumn] = $newValId[$targetClass->fieldNames[$targetColumn]]; } - + $this->_columnTypes[$sourceColumn] = $targetClass->getTypeOfColumn($targetColumn); } } else { @@ -537,7 +537,7 @@ class BasicEntityPersister $result[$this->getOwningTable($field)][$columnName] = $newVal; } } - + return $result; } @@ -589,7 +589,7 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, $lockMode, $limit); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); - + if ($entity !== null) { $hints[Query::HINT_REFRESH] = true; $hints[Query::HINT_REFRESH_ENTITY] = $entity; @@ -597,7 +597,7 @@ class BasicEntityPersister $hydrator = $this->_em->newHydrator($this->_selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); $entities = $hydrator->hydrateAll($stmt, $this->_rsm, $hints); - + return $entities ? $entities[0] : null; } @@ -626,17 +626,17 @@ class BasicEntityPersister // Mark inverse side as fetched in the hints, otherwise the UoW would // try to load it in a separate query (remember: to-one inverse sides can not be lazy). $hints = array(); - + if ($isInverseSingleValued) { $hints['fetched'][$targetClass->name][$assoc['inversedBy']] = true; - + if ($targetClass->subClasses) { foreach ($targetClass->subClasses as $targetSubclassName) { $hints['fetched'][$targetSubclassName][$assoc['inversedBy']] = true; } } } - + /* cascade read-only status if ($this->_em->getUnitOfWork()->isReadOnly($sourceEntity)) { $hints[Query::HINT_READ_ONLY] = true; @@ -652,7 +652,7 @@ class BasicEntityPersister } else { $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); $owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']); - + // TRICKY: since the association is specular source and target are flipped foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) { if ( ! isset($sourceClass->fieldNames[$sourceKeyColumn])) { @@ -660,12 +660,12 @@ class BasicEntityPersister $sourceClass->name, $sourceKeyColumn ); } - + // unset the old value and set the new sql aliased value here. By definition // unset($identifier[$targetKeyColumn] works here with how UnitOfWork::createEntity() calls this method. $identifier[$this->_getSQLTableAlias($targetClass->name) . "." . $targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); - + unset($identifier[$targetKeyColumn]); } @@ -681,7 +681,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. @@ -691,16 +691,16 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($id); list($params, $types) = $this->expandParameters($id); $stmt = $this->_conn->executeQuery($sql, $params, $types); - + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); $hydrator->hydrateAll($stmt, $this->_rsm, array(Query::HINT_REFRESH => true)); if (isset($this->_class->lifecycleCallbacks[Events::postLoad])) { $this->_class->invokeLifecycleCallbacks(Events::postLoad, $entity); } - + $evm = $this->_em->getEventManager(); - + if ($evm->hasListeners(Events::postLoad)) { $evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->_em)); } @@ -708,7 +708,7 @@ class BasicEntityPersister /** * Loads a list of entities by a list of field criteria. - * + * * @param array $criteria * @param array $orderBy * @param int $limit @@ -723,13 +723,13 @@ class BasicEntityPersister $stmt = $this->_conn->executeQuery($sql, $params, $types); $hydrator = $this->_em->newHydrator(($this->_selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); - + return $hydrator->hydrateAll($stmt, $this->_rsm, array('deferEagerLoads' => true)); } /** * Get (sliced or full) elements of the given collection. - * + * * @param array $assoc * @param object $sourceEntity * @param int|null $offset @@ -739,16 +739,16 @@ class BasicEntityPersister public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) { $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit); - + return $this->loadArrayFromStatement($assoc, $stmt); } /** * Load an array of entities from a given dbal statement. - * + * * @param array $assoc * @param Doctrine\DBAL\Statement $stmt - * + * * @return array */ private function loadArrayFromStatement($assoc, $stmt) @@ -763,21 +763,21 @@ class BasicEntityPersister } $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); - + return $hydrator->hydrateAll($stmt, $rsm, $hints); } /** * Hydrate a collection from a given dbal statement. - * + * * @param array $assoc * @param Doctrine\DBAL\Statement $stmt * @param PersistentCollection $coll - * + * * @return array */ private function loadCollectionFromStatement($assoc, $stmt, $coll) - { + { $hints = array('deferEagerLoads' => true, 'collection' => $coll); if (isset($assoc['indexBy'])) { @@ -788,7 +788,7 @@ class BasicEntityPersister } $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); - + return $hydrator->hydrateAll($stmt, $rsm, $hints); } @@ -805,7 +805,7 @@ class BasicEntityPersister public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) { $stmt = $this->getManyToManyStatement($assoc, $sourceEntity); - + return $this->loadCollectionFromStatement($assoc, $stmt, $coll); } @@ -813,15 +813,15 @@ class BasicEntityPersister { $criteria = array(); $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); - + if ($assoc['isOwningSide']) { $quotedJoinTable = $sourceClass->getQuotedJoinTableName($assoc, $this->_platform); - + foreach ($assoc['relationToSourceKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) { if ($sourceClass->containsForeignIdentifier) { $field = $sourceClass->getFieldForColumn($sourceKeyColumn); $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); - + if (isset($sourceClass->associationMappings[$field])) { $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); $value = $value[$this->_em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]]; @@ -839,18 +839,18 @@ class BasicEntityPersister } else { $owningAssoc = $this->_em->getClassMetadata($assoc['targetEntity'])->associationMappings[$assoc['mappedBy']]; $quotedJoinTable = $sourceClass->getQuotedJoinTableName($owningAssoc, $this->_platform); - + // TRICKY: since the association is inverted source and target are flipped foreach ($owningAssoc['relationToTargetKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) { if ($sourceClass->containsForeignIdentifier) { $field = $sourceClass->getFieldForColumn($sourceKeyColumn); $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); - + if (isset($sourceClass->associationMappings[$field])) { $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); $value = $value[$this->_em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]]; } - + $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $value; } else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) { $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); @@ -864,7 +864,7 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset); list($params, $types) = $this->expandParameters($criteria); - + return $this->_conn->executeQuery($sql, $params, $types); } @@ -890,7 +890,7 @@ class BasicEntityPersister $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $this->_getSQLTableAlias($this->_class->name)) : ''; $lockSql = ''; - + if ($lockMode == LockMode::PESSIMISTIC_READ) { $lockSql = ' ' . $this->_platform->getReadLockSql(); } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) { @@ -908,7 +908,7 @@ class BasicEntityPersister /** * Gets the ORDER BY SQL snippet for ordered collections. - * + * * @param array $orderBy * @param string $baseTableAlias * @return string @@ -917,7 +917,7 @@ class BasicEntityPersister protected final function _getOrderBySQL(array $orderBy, $baseTableAlias) { $orderBySql = ''; - + foreach ($orderBy as $fieldName => $orientation) { if ( ! isset($this->_class->fieldMappings[$fieldName])) { throw ORMException::unrecognizedField($fieldName); @@ -928,7 +928,7 @@ class BasicEntityPersister : $baseTableAlias; $columnName = $this->_class->getQuotedColumnName($fieldName, $this->_platform); - + $orderBySql .= $orderBySql ? ', ' : ' ORDER BY '; $orderBySql .= $tableAlias . '.' . $columnName . ' ' . $orientation; } @@ -944,7 +944,7 @@ class BasicEntityPersister * list SQL fragment. Note that in the implementation of BasicEntityPersister * the resulting SQL fragment is generated only once and cached in {@link _selectColumnListSql}. * Subclasses may or may not do the same. - * + * * @return string The SQL fragment. * @todo Rename: _getSelectColumnsSQL() */ @@ -961,75 +961,75 @@ class BasicEntityPersister // Add regular columns to select list foreach ($this->_class->fieldNames as $field) { if ($columnList) $columnList .= ', '; - + $columnList .= $this->_getSelectColumnSQL($field, $this->_class); } $this->_selectJoinSql = ''; $eagerAliasCounter = 0; - + foreach ($this->_class->associationMappings as $assocField => $assoc) { $assocColumnSQL = $this->_getSelectColumnAssociationSQL($assocField, $assoc, $this->_class); - + if ($assocColumnSQL) { if ($columnList) $columnList .= ', '; - + $columnList .= $assocColumnSQL; } - + if ($assoc['type'] & ClassMetadata::TO_ONE && ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || !$assoc['isOwningSide'])) { $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); - + if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) { continue; // now this is why you shouldn't use inheritance } - + $assocAlias = 'e' . ($eagerAliasCounter++); $this->_rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField); - + foreach ($eagerEntity->fieldNames AS $field) { if ($columnList) $columnList .= ', '; - + $columnList .= $this->_getSelectColumnSQL($field, $eagerEntity, $assocAlias); } - + foreach ($eagerEntity->associationMappings as $assoc2Field => $assoc2) { $assoc2ColumnSQL = $this->_getSelectColumnAssociationSQL($assoc2Field, $assoc2, $eagerEntity, $assocAlias); - + if ($assoc2ColumnSQL) { if ($columnList) $columnList .= ', '; $columnList .= $assoc2ColumnSQL; } } - + $this->_selectJoinSql .= ' LEFT JOIN'; // TODO: Inner join when all join columns are NOT nullable. $first = true; - + if ($assoc['isOwningSide']) { $this->_selectJoinSql .= ' ' . $eagerEntity->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; - + foreach ($assoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { if ( ! $first) { $this->_selectJoinSql .= ' AND '; } - - $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.' . $sourceCol . ' = ' + + $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.' . $sourceCol . ' = ' . $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias) . '.' . $targetCol . ' '; $first = false; } } else { $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']); - - $this->_selectJoinSql .= ' ' . $eagerEntity->getQuotedTableName($this->_platform) . ' ' + + $this->_selectJoinSql .= ' ' . $eagerEntity->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) . ' ON '; foreach ($owningAssoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { if ( ! $first) { $this->_selectJoinSql .= ' AND '; } - - $this->_selectJoinSql .= $this->_getSQLTableAlias($owningAssoc['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = ' + + $this->_selectJoinSql .= $this->_getSQLTableAlias($owningAssoc['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = ' . $this->_getSQLTableAlias($owningAssoc['targetEntity']) . '.' . $targetCol . ' '; $first = false; } @@ -1041,33 +1041,33 @@ class BasicEntityPersister return $this->_selectColumnListSql; } - + /** * Gets the SQL join fragment used when selecting entities from an association. - * + * * @param string $field * @param array $assoc * @param ClassMetadata $class * @param string $alias - * - * @return string + * + * @return string */ protected function _getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r') { $columnList = ''; - + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { if ($columnList) $columnList .= ', '; $columnAlias = $srcColumn . $this->_sqlAliasCounter++; $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); - $columnList .= $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) ) + $columnList .= $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) ) . '.' . $srcColumn . ' AS ' . $resultColumnName; $this->_rsm->addMetaResult($alias, $resultColumnName, $srcColumn, isset($assoc['id']) && $assoc['id'] === true); } } - + return $columnList; } @@ -1087,10 +1087,10 @@ class BasicEntityPersister $owningAssoc = $this->_em->getClassMetadata($manyToMany['targetEntity'])->associationMappings[$manyToMany['mappedBy']]; $joinClauses = $owningAssoc['relationToSourceKeyColumns']; } - + $joinTableName = $this->_class->getQuotedJoinTableName($owningAssoc, $this->_platform); $joinSql = ''; - + foreach ($joinClauses as $joinTableColumn => $sourceColumn) { if ($joinSql != '') $joinSql .= ' AND '; @@ -1109,7 +1109,7 @@ class BasicEntityPersister /** * Gets the INSERT SQL used by the persister to persist a new entity. - * + * * @return string */ protected function _getInsertSQL() @@ -1117,7 +1117,7 @@ class BasicEntityPersister if ($this->_insertSql === null) { $insertSql = ''; $columns = $this->_getInsertColumnList(); - + if (empty($columns)) { $insertSql = $this->_platform->getEmptyIdentityInsertSQL( $this->_class->getQuotedTableName($this->_platform), @@ -1130,10 +1130,10 @@ class BasicEntityPersister $insertSql = 'INSERT INTO ' . $this->_class->getQuotedTableName($this->_platform) . ' (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $values) . ')'; } - + $this->_insertSql = $insertSql; } - + return $this->_insertSql; } @@ -1148,15 +1148,15 @@ class BasicEntityPersister protected function _getInsertColumnList() { $columns = array(); - + foreach ($this->_class->reflFields as $name => $field) { if ($this->_class->isVersioned && $this->_class->versionField == $name) { continue; } - + if (isset($this->_class->associationMappings[$name])) { $assoc = $this->_class->associationMappings[$name]; - + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { foreach ($assoc['targetToSourceKeyColumns'] as $sourceCol) { $columns[] = $sourceCol; @@ -1181,10 +1181,10 @@ class BasicEntityPersister protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r') { $columnName = $class->columnNames[$field]; - $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) + $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); - + $this->_rsm->addFieldResult($alias, $columnAlias, $field); return $sql . ' AS ' . $columnAlias; @@ -1192,7 +1192,7 @@ class BasicEntityPersister /** * Gets the SQL table alias for the given class name. - * + * * @param string $className * @return string The SQL table alias. * @todo Reconsider. Binding table aliases to class names is not such a good idea. @@ -1202,15 +1202,15 @@ class BasicEntityPersister if ($assocName) { $className .= '#' . $assocName; } - + if (isset($this->_sqlTableAliases[$className])) { return $this->_sqlTableAliases[$className]; } - + $tableAlias = 't' . $this->_sqlAliasCounter++; $this->_sqlTableAliases[$className] = $tableAlias; - + return $tableAlias; } @@ -1234,9 +1234,9 @@ class BasicEntityPersister $sql = 'SELECT 1 ' . $this->_platform->appendLockHint($this->getLockTablesSql(), $lockMode) . ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ' . $lockSql; - + list($params, $types) = $this->expandParameters($criteria); - + $stmt = $this->_conn->executeQuery($sql, $params, $types); } @@ -1265,25 +1265,25 @@ class BasicEntityPersister protected function _getSelectConditionSQL(array $criteria, $assoc = null) { $conditionSql = ''; - + foreach ($criteria as $field => $value) { $conditionSql .= $conditionSql ? ' AND ' : ''; if (isset($this->_class->columnNames[$field])) { $className = (isset($this->_class->fieldMappings[$field]['inherited'])) - ? $this->_class->fieldMappings[$field]['inherited'] + ? $this->_class->fieldMappings[$field]['inherited'] : $this->_class->name; - + $conditionSql .= $this->_getSQLTableAlias($className) . '.' . $this->_class->getQuotedColumnName($field, $this->_platform); } else if (isset($this->_class->associationMappings[$field])) { if ( ! $this->_class->associationMappings[$field]['isOwningSide']) { throw ORMException::invalidFindByInverseAssociation($this->_class->name, $field); } - + $className = (isset($this->_class->associationMappings[$field]['inherited'])) ? $this->_class->associationMappings[$field]['inherited'] : $this->_class->name; - + $conditionSql .= $this->_getSQLTableAlias($className) . '.' . $this->_class->associationMappings[$field]['joinColumns'][0]['name']; } else if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) { // very careless developers could potentially open up this normally hidden api for userland attacks, @@ -1294,7 +1294,7 @@ class BasicEntityPersister } else { throw ORMException::unrecognizedField($field); } - + $conditionSql .= (is_array($value)) ? ' IN (?)' : (($value === null) ? ' IS NULL' : ' = ?'); } return $conditionSql; @@ -1312,7 +1312,7 @@ class BasicEntityPersister public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) { $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit); - + return $this->loadArrayFromStatement($assoc, $stmt); } @@ -1328,7 +1328,7 @@ class BasicEntityPersister public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) { $stmt = $this->getOneToManyStatement($assoc, $sourceEntity); - + return $this->loadCollectionFromStatement($assoc, $stmt, $coll); } @@ -1353,12 +1353,12 @@ class BasicEntityPersister if ($sourceClass->containsForeignIdentifier) { $field = $sourceClass->getFieldForColumn($sourceKeyColumn); $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); - + if (isset($sourceClass->associationMappings[$field])) { $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value); $value = $value[$this->_em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]]; } - + $criteria[$tableAlias . "." . $targetKeyColumn] = $value; } else { $criteria[$tableAlias . "." . $targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); @@ -1389,13 +1389,13 @@ class BasicEntityPersister $types[] = $this->getType($field, $value); $params[] = $this->getValue($value); } - + return array($params, $types); } - + /** * Infer field type to be used by parameter type casting. - * + * * @param string $field * @param mixed $value * @return integer @@ -1409,11 +1409,11 @@ class BasicEntityPersister case (isset($this->_class->associationMappings[$field])): $assoc = $this->_class->associationMappings[$field]; - + if (count($assoc['sourceToTargetKeyColumns']) > 1) { throw Query\QueryException::associationPathCompositeKeyNotSupported(); } - + $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); $targetColumn = $assoc['joinColumns'][0]['referencedColumnName']; $type = null; @@ -1431,36 +1431,36 @@ class BasicEntityPersister if (is_array($value)) { $type += Connection::ARRAY_PARAM_OFFSET; } - + return $type; } - + /** * Retrieve parameter value - * + * * @param mixed $value - * @return mixed + * @return mixed */ private function getValue($value) { if (is_array($value)) { $newValue = array(); - + foreach ($value as $itemValue) { $newValue[] = $this->getIndividualValue($itemValue); } - + return $newValue; } - + return $this->getIndividualValue($value); } - + /** * Retrieve an invidiual parameter value - * + * * @param mixed $value - * @return mixed + * @return mixed */ private function getIndividualValue($value) { @@ -1471,11 +1471,11 @@ class BasicEntityPersister $class = $this->_em->getClassMetadata(get_class($value)); $idValues = $class->getIdentifierValues($value); } - + $value = $idValues[key($idValues)]; } - - return $value; + + return $value; } /** @@ -1487,17 +1487,17 @@ class BasicEntityPersister public function exists($entity, array $extraConditions = array()) { $criteria = $this->_class->getIdentifierValues($entity); - + if ($extraConditions) { $criteria = array_merge($criteria, $extraConditions); } - $sql = 'SELECT 1' - . ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($this->_class->name) + $sql = 'SELECT 1 ' + . $this->getLockTablesSql() . ' WHERE ' . $this->_getSelectConditionSQL($criteria); - + list($params, $types) = $this->expandParameters($criteria); - + return (bool) $this->_conn->fetchColumn($sql, $params); } } diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index c95fee755..fb60d5e32 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -46,7 +46,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister /** * Map of table to quoted table names. - * + * * @var array */ private $_quotedTableMap = array(); @@ -59,7 +59,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $class = ($this->_class->name !== $this->_class->rootEntityName) ? $this->_em->getClassMetadata($this->_class->rootEntityName) : $this->_class; - + return $class->getTableName(); } @@ -73,10 +73,10 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister { if (isset($this->_class->fieldMappings[$this->_class->versionField]['inherited'])) { $definingClassName = $this->_class->fieldMappings[$this->_class->versionField]['inherited']; - + return $this->_em->getClassMetadata($definingClassName); } - + return $this->_class; } @@ -92,7 +92,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister if (isset($this->_owningTableMap[$fieldName])) { return $this->_owningTableMap[$fieldName]; } - + if (isset($this->_class->associationMappings[$fieldName]['inherited'])) { $cm = $this->_em->getClassMetadata($this->_class->associationMappings[$fieldName]['inherited']); } else if (isset($this->_class->fieldMappings[$fieldName]['inherited'])) { @@ -130,15 +130,15 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister // Prepare statements for sub tables. $subTableStmts = array(); - + if ($rootClass !== $this->_class) { $subTableStmts[$this->_class->getTableName()] = $this->_conn->prepare($this->_getInsertSQL()); } - + foreach ($this->_class->parentClasses as $parentClassName) { $parentClass = $this->_em->getClassMetadata($parentClassName); $parentTableName = $parentClass->getTableName(); - + if ($parentClass !== $rootClass) { $parentPersister = $this->_em->getUnitOfWork()->getEntityPersister($parentClassName); $subTableStmts[$parentTableName] = $this->_conn->prepare($parentPersister->_getInsertSQL()); @@ -153,11 +153,11 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister // Execute insert on root table $paramIndex = 1; - + foreach ($insertData[$rootTableName] as $columnName => $value) { $rootTableStmt->bindValue($paramIndex++, $value, $this->_columnTypes[$columnName]); } - + $rootTableStmt->execute(); if ($isPostInsertId) { @@ -172,23 +172,23 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister foreach ($subTableStmts as $tableName => $stmt) { $data = isset($insertData[$tableName]) ? $insertData[$tableName] : array(); $paramIndex = 1; - + foreach ((array) $id as $idName => $idVal) { $type = isset($this->_columnTypes[$idName]) ? $this->_columnTypes[$idName] : Type::STRING; - + $stmt->bindValue($paramIndex++, $idVal, $type); } - + foreach ($data as $columnName => $value) { $stmt->bindValue($paramIndex++, $value, $this->_columnTypes[$columnName]); } - + $stmt->execute(); } } $rootTableStmt->closeCursor(); - + foreach ($subTableStmts as $stmt) { $stmt->closeCursor(); } @@ -220,7 +220,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $entity, $this->_quotedTableMap[$tableName], $data, $isVersioned && $versionedTable == $tableName ); } - + // Make sure the table with the version column is updated even if no columns on that // table were affected. if ($isVersioned && ! isset($updateData[$versionedTable])) { @@ -251,7 +251,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister } else { // Delete from all tables individually, starting from this class' table up to the root table. $this->_conn->delete($this->_class->getQuotedTableName($this->_platform), $id); - + foreach ($this->_class->parentClasses as $parentClass) { $this->_conn->delete( $this->_em->getClassMetadata($parentClass)->getQuotedTableName($this->_platform), $id @@ -270,16 +270,16 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister // Create the column list fragment only once if ($this->_selectColumnListSql === null) { - + $this->_rsm = new ResultSetMapping(); $this->_rsm->addEntityResult($this->_class->name, 'r'); - + // Add regular columns $columnList = ''; - + foreach ($this->_class->fieldMappings as $fieldName => $mapping) { if ($columnList != '') $columnList .= ', '; - + $columnList .= $this->_getSelectColumnSQL( $fieldName, isset($mapping['inherited']) ? $this->_em->getClassMetadata($mapping['inherited']) : $this->_class @@ -290,12 +290,12 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister foreach ($this->_class->associationMappings as $assoc2) { if ($assoc2['isOwningSide'] && $assoc2['type'] & ClassMetadata::TO_ONE) { $tableAlias = isset($assoc2['inherited']) ? $this->_getSQLTableAlias($assoc2['inherited']) : $baseTableAlias; - + foreach ($assoc2['targetToSourceKeyColumns'] as $srcColumn) { if ($columnList != '') $columnList .= ', '; - + $columnList .= $this->getSelectJoinColumnSQL( - $tableAlias, + $tableAlias, $srcColumn, isset($assoc2['inherited']) ? $assoc2['inherited'] : $this->_class->name ); @@ -309,23 +309,23 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $columnList .= ', ' . $tableAlias . '.' . $discrColumn; $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn); - + $this->_rsm->setDiscriminatorColumn('r', $resultColumnName); $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn); } // INNER JOIN parent tables $joinSql = ''; - + foreach ($this->_class->parentClasses as $parentClassName) { $parentClass = $this->_em->getClassMetadata($parentClassName); $tableAlias = $this->_getSQLTableAlias($parentClassName); $joinSql .= ' INNER JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; $first = true; - + foreach ($idColumns as $idColumn) { if ($first) $first = false; else $joinSql .= ' AND '; - + $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; } } @@ -339,7 +339,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister // Add subclass columns foreach ($subClass->fieldMappings as $fieldName => $mapping) { if (isset($mapping['inherited'])) continue; - + $columnList .= ', ' . $this->_getSelectColumnSQL($fieldName, $subClass); } @@ -348,9 +348,9 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister if ($assoc2['isOwningSide'] && $assoc2['type'] & ClassMetadata::TO_ONE && ! isset($assoc2['inherited'])) { foreach ($assoc2['targetToSourceKeyColumns'] as $srcColumn) { if ($columnList != '') $columnList .= ', '; - + $columnList .= $this->getSelectJoinColumnSQL( - $tableAlias, + $tableAlias, $srcColumn, isset($assoc2['inherited']) ? $assoc2['inherited'] : $subClass->name ); @@ -362,10 +362,10 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister // Add LEFT JOIN $joinSql .= ' LEFT JOIN ' . $subClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; $first = true; - + foreach ($idColumns as $idColumn) { if ($first) $first = false; else $joinSql .= ' AND '; - + $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; } } @@ -382,7 +382,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister } $lockSql = ''; - + if ($lockMode == LockMode::PESSIMISTIC_READ) { $lockSql = ' ' . $this->_platform->getReadLockSql(); } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) { @@ -408,29 +408,29 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister // INNER JOIN parent tables $joinSql = ''; - + foreach ($this->_class->parentClasses as $parentClassName) { $parentClass = $this->_em->getClassMetadata($parentClassName); $tableAlias = $this->_getSQLTableAlias($parentClassName); $joinSql .= ' INNER JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; $first = true; - + foreach ($idColumns as $idColumn) { if ($first) $first = false; else $joinSql .= ' AND '; - + $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; } } return 'FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $baseTableAlias . $joinSql; } - + /* Ensure this method is never called. This persister overrides _getSelectEntitiesSQL directly. */ protected function _getSelectColumnListSQL() { throw new \BadMethodCallException("Illegal invocation of ".__METHOD__."."); } - + /** {@inheritdoc} */ protected function _getInsertColumnList() { 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/lib/Doctrine/ORM/Query/Expr/Base.php b/lib/Doctrine/ORM/Query/Expr/Base.php index beac2bb9c..975d450cd 100644 --- a/lib/Doctrine/ORM/Query/Expr/Base.php +++ b/lib/Doctrine/ORM/Query/Expr/Base.php @@ -57,7 +57,7 @@ abstract class Base public function add($arg) { - if ( $arg !== null ) { + if ( $arg !== null || ($arg instanceof self && $arg->count() > 0) ) { // If we decide to keep Expr\Base instances, we can use this check if ( ! is_string($arg)) { $class = get_class($arg); 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)); } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1454Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1454Test.php new file mode 100644 index 000000000..eaf9dd3f9 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1454Test.php @@ -0,0 +1,69 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1454File'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1454Picture'), + )); + } catch (\Exception $ignored) { + + } + } + + public function testFailingCase() + { + $pic = new DDC1454Picture(); + $this->_em->getUnitOfWork()->getEntityState($pic); + } + +} + +/** + * @Entity + */ +class DDC1454Picture extends DDC1454File +{ + +} + +/** + * @Entity + * @InheritanceType("JOINED") + * @DiscriminatorColumn(name="discr", type="string") + * @DiscriminatorMap({"picture" = "DDC1454Picture"}) + */ +class DDC1454File +{ + /** + * @Column(name="file_id", type="integer") + * @Id + */ + public $fileId; + + public function __construct() { + $this->fileId = rand(); + } + + /** + * Get fileId + */ + public function getFileId() { + return $this->fileId; + } + +} \ No newline at end of file