diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index b1b24230b..6c8d0e5ff 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -57,6 +57,11 @@ abstract class AbstractQuery */ const HYDRATE_SINGLE_SCALAR = 4; + /** + * Very simple object hydrator (optimized for performance). + */ + const HYDRATE_SIMPLEOBJECT = 5; + /** * @var array The parameter map of this query. */ @@ -331,6 +336,26 @@ abstract class AbstractQuery return $this->_expireResultCache; } + /** + * Change the default fetch mode of an association for this query. + * + * $fetchMode can be one of ClassMetadata::FETCH_EAGER or ClassMetadata::FETCH_LAZY + * + * @param string $class + * @param string $assocName + * @param int $fetchMode + * @return AbstractQuery + */ + public function setFetchMode($class, $assocName, $fetchMode) + { + if ($fetchMode !== Mapping\ClassMetadata::FETCH_EAGER) { + $fetchMode = Mapping\ClassMetadata::FETCH_LAZY; + } + + $this->_hints['fetchMode'][$class][$assocName] = $fetchMode; + return $this; + } + /** * Defines the processing mode to be used during hydration / result set transformation. * diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 2839099fc..43b257788 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -679,6 +679,9 @@ class EntityManager implements ObjectManager case Query::HYDRATE_SINGLE_SCALAR: $hydrator = new Internal\Hydration\SingleScalarHydrator($this); break; + case Query::HYDRATE_SIMPLEOBJECT: + $hydrator = new Internal\Hydration\SimpleObjectHydrator($this); + break; default: if ($class = $this->config->getCustomHydrationMode($hydrationMode)) { $hydrator = new $class($this); diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 302cc6b54..a9697c15a 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -211,7 +211,16 @@ abstract class AbstractHydrator } if (isset($cache[$key]['isMetaColumn'])) { - $rowData[$dqlAlias][$cache[$key]['fieldName']] = $value; + if (!isset($rowData[$dqlAlias][$cache[$key]['fieldName']]) || $value !== null) { + $rowData[$dqlAlias][$cache[$key]['fieldName']] = $value; + } + continue; + } + + // in an inheritance hierachy the same field could be defined several times. + // We overwrite this value so long we dont have a non-null value, that value we keep. + // Per definition it cannot be that a field is defined several times and has several values. + if (isset($rowData[$dqlAlias][$cache[$key]['fieldName']]) && $value === null) { continue; } diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 202fdc7ff..a820832f5 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -59,6 +59,9 @@ class ObjectHydrator extends AbstractHydrator $this->_resultPointers = $this->_idTemplate = array(); $this->_resultCounter = 0; + if (!isset($this->_hints['deferEagerLoad'])) { + $this->_hints['deferEagerLoad'] = true; + } foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { $this->_identifierMap[$dqlAlias] = array(); @@ -108,11 +111,17 @@ class ObjectHydrator extends AbstractHydrator */ protected function _cleanup() { + $eagerLoad = (isset($this->_hints['deferEagerLoad'])) && $this->_hints['deferEagerLoad'] == true; + parent::_cleanup(); $this->_identifierMap = $this->_initializedCollections = $this->_existingCollections = $this->_resultPointers = array(); + + if ($eagerLoad) { + $this->_em->getUnitOfWork()->triggerEagerLoads(); + } } /** @@ -395,6 +404,10 @@ class ObjectHydrator extends AbstractHydrator $result[$key] = $element; $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $key; } + + if (isset($this->_hints['collection'])) { + $this->_hints['collection']->hydrateSet($key, $element); + } } else { if ($this->_rsm->isMixed) { $element = array(0 => $element); @@ -402,6 +415,10 @@ class ObjectHydrator extends AbstractHydrator $result[] = $element; $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; ++$this->_resultCounter; + + if (isset($this->_hints['collection'])) { + $this->_hints['collection']->hydrateAdd($element); + } } // Update result pointer diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php new file mode 100644 index 000000000..3524f89e9 --- /dev/null +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -0,0 +1,141 @@ +. + */ + + +namespace Doctrine\ORM\Internal\Hydration; + +use \PDO; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\DBAL\Types\Type; + +class SimpleObjectHydrator extends AbstractHydrator +{ + const REFRESH_ENTITY = 'doctrine_refresh_entity'; + + /** + * @var ClassMetadata + */ + private $class; + + private $declaringClasses = array(); + + protected function _hydrateAll() + { + $result = array(); + $cache = array(); + + while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { + $this->_hydrateRow($row, $cache, $result); + } + + $this->_em->getUnitOfWork()->triggerEagerLoads(); + + return $result; + } + + protected function _prepare() + { + if (count($this->_rsm->aliasMap) == 1) { + $this->class = $this->_em->getClassMetadata(reset($this->_rsm->aliasMap)); + if ($this->class->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { + foreach ($this->_rsm->declaringClasses AS $column => $class) { + $this->declaringClasses[$column] = $this->_em->getClassMetadata($class); + } + } + } else { + throw new \RuntimeException("Cannot use SimpleObjectHydrator with a ResultSetMapping not containing exactly one object result."); + } + if ($this->_rsm->scalarMappings) { + throw new \RuntimeException("Cannot use SimpleObjectHydrator with a ResultSetMapping that contains scalar mappings."); + } + } + + protected function _hydrateRow(array $sqlResult, array &$cache, array &$result) + { + $data = array(); + if ($this->class->inheritanceType == ClassMetadata::INHERITANCE_TYPE_NONE) { + foreach ($sqlResult as $column => $value) { + + if (!isset($cache[$column])) { + if (isset($this->_rsm->fieldMappings[$column])) { + $cache[$column]['name'] = $this->_rsm->fieldMappings[$column]; + $cache[$column]['field'] = true; + } else { + $cache[$column]['name'] = $this->_rsm->metaMappings[$column]; + } + } + + if (isset($cache[$column]['field'])) { + $value = Type::getType($this->class->fieldMappings[$cache[$column]['name']]['type']) + ->convertToPHPValue($value, $this->_platform); + } + $data[$cache[$column]['name']] = $value; + } + $entityName = $this->class->name; + } else { + $discrColumnName = $this->_platform->getSQLResultCasing($this->class->discriminatorColumn['name']); + $entityName = $this->class->discriminatorMap[$sqlResult[$discrColumnName]]; + unset($sqlResult[$discrColumnName]); + foreach ($sqlResult as $column => $value) { + if (!isset($cache[$column])) { + if (isset($this->_rsm->fieldMappings[$column])) { + $field = $this->_rsm->fieldMappings[$column]; + $class = $this->declaringClasses[$column]; + if ($class->name == $entityName || is_subclass_of($entityName, $class->name)) { + $cache[$column]['name'] = $field; + $cache[$column]['class'] = $class; + } + } else if (isset($this->_rsm->relationMap[$column])) { + if ($this->_rsm->relationMap[$column] == $entityName || is_subclass_of($entityName, $this->_rsm->relationMap[$column])) { + $cache[$column]['name'] = $field; + } + } else { + $cache[$column]['name'] = $this->_rsm->metaMappings[$column]; + } + } + + if (isset($cache[$column]['class'])) { + $value = Type::getType($cache[$column]['class']->fieldMappings[$cache[$column]['name']]['type']) + ->convertToPHPValue($value, $this->_platform); + } + + // the second and part is to prevent overwrites in case of multiple + // inheritance classes using the same property name (See AbstractHydrator) + if (isset($cache[$column]) && (!isset($data[$cache[$column]['name']]) || $value !== null)) { + $data[$cache[$column]['name']] = $value; + } + } + } + + if (isset($this->_hints[self::REFRESH_ENTITY])) { + $this->_hints[Query::HINT_REFRESH] = true; + $id = array(); + if ($this->_class->isIdentifierComposite) { + foreach ($this->_class->identifier as $fieldName) { + $id[$fieldName] = $data[$fieldName]; + } + } else { + $id = array($this->_class->identifier[0] => $data[$this->_class->identifier[0]]); + } + $this->_em->getUnitOfWork()->registerManaged($this->_hints[self::REFRESH_ENTITY], $id, $data); + } + + $result[] = $this->_em->getUnitOfWork()->createEntity($entityName, $data, $this->_hints); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php index bfe1e60d9..425da798a 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php @@ -28,24 +28,11 @@ use Doctrine\ORM\Mapping\ClassMetadata, * types in the hierarchy. * * @author Roman Borschel + * @author Benjamin Eberlei * @since 2.0 */ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister { - /** - * Map from column names to class metadata instances that declare the field the column is mapped to. - * - * @var array - */ - private $declaringClassMap = array(); - - /** - * Map from column names to class names that declare the field the association with join column is mapped to. - * - * @var array - */ - private $declaringJoinColumnMap = array(); - /** * {@inheritdoc} */ @@ -69,49 +56,12 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister /** * {@inheritdoc} */ - protected function _processSQLResult(array $sqlResult) - { - $data = array(); - $discrColumnName = $this->_platform->getSQLResultCasing($this->_class->discriminatorColumn['name']); - $entityName = $this->_class->discriminatorMap[$sqlResult[$discrColumnName]]; - unset($sqlResult[$discrColumnName]); - foreach ($sqlResult as $column => $value) { - $realColumnName = $this->_resultColumnNames[$column]; - if (isset($this->declaringClassMap[$column])) { - $class = $this->declaringClassMap[$column]; - if ($class->name == $entityName || is_subclass_of($entityName, $class->name)) { - $field = $class->fieldNames[$realColumnName]; - if (isset($data[$field])) { - $data[$realColumnName] = $value; - } else { - $data[$field] = Type::getType($class->fieldMappings[$field]['type']) - ->convertToPHPValue($value, $this->_platform); - } - } - } else if (isset($this->declaringJoinColumnMap[$column])) { - if ($this->declaringJoinColumnMap[$column] == $entityName || is_subclass_of($entityName, $this->declaringJoinColumnMap[$column])) { - $data[$realColumnName] = $value; - } - } else { - $data[$realColumnName] = $value; - } - } - - return array($entityName, $data); - } - - /** - * {@inheritdoc} - */ - protected function _getSelectColumnSQL($field, ClassMetadata $class) + protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r') { $columnName = $class->columnNames[$field]; - $sql = $this->_getSQLTableAlias($class->name) . '.' . $class->getQuotedColumnName($field, $this->_platform); + $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); - if ( ! isset($this->_resultColumnNames[$columnAlias])) { - $this->_resultColumnNames[$columnAlias] = $columnName; - $this->declaringClassMap[$columnAlias] = $class; - } + $this->_rsm->addFieldResult($alias, $columnAlias, $field, $class->name); return "$sql AS $columnAlias"; } @@ -120,10 +70,7 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister { $columnAlias = $joinColumnName . $this->_sqlAliasCounter++; $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); - if ( ! isset($this->_resultColumnNames[$resultColumnName])) { - $this->_resultColumnNames[$resultColumnName] = $joinColumnName; - $this->declaringJoinColumnMap[$resultColumnName] = $className; - } + $this->_rsm->addMetaResult('r', $resultColumnName, $joinColumnName); return $tableAlias . ".$joinColumnName AS $columnAlias"; } diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index c9ae5ddd9..6999f85ce 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -22,6 +22,7 @@ namespace Doctrine\ORM\Persisters; use PDO, Doctrine\DBAL\LockMode, Doctrine\DBAL\Types\Type, + Doctrine\DBAL\Connection, Doctrine\ORM\ORMException, Doctrine\ORM\OptimisticLockException, Doctrine\ORM\EntityManager, @@ -69,6 +70,7 @@ use PDO, * * @author Roman Borschel * @author Giorgio Sironi + * @author Benjamin Eberlei * @since 2.0 */ class BasicEntityPersister @@ -107,15 +109,15 @@ class BasicEntityPersister * @var array */ protected $_queuedInserts = array(); - + /** - * Case-sensitive mappings of column names as they appear in an SQL result set - * to column names as they are defined in the mapping. This is necessary because different - * RDBMS vendors return column names in result sets in different casings. + * ResultSetMapping that is used for all queries. Is generated lazily once per request. * - * @var array + * TODO: Evaluate Caching in combination with the other cached SQL snippets. + * + * @var Query\ResultSetMapping */ - protected $_resultColumnNames = array(); + protected $_rsm; /** * The map of column names to DBAL mapping types of all prepared columns used @@ -142,6 +144,14 @@ class BasicEntityPersister * @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. @@ -557,10 +567,18 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, $lockMode); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt->closeCursor(); + + if ($entity !== null) { + $hints[Query::HINT_REFRESH] = true; + } - return $this->_createEntity($result, $entity, $hints); + if ($this->_selectJoinSql) { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + } else { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_SIMPLEOBJECT); + } + $entities = $hydrator->hydrateAll($stmt, $this->_rsm, $hints); + return $entities ? $entities[0] : null; } /** @@ -577,6 +595,10 @@ class BasicEntityPersister */ public function loadOneToOneEntity(array $assoc, $sourceEntity, $targetEntity, array $identifier = array()) { + if ($foundEntity = $this->_em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity'])) { + return $foundEntity; + } + $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); if ($assoc['isOwningSide']) { @@ -641,79 +663,9 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($id); list($params, $types) = $this->expandParameters($id); $stmt = $this->_conn->executeQuery($sql, $params, $types); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt->closeCursor(); - - $metaColumns = array(); - $newData = array(); - - // Refresh simple state - foreach ($result as $column => $value) { - $column = $this->_resultColumnNames[$column]; - if (isset($this->_class->fieldNames[$column])) { - $fieldName = $this->_class->fieldNames[$column]; - $newValue = $this->_conn->convertToPHPValue($value, $this->_class->fieldMappings[$fieldName]['type']); - $this->_class->reflFields[$fieldName]->setValue($entity, $newValue); - $newData[$fieldName] = $newValue; - } else { - $metaColumns[$column] = $value; - } - } - - // Refresh associations - foreach ($this->_class->associationMappings as $field => $assoc) { - $value = $this->_class->reflFields[$field]->getValue($entity); - if ($assoc['type'] & ClassMetadata::TO_ONE) { - if ($value instanceof Proxy && ! $value->__isInitialized__) { - continue; // skip uninitialized proxies - } - - if ($assoc['isOwningSide']) { - $joinColumnValues = array(); - foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { - if ($metaColumns[$srcColumn] !== null) { - $joinColumnValues[$targetColumn] = $metaColumns[$srcColumn]; - } - } - if ( ! $joinColumnValues && $value !== null) { - $this->_class->reflFields[$field]->setValue($entity, null); - $newData[$field] = null; - } else if ($value !== null) { - // Check identity map first, if the entity is not there, - // place a proxy in there instead. - $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); - if ($found = $this->_em->getUnitOfWork()->tryGetById($joinColumnValues, $targetClass->rootEntityName)) { - $this->_class->reflFields[$field]->setValue($entity, $found); - // Complete inverse side, if necessary. - if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) { - $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']]; - $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($found, $entity); - } - $newData[$field] = $found; - } else { - // FIXME: What is happening with subClassees here? - $proxy = $this->_em->getProxyFactory()->getProxy($assoc['targetEntity'], $joinColumnValues); - $this->_class->reflFields[$field]->setValue($entity, $proxy); - $newData[$field] = $proxy; - $this->_em->getUnitOfWork()->registerManaged($proxy, $joinColumnValues, array()); - } - } - } else { - // Inverse side of 1-1/1-x can never be lazy. - //$newData[$field] = $assoc->load($entity, null, $this->_em); - $newData[$field] = $this->_em->getUnitOfWork()->getEntityPersister($assoc['targetEntity']) - ->loadOneToOneEntity($assoc, $entity, null); - } - } else if ($value instanceof PersistentCollection && $value->isInitialized()) { - $value->setInitialized(false); - // no matter if dirty or non-dirty entities are already loaded, smoke them out! - // the beauty of it being, they are still in the identity map - $value->unwrap()->clear(); - $newData[$field] = $value; - } - } - - $this->_em->getUnitOfWork()->setOriginalEntityData($entity, $newData); + + $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); @@ -736,14 +688,13 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria); list($params, $types) = $this->expandParameters($criteria); $stmt = $this->_conn->executeQuery($sql, $params, $types); - $result = $stmt->fetchAll(PDO::FETCH_ASSOC); - $stmt->closeCursor(); - foreach ($result as $row) { - $entities[] = $this->_createEntity($row); + if ($this->_selectJoinSql) { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + } else { + $hydrator = $this->_em->newHydrator(Query::HYDRATE_SIMPLEOBJECT); } - - return $entities; + return $hydrator->hydrateAll($stmt, $this->_rsm, array('deferEagerLoads' => true)); } /** @@ -770,19 +721,17 @@ class BasicEntityPersister */ private function loadArrayFromStatement($assoc, $stmt) { - $entities = array(); + $hints = array('deferEagerLoads' => true); + if (isset($assoc['indexBy'])) { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entity = $this->_createEntity($result); - $entities[$this->_class->reflFields[$assoc['indexBy']]->getValue($entity)] = $entity; - } + $rsm = clone ($this->_rsm); // this is necessary because the "default rsm" should be changed. + $rsm->addIndexBy('r', $assoc['indexBy']); } else { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entities[] = $this->_createEntity($result); - } + $rsm = $this->_rsm; } - $stmt->closeCursor(); - return $entities; + + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + return $hydrator->hydrateAll($stmt, $rsm, $hints); } /** @@ -793,18 +742,18 @@ class BasicEntityPersister * @param PersistentCollection $coll */ private function loadCollectionFromStatement($assoc, $stmt, $coll) - { + { + $hints = array('deferEagerLoads' => true, 'collection' => $coll); + if (isset($assoc['indexBy'])) { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $entity = $this->_createEntity($result); - $coll->hydrateSet($this->_class->reflFields[$assoc['indexBy']]->getValue($entity), $entity); - } + $rsm = clone ($this->_rsm); // this is necessary because the "default rsm" should be changed. + $rsm->addIndexBy('r', $assoc['indexBy']); } else { - while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { - $coll->hydrateAdd($this->_createEntity($result)); - } + $rsm = $this->_rsm; } - $stmt->closeCursor(); + + $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT); + $hydrator->hydrateAll($stmt, $rsm, $hints); } /** @@ -827,7 +776,6 @@ class BasicEntityPersister { $criteria = array(); $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); - $joinTableConditions = array(); if ($assoc['isOwningSide']) { foreach ($assoc['relationToSourceKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) { if ($sourceClass->containsForeignIdentifier) { @@ -873,72 +821,6 @@ class BasicEntityPersister return $this->_conn->executeQuery($sql, $params, $types); } - /** - * Creates or fills a single entity object from an SQL result. - * - * @param $result The SQL result. - * @param object $entity The entity object to fill, if any. - * @param array $hints Hints for entity creation. - * @return object The filled and managed entity object or NULL, if the SQL result is empty. - */ - private function _createEntity($result, $entity = null, array $hints = array()) - { - if ($result === false) { - return null; - } - - list($entityName, $data) = $this->_processSQLResult($result); - - if ($entity !== null) { - $hints[Query::HINT_REFRESH] = true; - $id = array(); - if ($this->_class->isIdentifierComposite) { - foreach ($this->_class->identifier as $fieldName) { - $id[$fieldName] = $data[$fieldName]; - } - } else { - $id = array($this->_class->identifier[0] => $data[$this->_class->identifier[0]]); - } - $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data); - } - - return $this->_em->getUnitOfWork()->createEntity($entityName, $data, $hints); - } - - /** - * Processes an SQL result set row that contains data for an entity of the type - * this persister is responsible for. - * - * Subclasses are supposed to override this method if they need to change the - * hydration procedure for entities loaded through basic find operations or - * lazy-loading (not DQL). - * - * @param array $sqlResult The SQL result set row to process. - * @return array A tuple where the first value is the actual type of the entity and - * the second value the prepared data of the entity (a map from field - * names to values). - */ - protected function _processSQLResult(array $sqlResult) - { - $data = array(); - foreach ($sqlResult as $column => $value) { - $column = $this->_resultColumnNames[$column]; - if (isset($this->_class->fieldNames[$column])) { - $field = $this->_class->fieldNames[$column]; - if (isset($data[$field])) { - $data[$column] = $value; - } else { - $data[$field] = Type::getType($this->_class->fieldMappings[$field]['type']) - ->convertToPHPValue($value, $this->_platform); - } - } else { - $data[$column] = $value; - } - } - - return array($this->_class->name, $data); - } - /** * Gets the SELECT SQL to select one or more entities by a set of field criteria. * @@ -970,7 +852,7 @@ class BasicEntityPersister return $this->_platform->modifyLimitQuery('SELECT ' . $this->_getSelectColumnListSQL() . $this->_platform->appendLockHint(' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($this->_class->name), $lockMode) - . $joinSql + . $this->_selectJoinSql . $joinSql . ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql, $limit, $offset) . $lockSql; @@ -1023,6 +905,8 @@ class BasicEntityPersister } $columnList = ''; + $this->_rsm = new Query\ResultSetMapping(); + $this->_rsm->addEntityResult($this->_class->name, 'r'); // r for root // Add regular columns to select list foreach ($this->_class->fieldNames as $field) { @@ -1030,16 +914,53 @@ class BasicEntityPersister $columnList .= $this->_getSelectColumnSQL($field, $this->_class); } - foreach ($this->_class->associationMappings as $assoc) { - if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { - foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + $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. + if ($assoc['isOwningSide']) { + $this->_selectJoinSql .= ' ' . $eagerEntity->table['name'] . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; + + foreach ($assoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { + $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.'.$sourceCol.' = ' . + $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias) . '.'.$targetCol.' '; + } + } else { + $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); + $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']); + + $this->_selectJoinSql .= ' ' . $eagerEntity->table['name'] . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; - $columnAlias = $srcColumn . $this->_sqlAliasCounter++; - $columnList .= $this->_getSQLTableAlias($this->_class->name) . ".$srcColumn AS $columnAlias"; - $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); - if ( ! isset($this->_resultColumnNames[$resultColumnName])) { - $this->_resultColumnNames[$resultColumnName] = $srcColumn; + foreach ($owningAssoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { + $this->_selectJoinSql .= $this->_getSQLTableAlias($owningAssoc['sourceEntity'], $assocAlias) . '.'.$sourceCol.' = ' . + $this->_getSQLTableAlias($owningAssoc['targetEntity']) . '.' . $targetCol . ' '; } } } @@ -1049,6 +970,22 @@ class BasicEntityPersister return $this->_selectColumnListSql; } + + 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++; + $columnList .= $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) ) . ".$srcColumn AS $columnAlias"; + $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addMetaResult($alias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); + } + } + return $columnList; + } /** * Gets the SQL join fragment used when selecting entities from a @@ -1152,15 +1089,14 @@ class BasicEntityPersister * @param string $field The field name. * @param ClassMetadata $class The class that declares this field. The table this class is * mapped to must own the column for the given field. + * @param string $alias */ - protected function _getSelectColumnSQL($field, ClassMetadata $class) + protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r') { $columnName = $class->columnNames[$field]; - $sql = $this->_getSQLTableAlias($class->name) . '.' . $class->getQuotedColumnName($field, $this->_platform); + $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform); $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); - if ( ! isset($this->_resultColumnNames[$columnAlias])) { - $this->_resultColumnNames[$columnAlias] = $columnName; - } + $this->_rsm->addFieldResult($alias, $columnAlias, $field); return "$sql AS $columnAlias"; } @@ -1172,14 +1108,18 @@ class BasicEntityPersister * @return string The SQL table alias. * @todo Reconsider. Binding table aliases to class names is not such a good idea. */ - protected function _getSQLTableAlias($className) + protected function _getSQLTableAlias($className, $assocName = '') { + if ($assocName) { + $className .= '#'.$assocName; + } + if (isset($this->_sqlTableAliases[$className])) { return $this->_sqlTableAliases[$className]; } $tableAlias = 't' . $this->_sqlAliasCounter++; - $this->_sqlTableAliases[$className] = $tableAlias; + $this->_sqlTableAliases[$className] = $tableAlias; return $tableAlias; } @@ -1265,7 +1205,7 @@ class BasicEntityPersister } else { throw ORMException::unrecognizedField($field); } - $conditionSql .= ' = ?'; + $conditionSql .= (is_array($value)) ? ' IN (?)' : ' = ?'; } return $conditionSql; } @@ -1330,6 +1270,7 @@ class BasicEntityPersister $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset); list($params, $types) = $this->expandParameters($criteria); + return $this->_conn->executeQuery($sql, $params, $types); } @@ -1348,6 +1289,10 @@ class BasicEntityPersister if (isset($this->_class->fieldMappings[$field])) { $type = Type::getType($this->_class->fieldMappings[$field]['type'])->getBindingType(); } + if (is_array($value)) { + $type += Connection::ARRAY_PARAM_OFFSET; + } + $params[] = $value; $types[] = $type; } diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 8aa784001..2490085cf 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -21,13 +21,15 @@ namespace Doctrine\ORM\Persisters; use Doctrine\ORM\ORMException, Doctrine\ORM\Mapping\ClassMetadata, - Doctrine\DBAL\LockMode; + Doctrine\DBAL\LockMode, + Doctrine\ORM\Query\ResultSetMapping; /** * The joined subclass persister maps a single entity instance to several tables in the * database as it is defined by the Class Table Inheritance strategy. * * @author Roman Borschel + * @author Benjamin Eberlei * @since 2.0 * @see http://martinfowler.com/eaaCatalog/classTableInheritance.html */ @@ -243,6 +245,10 @@ 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) { @@ -278,7 +284,8 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister } $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn); - $this->_resultColumnNames[$resultColumnName] = $discrColumn; + $this->_rsm->setDiscriminatorColumn('r', $discrColumn); + $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn); } // INNER JOIN parent tables diff --git a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php index 0ffa93826..bf9104580 100644 --- a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php +++ b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php @@ -26,6 +26,7 @@ use Doctrine\ORM\Mapping\ClassMetadata; * SINGLE_TABLE strategy. * * @author Roman Borschel + * @author Benjamin Eberlei * @since 2.0 * @link http://martinfowler.com/eaaCatalog/singleTableInheritance.html */ @@ -48,7 +49,8 @@ class SingleTablePersister extends AbstractEntityInheritancePersister $rootClass = $this->_em->getClassMetadata($this->_class->rootEntityName); $tableAlias = $this->_getSQLTableAlias($rootClass->name); $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn); - $this->_resultColumnNames[$resultColumnName] = $discrColumn; + $this->_rsm->setDiscriminatorColumn('r', $discrColumn); + $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn); // Append subclass columns foreach ($this->_class->subClasses as $subClassName) { @@ -86,9 +88,9 @@ class SingleTablePersister extends AbstractEntityInheritancePersister } /** {@inheritdoc} */ - protected function _getSQLTableAlias($className) + protected function _getSQLTableAlias($className, $assocName = '') { - return parent::_getSQLTableAlias($this->_class->rootEntityName); + return parent::_getSQLTableAlias($this->_class->rootEntityName, $assocName); } /** {@inheritdoc} */ diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index d0f5bc022..db29d9c64 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -218,6 +218,13 @@ class UnitOfWork implements PropertyChangedListener //private $_readOnlyObjects = array(); + /** + * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested. + * + * @var array + */ + private $eagerLoadingEntities = array(); + /** * Initializes a new UnitOfWork instance, bound to the given EntityManager. * @@ -1886,6 +1893,9 @@ class UnitOfWork implements PropertyChangedListener $class->reflFields[$field]->setValue($entity, $value); } } + + // Loading the entity right here, if its in the eager loading map get rid of it there. + unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]); // Properly initialize any unfetched associations, if partial objects are not allowed. if ( ! isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { @@ -1900,6 +1910,7 @@ class UnitOfWork implements PropertyChangedListener if ($assoc['type'] & ClassMetadata::TO_ONE) { if ($assoc['isOwningSide']) { $associatedId = array(); + // TODO: Is this even computed right in all cases of composite keys? foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null; if ($joinColumnValue !== null) { @@ -1915,6 +1926,10 @@ class UnitOfWork implements PropertyChangedListener $class->reflFields[$field]->setValue($entity, null); $this->originalEntityData[$oid][$field] = null; } else { + if (!isset($hints['fetchMode'][$class->name][$field])) { + $hints['fetchMode'][$class->name][$field] = $assoc['fetch']; + } + // Foreign key is set // Check identity map first // FIXME: Can break easily with composite keys if join column values are in @@ -1922,16 +1937,38 @@ class UnitOfWork implements PropertyChangedListener $relatedIdHash = implode(' ', $associatedId); if (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])) { $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash]; + + // if this is an uninitialized proxy, we are deferring eager loads, + // this association is marked as eager fetch, and its an uninitialized proxy (wtf!) + // then we cann append this entity for eager loading! + if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER && + isset($hints['deferEagerLoad']) && + !$targetClass->isIdentifierComposite && + $newValue instanceof Proxy && + $newValue->__isInitialized__ === false) { + + $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); + } } else { if ($targetClass->subClasses) { - // If it might be a subtype, it can not be lazy + // If it might be a subtype, it can not be lazy. There isn't even + // a way to solve this with deferred eager loading, which means putting + // an entity with subclasses at a *-to-one location is really bad! (performance-wise) $newValue = $this->getEntityPersister($assoc['targetEntity']) ->loadOneToOneEntity($assoc, $entity, null, $associatedId); } else { - if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { - // TODO: Maybe it could be optimized to do an eager fetch with a JOIN inside - // the persister instead of this rather unperformant approach. - $newValue = $this->em->find($assoc['targetEntity'], $associatedId); + // Deferred eager load only works for single identifier classes + + if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER) { + if (isset($hints['deferEagerLoad']) && !$targetClass->isIdentifierComposite) { + // TODO: Is there a faster approach? + $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); + + $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); + } else { + // TODO: This is very imperformant, ignore it? + $newValue = $this->em->find($assoc['targetEntity'], $associatedId); + } } else { $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); } @@ -1945,6 +1982,11 @@ class UnitOfWork implements PropertyChangedListener } $this->originalEntityData[$oid][$field] = $newValue; $class->reflFields[$field]->setValue($entity, $newValue); + + if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) { + $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']]; + $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity); + } } } else { // Inverse side of x-to-one can never be lazy @@ -1955,10 +1997,10 @@ class UnitOfWork implements PropertyChangedListener // Inject collection $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection); $pColl->setOwner($entity, $assoc); - + $reflField = $class->reflFields[$field]; $reflField->setValue($entity, $pColl); - + if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { $this->loadCollection($pColl); $pColl->takeSnapshot(); @@ -1982,6 +2024,25 @@ class UnitOfWork implements PropertyChangedListener return $entity; } + /** + * @return void + */ + public function triggerEagerLoads() + { + if (!$this->eagerLoadingEntities) { + return; + } + + // avoid infinite recursion + $eagerLoadingEntities = $this->eagerLoadingEntities; + $this->eagerLoadingEntities = array(); + + foreach ($eagerLoadingEntities AS $entityName => $ids) { + $class = $this->em->getClassMetadata($entityName); + $this->getEntityPersister($entityName)->loadAll(array_combine($class->identifier, array(array_values($ids)))); + } + } + /** * Initializes (loads) an uninitialized persistent collection of an entity. * diff --git a/lib/vendor/doctrine-dbal b/lib/vendor/doctrine-dbal index 556351d9d..9cd6df38d 160000 --- a/lib/vendor/doctrine-dbal +++ b/lib/vendor/doctrine-dbal @@ -1 +1 @@ -Subproject commit 556351d9d6b4a33506f2c1535cccee34faa65d62 +Subproject commit 9cd6df38d841abb4719286ea35a1b37aa2679f8d diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index 319d5bb50..077ee4a7c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -35,6 +35,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\AdvancedDqlQueryTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToOneUnidirectionalAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToOneBidirectionalAssociationTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToOneEagerLoadingTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToManyBidirectionalAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToManyUnidirectionalAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ManyToManyBasicAssociationTest'); diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 7a7da0c7a..b6ca444ca 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -946,4 +946,35 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertNull($this->_em->find(get_class($ph), $ph->phonenumber)->getUser()); } + + /** + * @group DDC-952 + */ + public function testManyToOneFetchModeQuery() + { + $user = new CmsUser(); + $user->username = "beberlei"; + $user->name = "Benjamin E."; + $user->status = 'active'; + + $article = new CmsArticle(); + $article->topic = "foo"; + $article->text = "bar"; + $article->user = $user; + + $this->_em->persist($article); + $this->_em->persist($user); + $this->_em->flush(); + $this->_em->clear(); + + $qc = $this->getCurrentQueryCount(); + $dql = "SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a WHERE a.id = ?1"; + $article = $this->_em->createQuery($dql) + ->setParameter(1, $article->id) + ->setFetchMode('Doctrine\Tests\Models\CMS\CmsArticle', 'user', \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER) + ->getSingleResult(); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $article->user, "It IS a proxy, ..."); + $this->assertTrue($article->user->__isInitialized__, "...but its initialized!"); + $this->assertEquals($qc+2, $this->getCurrentQueryCount()); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php index a82b09a47..f344ff159 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityRepositoryTest.php @@ -172,10 +172,10 @@ class EntityRepositoryTest extends \Doctrine\Tests\OrmFunctionalTestCase $userId = $user->id; - $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId); + $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $userId); $this->setExpectedException('Doctrine\ORM\OptimisticLockException'); - $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $userId, \Doctrine\DBAL\LockMode::OPTIMISTIC); + $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $userId, \Doctrine\DBAL\LockMode::OPTIMISTIC); } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index 031061b84..4bf010602 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -196,8 +196,8 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(2, count($someUsers)); $this->assertEquals(2, count($otherUsers)); - // +2 queries executed by slice, +4 are executed by EAGER fetching of User Address. - $this->assertEquals($queryCount + 2 + 4, $this->getCurrentQueryCount()); + // +2 queries executed by slice + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount(), "Slicing two parts should only execute two additional queries."); } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php new file mode 100644 index 000000000..044a17381 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -0,0 +1,198 @@ +_em); + try { + $schemaTool->createSchema(array( + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\Train'), + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\TrainDriver'), + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\Waggon'), + )); + } catch(\Exception $e) {} + } + + public function testEagerLoadOneToOneOwningSide() + { + $train = new Train(); + $driver = new TrainDriver("Benjamin"); + $waggon = new Waggon(); + + $train->setDriver($driver); + $train->addWaggon($waggon); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $train = $this->_em->find(get_class($train), $train->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $train->driver); + $this->assertEquals("Benjamin", $train->driver->name); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } + + public function testEagerLoadOneToOneNullOwningSide() + { + $train = new Train(); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $train = $this->_em->find(get_class($train), $train->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $train->driver); + $this->assertNull($train->driver); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } + + public function testEagerLoadOneToOneInverseSide() + { + $train = new Train(); + $driver = new TrainDriver("Benjamin"); + $train->setDriver($driver); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $driver = $this->_em->find(get_class($driver), $driver->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $driver->train); + $this->assertNotNull($driver->train); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } + + public function testEagerLoadOneToOneNullInverseSide() + { + $driver = new TrainDriver("Dagny Taggert"); + + $this->_em->persist($driver); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertNull($driver->train); + + $sqlCount = count($this->_sqlLoggerStack->queries); + + $driver = $this->_em->find(get_class($driver), $driver->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $driver->train); + $this->assertNull($driver->train); + + $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); + } + + public function testEagerLoadManyToOne() + { + $train = new Train(); + $waggon = new Waggon(); + $train->addWaggon($waggon); + + $this->_em->persist($train); // cascades + $this->_em->flush(); + $this->_em->clear(); + + $waggon = $this->_em->find(get_class($waggon), $waggon->id); + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $waggon->train); + $this->assertNotNull($waggon->train); + } +} + +/** + * @Entity + */ +class Train +{ + /** + * @id @column(type="integer") @generatedValue + * @var int + */ + public $id; + /** + * Owning side + * @OneToOne(targetEntity="TrainDriver", inversedBy="train", fetch="EAGER", cascade={"persist"}) + */ + public $driver; + /** + * @oneToMany(targetEntity="Waggon", mappedBy="train", cascade={"persist"}) + */ + public $waggons; + + public function __construct() + { + $this->waggons = new \Doctrine\Common\Collections\ArrayCollection(); + } + + public function setDriver(TrainDriver $driver) + { + $this->driver = $driver; + $driver->setTrain($this); + } + + public function addWaggon(Waggon $w) + { + $w->setTrain($this); + $this->waggons[] = $w; + } +} + +/** + * @Entity + */ +class TrainDriver +{ + /** @Id @Column(type="integer") @GeneratedValue */ + public $id; + /** @column(type="string") */ + public $name; + /** + * Inverse side + * @OneToOne(targetEntity="Train", mappedBy="driver", fetch="EAGER") + */ + public $train; + + public function __construct($name) + { + $this->name = $name; + } + + public function setTrain(Train $t) + { + $this->train = $t; + } +} + +/** + * @Entity + */ +class Waggon +{ + /** @id @generatedValue @column(type="integer") */ + public $id; + /** @ManyToOne(targetEntity="Train", inversedBy="waggons", fetch="EAGER") */ + public $train; + + public function setTrain($train) + { + $this->train = $train; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index c753960f8..c4a0beda0 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\CMS\CmsUser, Doctrine\Tests\Models\CMS\CmsArticle; +use Doctrine\ORM\Mapping\ClassMetadata; require_once __DIR__ . '/../../TestInit.php'; @@ -313,4 +314,34 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertTrue($result[0]->user instanceof \Doctrine\ORM\Proxy\Proxy); $this->assertFalse($result[0]->user->__isInitialized__); } + + /** + * @group DDC-952 + */ + public function testEnableFetchEagerMode() + { + for ($i = 0; $i < 10; $i++) { + $article = new CmsArticle; + $article->topic = "dr. dolittle"; + $article->text = "Once upon a time ..."; + $author = new CmsUser; + $author->name = "anonymous"; + $author->username = "anon".$i; + $author->status = "here"; + $article->user = $author; + $this->_em->persist($author); + $this->_em->persist($article); + } + $this->_em->flush(); + $this->_em->clear(); + + $articles = $this->_em->createQuery('select a from Doctrine\Tests\Models\CMS\CmsArticle a') + ->setFetchMode('Doctrine\Tests\Models\CMS\CmsArticle', 'user', ClassMetadata::FETCH_EAGER) + ->getResult(); + + $this->assertEquals(10, count($articles)); + foreach ($articles AS $article) { + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $article); + } + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php index d37442171..3e66e0b12 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php @@ -42,7 +42,7 @@ class ReferenceProxyTest extends \Doctrine\Tests\OrmFunctionalTestCase { $id = $this->createProduct(); - $productProxy = $this->_factory->getProxy('Doctrine\Tests\Models\ECommerce\ECommerceProduct', array('id' => $id)); + $productProxy = $this->_em->getReference('Doctrine\Tests\Models\ECommerce\ECommerceProduct', array('id' => $id)); $this->assertEquals('Doctrine Cookbook', $productProxy->getName()); } diff --git a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php index ca3856679..7271f42c3 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php @@ -2,6 +2,8 @@ namespace Doctrine\Tests\ORM\Functional; +use Doctrine\ORM\Mapping\ClassMetadata; + require_once __DIR__ . '/../../TestInit.php'; class SingleTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase @@ -349,4 +351,20 @@ class SingleTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase $ref = $this->_em->getReference('Doctrine\Tests\Models\Company\CompanyFixContract', $this->fix->getId()); $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $ref, "A proxy can be generated only if no subclasses exists for the requested reference."); } + + /** + * @group DDC-952 + */ + public function testEagerLoadInheritanceHierachy() + { + $this->loadFullFixture(); + + $dql = 'SELECT f FROM Doctrine\Tests\Models\Company\CompanyFixContract f WHERE f.id = ?1'; + $contract = $this->_em->createQuery($dql) + ->setFetchMode('Doctrine\Tests\Models\Company\CompanyFixContract', 'salesPerson', ClassMetadata::FETCH_EAGER) + ->setParameter(1, $this->fix->getId()) + ->getSingleResult(); + + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $contract->getSalesPerson()); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php b/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php index 9ed8f0821..8b37a02ba 100644 --- a/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/StandardEntityPersisterTest.php @@ -33,12 +33,14 @@ class StandardEntityPersisterTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->persist($customer); $this->_em->flush(); $this->_em->clear(); + $cardId = $cart->getId(); unset($cart); $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\ECommerce\ECommerceCart'); $persister = $this->_em->getUnitOfWork()->getEntityPersister('Doctrine\Tests\Models\ECommerce\ECommerceCart'); $newCart = new ECommerceCart(); + $this->_em->getUnitOfWork()->registerManaged($newCart, array('id' => $cardId), array()); $persister->load(array('customer_id' => $customer->getId()), $newCart, $class->associationMappings['customer']); $this->assertEquals('Credit card', $newCart->getPayment()); } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1050Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1050Test.php new file mode 100644 index 000000000..82e9590c0 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1050Test.php @@ -0,0 +1,37 @@ +markTestSkipped('performance skipped'); + $this->useModelSet('cms'); + parent::setUp(); + } + + public function testPerformance() + { + for ($i = 2; $i < 10000; ++$i) { + $user = new \Doctrine\Tests\Models\CMS\CmsUser(); + $user->status = 'developer'; + $user->username = 'jwage'+$i; + $user->name = 'Jonathan'; + $this->_em->persist($user); + } + $this->_em->flush(); + $this->_em->clear(); + + $s = microtime(true); + $users = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->findAll(); + $e = microtime(true); + echo __FUNCTION__ . " - " . ($e - $s) . " seconds" . PHP_EOL; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php index d375f408c..d51bdd361 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php @@ -21,6 +21,11 @@ class DDC633Test extends \Doctrine\Tests\OrmFunctionalTestCase } } + /** + * @group DDC-633 + * @group DDC-952 + * @group DDC-914 + */ public function testOneToOneEager() { $app = new DDC633Appointment(); @@ -35,7 +40,35 @@ class DDC633Test extends \Doctrine\Tests\OrmFunctionalTestCase $eagerAppointment = $this->_em->find(__NAMESPACE__ . '\DDC633Appointment', $app->id); - $this->assertNotType('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); + // Eager loading of one to one leads to fetch-join + $this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); + $this->assertTrue($this->_em->contains($eagerAppointment->patient)); + } + + /** + * @group DDC-633 + * @group DDC-952 + */ + public function testDQLDeferredEagerLoad() + { + for ($i = 0; $i < 10; $i++) { + $app = new DDC633Appointment(); + $pat = new DDC633Patient(); + $app->patient = $pat; + $pat->appointment = $app; + + $this->_em->persist($app); + $this->_em->persist($pat); + } + $this->_em->flush(); + $this->_em->clear(); + + $appointments = $this->_em->createQuery("SELECT a FROM " . __NAMESPACE__ . "\DDC633Appointment a")->getResult(); + + foreach ($appointments AS $eagerAppointment) { + $this->assertType('Doctrine\ORM\Proxy\Proxy', $eagerAppointment->patient); + $this->assertTrue($eagerAppointment->patient->__isInitialized__, "Proxy should already be initialized due to eager loading!"); + } } } diff --git a/tests/Doctrine/Tests/ORM/Functional/TypeTest.php b/tests/Doctrine/Tests/ORM/Functional/TypeTest.php index 8ecee7ac7..a2a738b56 100644 --- a/tests/Doctrine/Tests/ORM/Functional/TypeTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/TypeTest.php @@ -119,7 +119,7 @@ class TypeTest extends \Doctrine\Tests\OrmFunctionalTestCase $dateTimeDb = $this->_em->find('Doctrine\Tests\Models\Generic\DateTimeModel', $dateTime->id); - $this->assertInstanceOf('DateTime', $dateTime->datetime); + $this->assertInstanceOf('DateTime', $dateTimeDb->datetime); $this->assertEquals('2009-10-02 20:10:52', $dateTimeDb->datetime->format('Y-m-d H:i:s')); } diff --git a/tests/Doctrine/Tests/ORM/Performance/InheritancePersisterPerformanceTest.php b/tests/Doctrine/Tests/ORM/Performance/InheritancePersisterPerformanceTest.php new file mode 100644 index 000000000..01113f21b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Performance/InheritancePersisterPerformanceTest.php @@ -0,0 +1,63 @@ +useModelSet('company'); + parent::setUp(); + } + + public function testCompanyContract() + { + $person = new \Doctrine\Tests\Models\Company\CompanyEmployee(); + $person->setName('Poor Sales Guy'); + $person->setDepartment('Sales'); + $person->setSalary(100); + $this->_em->persist($person); + + for ($i = 0; $i < 33; $i++) { + $fix = new \Doctrine\Tests\Models\Company\CompanyFixContract(); + $fix->setFixPrice(1000); + $fix->setSalesPerson($person); + $fix->markCompleted(); + $this->_em->persist($fix); + + $flex = new \Doctrine\Tests\Models\Company\CompanyFlexContract(); + $flex->setSalesPerson($person); + $flex->setHoursWorked(100); + $flex->setPricePerHour(100); + $flex->markCompleted(); + $this->_em->persist($flex); + + $ultra = new \Doctrine\Tests\Models\Company\CompanyFlexUltraContract(); + $ultra->setSalesPerson($person); + $ultra->setHoursWorked(150); + $ultra->setPricePerHour(150); + $ultra->setMaxPrice(7000); + $this->_em->persist($ultra); + } + + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $contracts = $this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyContract')->findAll(); + echo "99 CompanyContract: " . number_format(microtime(true) - $start, 6) . "\n"; + $this->assertEquals(99, count($contracts)); + + $this->_em->clear(); + + $start = microtime(true); + $contracts = $this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyContract')->findAll(); + echo "99 CompanyContract: " . number_format(microtime(true) - $start, 6) . "\n"; + $this->assertEquals(99, count($contracts)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Performance/PersisterPerformanceTest.php b/tests/Doctrine/Tests/ORM/Performance/PersisterPerformanceTest.php new file mode 100644 index 000000000..bbd4f445f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Performance/PersisterPerformanceTest.php @@ -0,0 +1,118 @@ +useModelSet('cms'); + parent::setUp(); + } + + public function testFindCmsArticle() + { + $author = new CmsUser(); + $author->name = "beberlei"; + $author->status = "active"; + $author->username = "beberlei"; + $this->_em->persist($author); + + $ids = array(); + for ($i = 0; $i < 100; $i++) { + $article = new CmsArticle(); + $article->text = "foo"; + $article->topic = "bar"; + $article->user = $author; + $this->_em->persist($article); + $ids[] = $article; + } + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->findAll(); + echo "100 CmsArticle findAll(): " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->findAll(); + echo "100 CmsArticle findAll(): " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + for ($i = 0; $i < 100; $i++) { + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->find($ids[$i]->id); + } + echo "100 CmsArticle find(): " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + for ($i = 0; $i < 100; $i++) { + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsArticle')->find($ids[$i]->id); + } + echo "100 CmsArticle find(): " . number_format(microtime(true) - $start, 6) . "\n"; + } + + public function testFindCmsGroup() + { + for ($i = 0; $i < 100; $i++) { + $group = new CmsGroup(); + $group->name = "foo" . $i; + $this->_em->persist($group); + } + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsGroup')->findAll(); + echo "100 CmsGroup: " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsGroup')->findAll(); + echo "100 CmsGroup: " . number_format(microtime(true) - $start, 6) . "\n"; + } + + public function testFindCmsUser() + { + for ($i = 0; $i < 100; $i++) { + $user = new CmsUser(); + $user->name = "beberlei"; + $user->status = "active"; + $user->username = "beberlei".$i; + $this->_em->persist($user); + } + + $this->_em->flush(); + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->findAll(); + echo "100 CmsUser: " . number_format(microtime(true) - $start, 6) . "\n"; + + $this->_em->clear(); + + $start = microtime(true); + $articles = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->findAll(); + echo "100 CmsUser: " . number_format(microtime(true) - $start, 6) . "\n"; + } +} + + +