From b66d530540d18c60720e1843fc65ce7e0399a71c Mon Sep 17 00:00:00 2001 From: romanb Date: Tue, 26 May 2009 11:30:07 +0000 Subject: [PATCH] [2.0] Class table inheritance updates. Started work on self-referencing associations. --- lib/Doctrine/DBAL/Connection.php | 2 +- lib/Doctrine/ORM/EntityRepository.php | 8 +- .../ORM/Internal/Hydration/ObjectHydrator.php | 22 ++-- lib/Doctrine/ORM/NativeQuery.php | 2 +- .../Persisters/JoinedSubclassPersister.php | 110 ++++++++---------- .../Persisters/StandardEntityPersister.php | 47 +++++--- lib/Doctrine/ORM/Query.php | 14 +-- .../ORM/Query/AST/JoinPathExpression.php | 8 +- .../Query/AST/StateFieldPathExpression.php | 27 ++++- lib/Doctrine/ORM/Query/ResultSetMapping.php | 4 +- lib/Doctrine/ORM/Query/SqlWalker.php | 7 +- .../Tests/Models/Company/CompanyPerson.php | 4 - .../Functional/ClassTableInheritanceTest.php | 65 ++++++++++- .../Tests/ORM/Functional/NativeQueryTest.php | 1 + 14 files changed, 200 insertions(+), 121 deletions(-) diff --git a/lib/Doctrine/DBAL/Connection.php b/lib/Doctrine/DBAL/Connection.php index c9d6a5042..b6576c5bc 100644 --- a/lib/Doctrine/DBAL/Connection.php +++ b/lib/Doctrine/DBAL/Connection.php @@ -409,7 +409,7 @@ class Connection */ public function fetchAll($sql, array $params = array()) { - return $this->execute($sql, $params)->fetchAll(PDO::FETCH_ASSOC); + return $this->execute($sql, $params)->fetchAll(\PDO::FETCH_ASSOC); } /** diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index de0d8b472..3b1dbd604 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -22,9 +22,11 @@ namespace Doctrine\ORM; /** - * A repository provides the illusion of an in-memory Entity store. - * Base class for all custom user-defined repositories. - * Provides basic finder methods, common to all repositories. + * An EntityRepository serves as a repository for entities with generic as well as + * business specific methods for retrieving entities. + * + * This class is designed for inheritance and users can subclass this class to + * write their own repositories. * * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.doctrine-project.org diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 75a981de7..eddee0cc1 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -95,9 +95,12 @@ class ObjectHydrator extends AbstractHydrator $this->_fetchedAssociations[$assoc->sourceEntityName][$assoc->sourceFieldName] = true; if ($assoc->mappedByFieldName) { $this->_fetchedAssociations[$assoc->targetEntityName][$assoc->mappedByFieldName] = true; - } else if ($inverseAssoc = $this->_em->getClassMetadata($assoc->targetEntityName) - ->inverseMappings[$assoc->sourceFieldName]) { - $this->_fetchedAssociations[$assoc->targetEntityName][$inverseAssoc->sourceFieldName] = true; + } else { + $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName); + if (isset($targetClass->inverseMappings[$assoc->sourceFieldName])) { + $inverseAssoc = $targetClass->inverseMappings[$assoc->sourceFieldName]; + $this->_fetchedAssociations[$assoc->targetEntityName][$inverseAssoc->sourceFieldName] = true; + } } } } @@ -230,10 +233,11 @@ class ObjectHydrator extends AbstractHydrator } } - private function getEntity(array $data, $className) + private function getEntity(array $data, $dqlAlias) { - if (isset($this->_rsm->discriminatorColumns[$className])) { - $discrColumn = $this->_rsm->discriminatorColumns[$className]; + $className = $this->_rsm->aliasMap[$dqlAlias]; + if (isset($this->_rsm->discriminatorColumns[$dqlAlias])) { + $discrColumn = $this->_rsm->discriminatorColumns[$dqlAlias]; $className = $this->_discriminatorMap[$className][$data[$discrColumn]]; unset($data[$discrColumn]); } @@ -351,7 +355,7 @@ class ObjectHydrator extends AbstractHydrator $index = $indexExists ? $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; $indexIsValid = $index !== false ? $this->isIndexKeyInUse($baseElement, $relationAlias, $index) : false; if ( ! $indexExists || ! $indexIsValid) { - $element = $this->getEntity($data, $entityName); + $element = $this->getEntity($data, $dqlAlias); // If it's a bi-directional many-to-many, also initialize the reverse collection. if ($relation->isManyToMany()) { @@ -393,7 +397,7 @@ class ObjectHydrator extends AbstractHydrator if ( ! isset($nonemptyComponents[$dqlAlias])) { //$this->setRelatedElement($baseElement, $relationAlias, null); } else { - $this->setRelatedElement($baseElement, $relationAlias, $this->getEntity($data, $entityName)); + $this->setRelatedElement($baseElement, $relationAlias, $this->getEntity($data, $dqlAlias)); } } } @@ -411,7 +415,7 @@ class ObjectHydrator extends AbstractHydrator $this->_rootAliases[$dqlAlias] = true; // Mark as root alias if ($this->_isSimpleQuery || ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { - $element = $this->getEntity($rowData[$dqlAlias], $entityName); + $element = $this->getEntity($rowData[$dqlAlias], $dqlAlias); if ($field = $this->_getCustomIndexField($dqlAlias)) { if ($this->_rsm->isMixed) { $result[] = array( diff --git a/lib/Doctrine/ORM/NativeQuery.php b/lib/Doctrine/ORM/NativeQuery.php index 32ac43d9f..d2262369d 100644 --- a/lib/Doctrine/ORM/NativeQuery.php +++ b/lib/Doctrine/ORM/NativeQuery.php @@ -27,7 +27,7 @@ namespace Doctrine\ORM; * @author Roman Borschel * @since 2.0 */ -class NativeQuery extends AbstractQuery +final class NativeQuery extends AbstractQuery { private $_sql; diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 19c8eafa1..8cec5bc60 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -199,78 +199,64 @@ class JoinedSubclassPersister extends StandardEntityPersister } /** - * Adds all parent classes as INNER JOINs and subclasses as OUTER JOINs - * to the query. + * Gets the SELECT SQL to select a single entity by a set of field criteria. * - * Callback that is invoked during the SQL construction process. - * - * @return array The custom joins in the format => + * @param array $criteria + * @return string The SQL. + * @todo Quote identifier. + * @override */ - /*public function getCustomJoins() + protected function _getSelectSingleEntitySql(array $criteria) { - $customJoins = array(); - $classMetadata = $this->_classMetadata; - foreach ($classMetadata->parentClasses as $parentClass) { - $customJoins[$parentClass] = 'INNER'; + $tableAliases = array(); + $aliasIndex = 1; + $idColumns = $this->_class->getIdentifierColumnNames(); + $baseTableAlias = 't0'; + + foreach (array_merge($this->_class->subClasses, $this->_class->parentClasses) as $className) { + $tableAliases[$className] = 't' . $aliasIndex++; + } + + $columnList = ''; + foreach ($this->_class->fieldMappings as $fieldName => $mapping) { + $tableAlias = isset($mapping['inherited']) ? + $tableAliases[$mapping['inherited']] : $baseTableAlias; + if ($columnList != '') $columnList .= ', '; + $columnList .= $tableAlias . '.' . $this->_class->columnNames[$fieldName]; } - foreach ($classMetadata->subClasses as $subClass) { - if ($subClass != $this->getComponentName()) { - $customJoins[$subClass] = 'LEFT'; + + $sql = 'SELECT ' . $columnList . ' FROM ' . $this->_class->primaryTable['name']. ' ' . $baseTableAlias; + + // INNER JOIN parent tables + foreach ($this->_class->parentClasses as $parentClassName) { + $parentClass = $this->_em->getClassMetadata($parentClassName); + $tableAlias = $tableAliases[$parentClassName]; + $sql .= ' INNER JOIN ' . $parentClass->primaryTable['name'] . ' ' . $tableAlias . ' ON '; + $first = true; + foreach ($idColumns as $idColumn) { + if ($first) $first = false; else $sql .= ' AND '; + $sql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; } } - return $customJoins; - }*/ - - /** - * Adds the discriminator column to the selected fields in a query as well as - * all fields of subclasses. In Class Table Inheritance the default behavior is that - * all subclasses are joined in through OUTER JOINs when querying a base class. - * - * Callback that is invoked during the SQL construction process. - * - * @return array An array with the field names that will get added to the query. - */ - /*public function getCustomFields() - { - $classMetadata = $this->_classMetadata; - $conn = $this->_conn; - $discrColumn = $classMetadata->discriminatorColumn; - $fields = array($discrColumn['name']); - if ($classMetadata->subClasses) { - foreach ($classMetadata->subClasses as $subClass) { - $fields = array_merge($conn->getClassMetadata($subClass)->fieldNames, $fields); - } - } - return array_unique($fields); - }*/ - - /** - * - * @todo Looks like this better belongs into the ClassMetadata class. - */ - /*public function getOwningClass($fieldName) - { - $conn = $this->_conn; - $classMetadata = $this->_classMetadata; - if ($classMetadata->hasField($fieldName) && ! $classMetadata->isInheritedField($fieldName)) { - return $classMetadata; - } - - foreach ($classMetadata->parentClasses as $parentClass) { - $parentTable = $conn->getClassMetadata($parentClass); - if ($parentTable->hasField($fieldName) && ! $parentTable->isInheritedField($fieldName)) { - return $parentTable; + // OUTER JOIN sub tables + foreach ($this->_class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $tableAlias = $tableAliases[$subClassName]; + $sql .= ' LEFT JOIN ' . $subClass->primaryTable['name'] . ' ' . $tableAlias . ' ON '; + $first = true; + foreach ($idColumns as $idColumn) { + if ($first) $first = false; else $sql .= ' AND '; + $sql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; } } - foreach ((array)$classMetadata->subClasses as $subClass) { - $subTable = $conn->getClassMetadata($subClass); - if ($subTable->hasField($fieldName) && ! $subTable->isInheritedField($fieldName)) { - return $subTable; - } + $conditionSql = ''; + foreach ($criteria as $field => $value) { + if ($conditionSql != '') $conditionSql .= ' AND '; + $conditionSql .= $baseTableAlias . '.' . $this->_class->columnNames[$field] . ' = ?'; } - throw \Doctrine\Common\DoctrineException::updateMe("Unable to find defining class of field '$fieldName'."); - }*/ + return $sql . ' WHERE ' . $conditionSql; + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php index 7f2c71619..a77ec6cf6 100644 --- a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php @@ -199,26 +199,21 @@ class StandardEntityPersister //... } - /** - * Callback that is invoked during the SQL construction process. - */ - public function getCustomJoins() - { - return array(); - } - - /** - * Callback that is invoked during the SQL construction process. - */ - public function getCustomFields() - { - return array(); - } - /** * Prepares the data changeset of an entity for database insertion. * The array that is passed as the second parameter is filled with - * => pairs during this preparation. + * => pairs, grouped by table name, during this preparation. + * + * Example: + * + * array( + * 'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...), + * 'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...), + * ... + * ) + * + * + * Notes to inheritors: Be sure to call parent::_prepareData($entity, $result, $isInsert); * * @param object $entity * @param array $result The reference to the data array. @@ -227,7 +222,9 @@ class StandardEntityPersister protected function _prepareData($entity, array &$result, $isInsert = false) { $platform = $this->_conn->getDatabasePlatform(); - foreach ($this->_em->getUnitOfWork()->getEntityChangeSet($entity) as $field => $change) { + $uow = $this->_em->getUnitOfWork(); + + foreach ($uow->getEntityChangeSet($entity) as $field => $change) { $oldVal = $change[0]; $newVal = $change[1]; @@ -239,6 +236,18 @@ class StandardEntityPersister if ( ! $assocMapping->isOneToOne() || $assocMapping->isInverseSide()) { continue; } + + //TODO: If the one-one is self-referencing, check whether the referenced entity ($newVal) + // is still scheduled for insertion. If so: + // 1) set $newVal = null, so that we insert a null value + // 2) schedule $entity for an update, so that the FK gets set through an update + // later, after the referenced entity has been inserted. + //$needsPostponedUpdate = ... + /*if ($assocMapping->sourceEntityName == $assocMapping->targetEntityName && + isset($this->_queuedInserts[spl_object_hash($entity)])) { + echo "SELF-REFERENCING!"; + }*/ + foreach ($assocMapping->sourceToTargetKeyColumns as $sourceColumn => $targetColumn) { $otherClass = $this->_em->getClassMetadata($assocMapping->targetEntityName); if ($newVal === null) { @@ -283,7 +292,7 @@ class StandardEntityPersister $stmt->execute(array_values($criteria)); $data = array(); foreach ($stmt->fetch(\PDO::FETCH_ASSOC) as $column => $value) { - $fieldName = $this->_class->lcColumnToFieldNames[$column]; + $fieldName = $this->_class->fieldNames[$column]; $data[$fieldName] = Type::getType($this->_class->getTypeOfField($fieldName)) ->convertToPHPValue($value); } diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 5aeb746a4..676293c1f 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -36,7 +36,7 @@ use Doctrine\ORM\Query\QueryException; * @author Konsta Vesterinen * @author Roman Borschel */ -class Query extends AbstractQuery +final class Query extends AbstractQuery { /** * A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts. @@ -53,32 +53,32 @@ class Query extends AbstractQuery /** * @var integer $_state The current state of this query. */ - protected $_state = self::STATE_CLEAN; + private $_state = self::STATE_CLEAN; /** * @var string $_dql Cached DQL query. */ - protected $_dql = null; + private $_dql = null; /** * @var Doctrine\ORM\Query\ParserResult The parser result that holds DQL => SQL information. */ - protected $_parserResult; + private $_parserResult; /** * @var CacheDriver The cache driver used for caching queries. */ - protected $_queryCache; + private $_queryCache; /** * @var boolean Boolean value that indicates whether or not expire the query cache. */ - protected $_expireQueryCache = false; + private $_expireQueryCache = false; /** * @var int Query Cache lifetime. */ - protected $_queryCacheTTL; + private $_queryCacheTTL; // End of Caching Stuff diff --git a/lib/Doctrine/ORM/Query/AST/JoinPathExpression.php b/lib/Doctrine/ORM/Query/AST/JoinPathExpression.php index 26899b9f5..d68c88c68 100644 --- a/lib/Doctrine/ORM/Query/AST/JoinPathExpression.php +++ b/lib/Doctrine/ORM/Query/AST/JoinPathExpression.php @@ -1,13 +1,11 @@ . */ namespace Doctrine\ORM\Query\AST; @@ -13,6 +28,12 @@ namespace Doctrine\ORM\Query\AST; */ class StateFieldPathExpression extends Node { + //const TYPE_COLLECTION_VALUED_ASSOCIATION = 1; + //const TYPE_SINGLE_VALUED_ASSOCIATION = 2; + //const TYPE_STATE_FIELD = 3; + //private $_type; + + private $_parts; // Information that is attached during semantical analysis. private $_isSimpleStateFieldPathExpression = false; diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index ffa43a2c1..b2b2c434a 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -71,9 +71,9 @@ class ResultSetMapping * @param $alias * @param $discrColumn */ - public function setDiscriminatorColumn($className, $alias, $discrColumn) + public function setDiscriminatorColumn($alias, $discrColumn) { - $this->discriminatorColumns[$className] = $discrColumn; + $this->discriminatorColumns[$alias] = $discrColumn; $this->columnOwnerMap[$discrColumn] = $alias; } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 98634388c..50b3afc5f 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -112,7 +112,7 @@ class SqlWalker . implode(', ', array_map(array($this, 'walkSelectExpression'), $selectClause->getSelectExpressions())); - foreach ($this->_selectedClasses as $dqlAlias => $class) { + foreach ($this->_selectedClasses as $dqlAlias => $class) { if ($this->_queryComponents[$dqlAlias]['relation'] === null) { $this->_resultSetMapping->addEntityResult($class->name, $dqlAlias); } else { @@ -122,6 +122,7 @@ class SqlWalker $this->_queryComponents[$dqlAlias]['relation'] ); } + //if ($this->_query->getHydrationMode() == \Doctrine\ORM\Query::HYDRATE_OBJECT) { if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) { $rootClass = $this->_em->getClassMetadata($class->rootEntityName); @@ -129,7 +130,7 @@ class SqlWalker $discrColumn = $rootClass->discriminatorColumn; $columnAlias = $this->getSqlColumnAlias($discrColumn['name']); $sql .= ", $tblAlias." . $discrColumn['name'] . ' AS ' . $columnAlias; - $this->_resultSetMapping->setDiscriminatorColumn($class->name, $dqlAlias, $columnAlias); + $this->_resultSetMapping->setDiscriminatorColumn($dqlAlias, $columnAlias); } //} } @@ -253,7 +254,7 @@ class SqlWalker $assoc = $targetQComp['relation']; } - if ($assoc->isOneToOne()/* || $assoc->isOneToMany()*/) { + if ($assoc->isOneToOne()) { $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; $joinColumns = $assoc->getSourceToTargetKeyColumns(); $first = true; diff --git a/tests/Doctrine/Tests/Models/Company/CompanyPerson.php b/tests/Doctrine/Tests/Models/Company/CompanyPerson.php index 1a4a75d87..524b83504 100644 --- a/tests/Doctrine/Tests/Models/Company/CompanyPerson.php +++ b/tests/Doctrine/Tests/Models/Company/CompanyPerson.php @@ -1,8 +1,4 @@ setName('Roman S. Borschel'); @@ -89,4 +88,66 @@ class ClassTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(2, $affected); */ } + + public function testMultiLevelUpdateAndFind() { + $manager = new CompanyManager; + $manager->setName('Roman S. Borschel'); + $manager->setSalary(100000); + $manager->setDepartment('IT'); + $manager->setTitle('CTO'); + $this->_em->save($manager); + $this->_em->flush(); + + $manager->setName('Roman B.'); + $manager->setSalary(119000); + $manager->setTitle('CEO'); + $this->_em->save($manager); + $this->_em->flush(); + + $this->_em->clear(); + + $manager = $this->_em->find('Doctrine\Tests\Models\Company\CompanyManager', $manager->getId()); + + $this->assertEquals('Roman B.', $manager->getName()); + $this->assertEquals(119000, $manager->getSalary()); + $this->assertEquals('CEO', $manager->getTitle()); + $this->assertTrue(is_numeric($manager->getId())); + } + + public function testSelfReferencingOneToOne() { + $manager = new CompanyManager; + $manager->setName('John Smith'); + $manager->setSalary(100000); + $manager->setDepartment('IT'); + $manager->setTitle('CTO'); + + $wife = new CompanyPerson; + $wife->setName('Mary Smith'); + $wife->setSpouse($manager); + + $this->assertSame($manager, $wife->getSpouse()); + $this->assertSame($wife, $manager->getSpouse()); + + $this->_em->save($manager); + $this->_em->save($wife); + + $this->_em->flush(); + + //var_dump($this->_em->getConnection()->fetchAll('select * from company_persons')); + //var_dump($this->_em->getConnection()->fetchAll('select * from company_employees')); + //var_dump($this->_em->getConnection()->fetchAll('select * from company_managers')); + + $this->_em->clear(); + + $query = $this->_em->createQuery('select p, s from Doctrine\Tests\Models\Company\CompanyPerson p join p.spouse s where p.name=\'Mary Smith\''); + + $result = $query->getResultList(); + $this->assertEquals(1, count($result)); + $this->assertTrue($result[0] instanceof CompanyPerson); + $this->assertEquals('Mary Smith', $result[0]->getName()); + $this->assertTrue($result[0]->getSpouse() instanceof CompanyEmployee); + + //var_dump($result); + + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php index d2ce890ae..c11608ce9 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php @@ -39,6 +39,7 @@ class NativeQueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $users = $query->getResultList(); $this->assertEquals(1, count($users)); + $this->assertTrue($users[0] instanceof CmsUser); $this->assertEquals('Roman', $users[0]->name); } }