From 55d70248a9dd3a1f293f23451de97e6509ac4f1b Mon Sep 17 00:00:00 2001 From: romanb Date: Thu, 21 May 2009 08:53:40 +0000 Subject: [PATCH] [2.0] Implemented class table inheritance (no DQL bulk UPDATE/DELETE support yet) --- lib/Doctrine/DBAL/Connection.php | 6 +- .../DBAL/Platforms/SqlitePlatform.php | 6 +- lib/Doctrine/DBAL/Types/Type.php | 5 - lib/Doctrine/ORM/Mapping/ClassMetadata.php | 64 ++-- .../ORM/Mapping/ClassMetadataFactory.php | 87 +++++- .../Persisters/JoinedSubclassPersister.php | 277 +++++++++--------- .../ORM/Persisters/SingleTablePersister.php | 3 +- .../Persisters/StandardEntityPersister.php | 77 ++--- lib/Doctrine/ORM/Query/SqlWalker.php | 125 ++++++-- lib/Doctrine/ORM/Tools/SchemaTool.php | 94 ++++-- .../Tests/Models/Company/CompanyEmployee.php | 39 +-- .../Tests/Models/Company/CompanyManager.php | 16 +- .../Tests/Models/Company/CompanyPerson.php | 62 ++++ .../Tests/ORM/Functional/AllTests.php | 1 + .../Functional/ClassTableInheritanceTest.php | 92 ++++++ .../Doctrine/Tests/OrmFunctionalTestCase.php | 27 +- 16 files changed, 670 insertions(+), 311 deletions(-) create mode 100644 tests/Doctrine/Tests/Models/Company/CompanyPerson.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php diff --git a/lib/Doctrine/DBAL/Connection.php b/lib/Doctrine/DBAL/Connection.php index 4fde682c5..c9d6a5042 100644 --- a/lib/Doctrine/DBAL/Connection.php +++ b/lib/Doctrine/DBAL/Connection.php @@ -515,7 +515,7 @@ class Connection { $this->connect(); try { - //echo "DBAL:" . $query . PHP_EOL; + echo "DBAL:" . $query . PHP_EOL; if ( ! empty($params)) { $stmt = $this->prepare($query); $stmt->execute($params); @@ -542,9 +542,9 @@ class Connection public function exec($query, array $params = array()) { $this->connect(); try { - //echo $query . PHP_EOL; + echo "DBAL:" . $query . PHP_EOL; if ( ! empty($params)) { - //var_dump($params); + var_dump($params); $stmt = $this->prepare($query); $stmt->execute($params); return $stmt->rowCount(); diff --git a/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php b/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php index 87d3c9b20..09f0efe4b 100644 --- a/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php +++ b/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php @@ -21,6 +21,8 @@ namespace Doctrine\DBAL\Platforms; +use Doctrine\Common\DoctrineException; + /** * The SqlitePlatform class describes the specifics and dialects of the SQLite * database platform. @@ -492,11 +494,11 @@ class SqlitePlatform extends AbstractPlatform public function getCreateTableSql($name, array $fields, array $options = array()) { if ( ! $name) { - throw ConnectionException::invalidTableName($name); + throw DoctrineException::invalidTableName($name); } if (empty($fields)) { - throw ConnectionException::noFieldsSpecifiedForTable($name); + throw DoctrineException::noFieldsSpecifiedForTable($name); } $queryFields = $this->getColumnDeclarationListSql($fields); diff --git a/lib/Doctrine/DBAL/Types/Type.php b/lib/Doctrine/DBAL/Types/Type.php index 3402d0419..8350c0bfd 100644 --- a/lib/Doctrine/DBAL/Types/Type.php +++ b/lib/Doctrine/DBAL/Types/Type.php @@ -50,11 +50,6 @@ abstract class Type */ public static function getType($name) { - if (is_object($name)) { - try { throw new \Exception(); } - catch (\Exception $e) { echo $e->getTraceAsString(); } - die(); - } if ( ! isset(self::$_typeObjects[$name])) { if ( ! isset(self::$_typesMap[$name])) { throw DoctrineException::updateMe("Unknown type: $name"); diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index 9fd611a93..184446e10 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -297,7 +297,7 @@ final class ClassMetadata * * name => * schema => - * catalog => + * catalog => //TODO: remove catalog? needed? * * @var array */ @@ -397,6 +397,15 @@ final class ClassMetadata */ public $insertSql; + /** + * A map of field names to class names, where the field names are association + * fields that have been inherited from another class and values are the names + * of the classes that define the association. + * + * @var array + */ + public $inheritedAssociationFields = array(); + /** * Initializes a new ClassMetadata instance that will hold the object-relational mapping * metadata of the class with the given name. @@ -1362,10 +1371,18 @@ final class ClassMetadata * This is mainly used to add inherited association mappings to derived classes. * * @param AssociationMapping $mapping + * @param string $owningClassName The name of the class that defined this mapping. + * @todo Rename: addInheritedAssociationMapping */ - public function addAssociationMapping(AssociationMapping $mapping) + public function addAssociationMapping(AssociationMapping $mapping, $owningClassName) { - $this->_storeAssociationMapping($mapping); + $sourceFieldName = $mapping->sourceFieldName; + if (isset($this->associationMappings[$sourceFieldName])) { + throw MappingException::duplicateFieldMapping(); + } + $this->associationMappings[$sourceFieldName] = $mapping; + $this->inheritedAssociationFields[$sourceFieldName] = $owningClassName; + $this->_registerMappingIfInverse($mapping); } /** @@ -1374,6 +1391,7 @@ final class ClassMetadata * This is mainly used to add inherited field mappings to derived classes. * * @param array $mapping + * @todo Rename: addInheritedFieldMapping */ public function addFieldMapping(array $fieldMapping) { @@ -1761,46 +1779,6 @@ final class ClassMetadata $this->sequenceGeneratorDefinition = $definition; } - /** - * INTERNAL: Called by ClassMetadataFactory. - * - * Tells this class descriptor to finish the mapping definition, making any - * final adjustments, i.e. generating some SQL strings. - */ - public function finishMapping() - { - $columns = $values = array(); - - if ($this->inheritanceType == self::INHERITANCE_TYPE_JOINED) { - //TODO - } else { - foreach ($this->reflFields as $name => $field) { - if (isset($this->associationMappings[$name])) { - $assoc = $this->associationMappings[$name]; - if ($assoc->isOwningSide && $assoc->isOneToOne()) { - foreach ($assoc->targetToSourceKeyColumns as $sourceCol) { - $columns[] = $sourceCol; - $values[] = '?'; - } - } - } else if ($this->generatorType != self::GENERATOR_TYPE_IDENTITY || $this->identifier[0] != $name) { - $columns[] = $this->columnNames[$name]; - $values[] = '?'; - } - } - } - - if ($this->discriminatorColumn) { - $columns[] = $this->discriminatorColumn['name']; - $values[] = '?'; - } - - $this->insertSql = 'INSERT INTO ' . $this->primaryTable['name'] - . ' (' . implode(', ', $columns) . ') ' - . 'VALUES (' . implode(', ', $values) . ')'; - - } - /** * Creates a string representation of this instance. * diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 0211a253b..25cfed740 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -34,6 +34,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; * @version $Revision$ * @link www.doctrine-project.org * @since 2.0 + * @todo Support for mapped superclasses (@DoctrineMappedSuperclass) */ class ClassMetadataFactory { @@ -43,6 +44,7 @@ class ClassMetadataFactory private $_driver; /** The used cache driver. */ private $_cacheDriver; + private $_loadedMetadata = array(); /** * Creates a new factory instance that uses the given metadata driver implementation. @@ -100,9 +102,11 @@ class ClassMetadataFactory } /** + * Sets the metadata descriptor for a specific class. + * This is only useful in very special cases, like when generating proxy classes. * - * @param $className - * @param $class + * @param string $className + * @param ClassMetadata $class */ public function setMetadataFor($className, $class) { @@ -113,7 +117,7 @@ class ClassMetadataFactory * Loads the metadata of the class in question and all it's ancestors whose metadata * is still not loaded. * - * @param string $name The name of the class for which the metadata should get loaded. + * @param string $name The name of the class for which the metadata should get loaded. * @param array $tables The metadata collection to which the loaded metadata is added. */ protected function _loadMetadata($name) @@ -133,6 +137,12 @@ class ClassMetadataFactory $parent = null; $visited = array(); foreach ($parentClasses as $className) { + if (isset($this->_loadedMetadata[$className])) { + $parent = $this->_loadedMetadata[$className]; + array_unshift($visited, $className); + continue; + } + $class = $this->_newClassMetadataInstance($className); if ($parent) { $class->setInheritanceType($parent->inheritanceType); @@ -167,7 +177,8 @@ class ClassMetadataFactory } $class->setParentClasses($visited); - $class->finishMapping(); + + $this->_generateStaticSql($class); $this->_loadedMetadata[$className] = $class; @@ -200,7 +211,9 @@ class ClassMetadataFactory $mapping['inherited'] = $parentClass->name; } $subClass->addFieldMapping($mapping); - $subClass->addReflectionProperty($fieldName, $parentClass->getReflectionProperty($fieldName)); + } + foreach ($parentClass->reflFields as $name => $field) { + $subClass->reflFields[$name] = $field; } } @@ -213,10 +226,72 @@ class ClassMetadataFactory private function _addInheritedRelations(ClassMetadata $subClass, ClassMetadata $parentClass) { foreach ($parentClass->associationMappings as $mapping) { - $subClass->addAssociationMapping($mapping); + if (isset($parentClass->inheritedAssociationFields[$mapping->sourceFieldName])) { + // parent class also inherited that one + $subClass->addAssociationMapping($mapping, $parentClass->inheritedAssociationFields[$mapping->sourceFieldName]); + } else { + // parent class defined that one + $subClass->addAssociationMapping($mapping, $parentClass->name); + } } } + /** + * Generates any static SQL strings for a class and stores them in the descriptor. + * + * @param ClassMetadata $class + */ + private function _generateStaticSql($class) + { + // Generate INSERT SQL + $columns = $values = array(); + if ($class->inheritanceType == ClassMetadata::INHERITANCE_TYPE_JOINED) { + foreach ($class->reflFields as $name => $field) { + if (isset($class->fieldMappings[$name]['inherited']) && ! isset($class->fieldMappings[$name]['id']) + || isset($class->inheritedAssociationFields[$name])) { + continue; + } + + if (isset($class->associationMappings[$name])) { + $assoc = $class->associationMappings[$name]; + if ($assoc->isOneToOne() && $assoc->isOwningSide) { + foreach ($assoc->targetToSourceKeyColumns as $sourceCol) { + $columns[] = $this->_targetPlatform->quoteIdentifier($sourceCol); + $values[] = '?'; + } + } + } else if ($class->name != $class->rootEntityName || ! $class->isIdGeneratorIdentity() || $class->identifier[0] != $name) { + $columns[] = $this->_targetPlatform->quoteIdentifier($class->columnNames[$name]); + $values[] = '?'; + } + } + } else { + foreach ($class->reflFields as $name => $field) { + if (isset($class->associationMappings[$name])) { + $assoc = $class->associationMappings[$name]; + if ($assoc->isOwningSide && $assoc->isOneToOne()) { + foreach ($assoc->targetToSourceKeyColumns as $sourceCol) { + $columns[] = $this->_targetPlatform->quoteIdentifier($sourceCol); + $values[] = '?'; + } + } + } else if ($class->generatorType != ClassMetadata::GENERATOR_TYPE_IDENTITY || $class->identifier[0] != $name) { + $columns[] = $this->_targetPlatform->quoteIdentifier($class->columnNames[$name]); + $values[] = '?'; + } + } + } + if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined() && $class->name == $class->rootEntityName) { + $columns[] = $class->discriminatorColumn['name']; + $values[] = '?'; + } + + $class->insertSql = 'INSERT INTO ' . + $this->_targetPlatform->quoteIdentifier($class->primaryTable['name']) + . ' (' . implode(', ', $columns) . ') ' + . 'VALUES (' . implode(', ', $values) . ')'; + } + /** * Completes the ID generator mapping. If "auto" is specified we choose the generator * most appropriate for the targeted database platform. diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 5f9ed573f..19c8eafa1 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -34,129 +34,169 @@ use Doctrine\Common\DoctrineException; * @since 2.0 * @todo Reimplement. */ -class JoinedSubclassPersister extends AbstractEntityPersister -{ +class JoinedSubclassPersister extends StandardEntityPersister +{ + /** Map that maps column names to the table names that own them. + * This is mainly a temporary cache, used during a single request. + */ + private $_owningTableMap = array(); + /** - * Inserts an entity that is part of a Class Table Inheritance hierarchy. + * {@inheritdoc} * - * @param object $record record to be inserted - * @return boolean * @override */ - /*public function insert($entity) + protected function _prepareData($entity, array &$result, $isInsert = false) { - $class = $entity->getClass(); - - $dataSet = array(); - - $this->_prepareData($entity, $dataSet, true); - - $dataSet = $this->_groupFieldsByDefiningClass($class, $dataSet); - - $component = $class->name; - $classes = $class->parentClasses; - array_unshift($classes, $component); - - $identifier = null; - foreach (array_reverse($classes) as $k => $parent) { - $parentClass = $this->_em->getClassMetadata($parent); - if ($k == 0) { - if ($parentClass->isIdGeneratorIdentity()) { - $this->_insertRow($parentClass->getTableName(), $dataSet[$parent]); - $identifier = $this->_conn->lastInsertId(); - } else if ($parentClass->isIdGeneratorSequence()) { - $seq = $entity->getClassMetadata()->getTableOption('sequenceName'); - if ( ! empty($seq)) { - $id = $this->_conn->getSequenceManager()->nextId($seq); - $identifierFields = $parentClass->identifier; - $dataSet[$parent][$identifierFields[0]] = $id; - $this->_insertRow($parentClass->getTableName(), $dataSet[$parent]); - } + parent::_prepareData($entity, $result, $isInsert); + // Populate the discriminator column + if ($isInsert) { + $discColumn = $this->_class->discriminatorColumn; + $rootClass = $this->_em->getClassMetadata($this->_class->rootEntityName); + $result[$rootClass->primaryTable['name']][$discColumn['name']] = + $this->_class->discriminatorValue; + } + } + + /** + * {@inheritdoc} + * + * @override + */ + public function getOwningTable($fieldName) + { + if ( ! isset($this->_owningTableMap[$fieldName])) { + if (isset($this->_class->associationMappings[$fieldName])) { + if (isset($this->_class->inheritedAssociationFields[$fieldName])) { + $this->_owningTableMap[$fieldName] = $this->_em->getClassMetadata( + $this->_class->inheritedAssociationFields[$fieldName])->primaryTable['name']; } else { - throw DoctrineException::updateMe("Unsupported identifier type '$identifierType'."); + $this->_owningTableMap[$fieldName] = $this->_class->primaryTable['name']; } - $entity->_assignIdentifier($identifier); + } else if (isset($this->_class->fieldMappings[$fieldName]['inherited'])) { + $this->_owningTableMap[$fieldName] = $this->_em->getClassMetadata( + $this->_class->fieldMappings[$fieldName]['inherited'])->primaryTable['name']; } else { - foreach ($entity->_identifier() as $id => $value) { - $dataSet[$parent][$parentClass->getColumnName($id)] = $value; - } - $this->_insertRow($parentClass->getTableName(), $dataSet[$parent]); + $this->_owningTableMap[$fieldName] = $this->_class->primaryTable['name']; } } + return $this->_owningTableMap[$fieldName]; + } - return true; - }*/ - /** - * Updates an entity that is part of a Class Table Inheritance hierarchy. + * {@inheritdoc} * - * @param Doctrine_Entity $record record to be updated - * @return boolean whether or not the update was successful + * @override */ - /*protected function _doUpdate($entity) + public function executeInserts() { - $conn = $this->_conn; - $classMetadata = $this->_classMetadata; - $identifier = $this->_convertFieldToColumnNames($record->identifier(), $classMetadata); - $dataSet = $this->_groupFieldsByDefiningClass($record); - $component = $classMetadata->name; - $classes = $classMetadata->parentClasses; - array_unshift($classes, $component); + if ( ! $this->_queuedInserts) { + return; + } - foreach ($record as $field => $value) { - if ($value instanceof Doctrine_ORM_Entity) { - if ( ! $value->exists()) { - $value->save(); + $postInsertIds = array(); + $idGen = $this->_class->idGenerator; + $isPostInsertId = $idGen->isPostInsertGenerator(); + + // Prepare statements for all tables + $stmts = $classes = array(); + $stmts[$this->_class->primaryTable['name']] = $this->_conn->prepare($this->_class->insertSql); + $classes[$this->_class->name] = $this->_class; + foreach ($this->_class->parentClasses as $parentClass) { + $classes[$parentClass] = $this->_em->getClassMetadata($parentClass); + $stmts[$classes[$parentClass]->primaryTable['name']] = $this->_conn->prepare($classes[$parentClass]->insertSql); + } + $rootTableName = $classes[$this->_class->rootEntityName]->primaryTable['name']; + + foreach ($this->_queuedInserts as $entity) { + $insertData = array(); + $this->_prepareData($entity, $insertData, true); + + // Execute insert on root table + $paramIndex = 1; + $stmt = $stmts[$rootTableName]; + foreach ($insertData[$rootTableName] as $columnName => $value) { + $stmt->bindValue($paramIndex++, $value/*, TODO: TYPE*/); + } + $stmt->execute(); + unset($insertData[$rootTableName]); + + if ($isPostInsertId) { + $id = $idGen->generate($this->_em, $entity); + $postInsertIds[$id] = $entity; + } else { + $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity); + } + + // Execute inserts on subtables + foreach ($insertData as $tableName => $data) { + $stmt = $stmts[$tableName]; + $paramIndex = 1; + foreach ((array)$id as $idVal) { + $stmt->bindValue($paramIndex++, $idVal/*, TODO: TYPE*/); } - $idValues = $value->identifier(); - $record->set($field, $idValues[0]); + foreach ($data as $columnName => $value) { + $stmt->bindValue($paramIndex++, $value/*, TODO: TYPE*/); + } + $stmt->execute(); } } - foreach (array_reverse($classes) as $class) { - $parentTable = $conn->getClassMetadata($class); - $this->_updateRow($parentTable->getTableName(), $dataSet[$class], $identifier); - } - - $record->assignIdentifier(true); + foreach ($stmts as $stmt) + $stmt->closeCursor(); - return true; - }*/ - - /** - * Deletes an entity that is part of a Class Table Inheritance hierarchy. + $this->_queuedInserts = array(); + + return $postInsertIds; + } + + /** + * Updates an entity. * + * @param object $entity The entity to update. + * @override */ - /*protected function _doDelete(Doctrine_ORM_Entity $record) + public function update($entity) { - $conn = $this->_conn; - try { - $class = $this->_classMetadata; - $conn->beginInternalTransaction(); - $this->_deleteComposites($record); + $updateData = array(); + $this->_prepareData($entity, $updateData); - $record->_state(Doctrine_ORM_Entity::STATE_TDIRTY); + $id = array_combine( + $this->_class->getIdentifierFieldNames(), + $this->_em->getUnitOfWork()->getEntityIdentifier($entity) + ); - $identifier = $this->_convertFieldToColumnNames($record->identifier(), $class); - - // run deletions, starting from the class, upwards the hierarchy - $conn->delete($class->getTableName(), $identifier); - foreach ($class->parentClasses as $parent) { - $parentClass = $conn->getClassMetadata($parent); - $this->_deleteRow($parentClass->getTableName(), $identifier); - } - - $record->_state(Doctrine_ORM_Entity::STATE_TCLEAN); - - $this->removeRecord($record); // @todo should be done in the unitofwork - $conn->commit(); - } catch (Exception $e) { - $conn->rollback(); - throw $e; + foreach ($updateData as $tableName => $data) { + $this->_conn->update($tableName, $updateData[$tableName], $id); } + } - return true; - }*/ + /** + * Deletes an entity. + * + * @param object $entity The entity to delete. + * @override + */ + public function delete($entity) + { + $id = array_combine( + $this->_class->getIdentifierFieldNames(), + $this->_em->getUnitOfWork()->getEntityIdentifier($entity) + ); + + // If the database platform supports FKs, just + // delete the row from the root table. Cascades do the rest. + if ($this->_conn->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $this->_conn->delete($this->_em->getClassMetadata($this->_class->rootEntityName) + ->primaryTable['name'], $id); + } else { + // Delete the parent tables, starting from this class' table up to the root table + $this->_conn->delete($this->_class->primaryTable['name'], $id); + foreach ($this->_class->parentClasses as $parentClass) { + $this->_conn->delete($this->_em->getClassMetadata($parentClass)->primaryTable['name'], $id); + } + } + } /** * Adds all parent classes as INNER JOINs and subclasses as OUTER JOINs @@ -205,21 +245,6 @@ class JoinedSubclassPersister extends AbstractEntityPersister return array_unique($fields); }*/ - /** - * - */ - /*public function getFieldNames() - { - if ($this->_fieldNames) { - return $this->_fieldNames; - } - - $fieldNames = $this->_classMetadata->fieldNames; - $this->_fieldNames = array_unique($fieldNames); - - return $fieldNames; - }*/ - /** * * @todo Looks like this better belongs into the ClassMetadata class. @@ -248,36 +273,4 @@ class JoinedSubclassPersister extends AbstractEntityPersister throw \Doctrine\Common\DoctrineException::updateMe("Unable to find defining class of field '$fieldName'."); }*/ - - /** - * Analyzes the fields of the entity and creates a map in which the field names - * are grouped by the class names they belong to. - * - * @return array - */ - /*protected function _groupFieldsByDefiningClass(Doctrine_ClassMetadata $class, array $fields) - { - $dataSet = array(); - $component = $class->name; - - $classes = array_merge(array($component), $class->parentClasses); - - foreach ($classes as $class) { - $dataSet[$class] = array(); - $parentClassMetadata = $this->_em->getClassMetadata($class); - foreach ($parentClassMetadata->fieldMappings as $fieldName => $mapping) { - if ((isset($mapping['id']) && $mapping['id'] === true) || - (isset($mapping['inherited']) && $mapping['inherited'] === true)) { - continue; - } - if ( ! array_key_exists($fieldName, $fields)) { - continue; - } - $columnName = $parentClassMetadata->getColumnName($fieldName); - $dataSet[$class][$columnName] = $fields[$fieldName]; - } - } - - return $dataSet; - }*/ } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php index a14e6b292..d709d5732 100644 --- a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php +++ b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php @@ -40,7 +40,8 @@ class SingleTablePersister extends StandardEntityPersister // Populate the discriminator column if ($isInsert) { $discColumn = $this->_class->discriminatorColumn; - $result[$discColumn['name']] = $this->_class->discriminatorValue; + $result[$this->_class->primaryTable['name']][$discColumn['name']] = + $this->_class->discriminatorValue; } } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php index 08c2cf983..eee02ea44 100644 --- a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php @@ -84,29 +84,6 @@ class StandardEntityPersister $this->_conn = $em->getConnection(); $this->_class = $class; } - - /** - * Inserts an entity. - * - * @param object $entity The entity to insert. - * @return mixed If the entity uses a post-insert ID generator, the generated - * ID is returned, NULL otherwise. - */ - public function insert($entity) - { - $insertData = array(); - $this->_prepareData($entity, $insertData, true); - - $stmt = $this->_conn->prepare($this->_class->insertSql); - $stmt->execute(array_values($insertData)); - $stmt->closeCursor(); - - $idGen = $this->_class->getIdGenerator(); - if ($idGen->isPostInsertGenerator()) { - return $idGen->generate($this->_em, $entity); - } - return null; - } /** * Adds an entity to the queued inserts. @@ -120,6 +97,8 @@ class StandardEntityPersister /** * Executes all queued inserts. + * + * @return array An array of any generated post-insert IDs. */ public function executeInserts() { @@ -128,14 +107,21 @@ class StandardEntityPersister } $postInsertIds = array(); - $idGen = $this->_class->getIdGenerator(); + $idGen = $this->_class->idGenerator; $isPostInsertId = $idGen->isPostInsertGenerator(); $stmt = $this->_conn->prepare($this->_class->insertSql); + $primaryTableName = $this->_class->primaryTable['name']; foreach ($this->_queuedInserts as $entity) { $insertData = array(); $this->_prepareData($entity, $insertData, true); - $stmt->execute(array_values($insertData)); + + $paramIndex = 1; + foreach ($insertData[$primaryTableName] as $value) { + $stmt->bindValue($paramIndex++, $value/*, TODO: TYPE */); + } + $stmt->execute(); + if ($isPostInsertId) { $postInsertIds[$idGen->generate($this->_em, $entity)] = $entity; } @@ -157,7 +143,8 @@ class StandardEntityPersister $this->_prepareData($entity, $updateData); $id = array_combine($this->_class->getIdentifierFieldNames(), $this->_em->getUnitOfWork()->getEntityIdentifier($entity)); - $this->_conn->update($this->_class->getTableName(), $updateData, $id); + $tableName = $this->_class->primaryTable['name']; + $this->_conn->update($tableName, $updateData[$tableName], $id); } /** @@ -171,7 +158,7 @@ class StandardEntityPersister $this->_class->getIdentifierFieldNames(), $this->_em->getUnitOfWork()->getEntityIdentifier($entity) ); - $this->_conn->delete($this->_class->getTableName(), $id); + $this->_conn->delete($this->_class->primaryTable['name'], $id); } /** @@ -229,7 +216,9 @@ class StandardEntityPersister } /** - * Prepares the data of an entity for an insert/update operation. + * 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. * * @param object $entity * @param array $result The reference to the data array. @@ -237,6 +226,7 @@ class StandardEntityPersister */ protected function _prepareData($entity, array &$result, $isInsert = false) { + $platform = $this->_conn->getDatabasePlatform(); foreach ($this->_em->getUnitOfWork()->getEntityChangeSet($entity) as $field => $change) { $oldVal = $change[0]; $newVal = $change[1]; @@ -245,27 +235,41 @@ class StandardEntityPersister if (isset($this->_class->associationMappings[$field])) { $assocMapping = $this->_class->associationMappings[$field]; + // Only owning side of 1-1 associations can have a FK column. if ( ! $assocMapping->isOneToOne() || $assocMapping->isInverseSide()) { continue; } foreach ($assocMapping->sourceToTargetKeyColumns as $sourceColumn => $targetColumn) { $otherClass = $this->_em->getClassMetadata($assocMapping->targetEntityName); if ($newVal === null) { - $result[$sourceColumn] = null; + $result[$this->getOwningTable($field)][$sourceColumn] = null; } else { - $result[$sourceColumn] = $otherClass->getReflectionProperty( - $otherClass->getFieldName($targetColumn))->getValue($newVal); + $result[$this->getOwningTable($field)][$sourceColumn] = + $otherClass->reflFields[$otherClass->fieldNames[$targetColumn]] + ->getValue($newVal); } } } else if ($newVal === null) { - $result[$columnName] = null; + $result[$this->getOwningTable($field)][$columnName] = null; } else { - $result[$columnName] = Type::getType($this->_class->getTypeOfField($field)) - ->convertToDatabaseValue($newVal, $this->_conn->getDatabasePlatform()); + $result[$this->getOwningTable($field)][$columnName] = Type::getType( + $this->_class->fieldMappings[$field]['type']) + ->convertToDatabaseValue($newVal, $platform); } } } + /** + * Gets the name of the table that owns the column the given field is mapped to. + * + * @param string $fieldName + * @return string + */ + public function getOwningTable($fieldName) + { + return $this->_class->primaryTable['name']; + } + /** * Loads an entity by a list of field criteria. * @@ -292,7 +296,7 @@ class StandardEntityPersister $this->_class->reflFields[$field]->setValue($entity, $value); } $id = array(); - if ($this->_class->isIdentifierComposite()) { + if ($this->_class->isIdentifierComposite) { foreach ($this->_class->identifier as $fieldName) { $id[] = $data[$fieldName]; } @@ -329,7 +333,8 @@ class StandardEntityPersister * Gets the SELECT SQL to select a single entity by a set of field criteria. * * @param array $criteria - * @return string + * @return string The SQL. + * @todo Quote identifier. */ protected function _getSelectSingleEntitySql(array $criteria) { diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 9975f653d..050d9a207 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -31,7 +31,7 @@ use Doctrine\Common\DoctrineException; * node. Therefore it is possible to only generate SQL parts by simply walking over * certain subtrees of the AST. * - * @author robo + * @author Roman Borschel * @since 2.0 */ class SqlWalker @@ -130,8 +130,9 @@ class SqlWalker } //if ($this->_query->getHydrationMode() == \Doctrine\ORM\Query::HYDRATE_OBJECT) { if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) { - $tblAlias = $this->getSqlTableAlias($class->getTableName() . $dqlAlias); - $discrColumn = $class->discriminatorColumn; + $rootClass = $this->_em->getClassMetadata($class->rootEntityName); + $tblAlias = $this->getSqlTableAlias($rootClass->getTableName() . $dqlAlias); + $discrColumn = $rootClass->discriminatorColumn; $columnAlias = $this->getSqlColumnAlias($discrColumn['name']); $sql .= ", $tblAlias." . $discrColumn['name'] . ' AS ' . $columnAlias; $this->_resultSetMapping->setDiscriminatorColumn($class->name, $dqlAlias, $columnAlias); @@ -156,9 +157,13 @@ class SqlWalker $dqlAlias = $rangeDecl->getAliasIdentificationVariable(); $this->_currentRootAlias = $dqlAlias; + $class = $rangeDecl->getClassMetadata(); - $sql .= $rangeDecl->getClassMetadata()->getTableName() . ' ' - . $this->getSqlTableAlias($rangeDecl->getClassMetadata()->getTableName() . $dqlAlias); + $sql .= $class->getTableName() . ' ' . $this->getSqlTableAlias($class->getTableName() . $dqlAlias); + + if ($class->isInheritanceTypeJoined()) { + $sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias); + } foreach ($firstIdentificationVarDecl->getJoinVariableDeclarations() as $joinVarDecl) { $sql .= $this->walkJoinVariableDeclaration($joinVarDecl); @@ -275,7 +280,7 @@ class SqlWalker $joinTable = $assoc->getJoinTable(); $joinTableAlias = $this->getSqlTableAlias($joinTable['name']); $sql .= $joinTable['name'] . ' ' . $joinTableAlias . ' ON '; - if ($targetQComp['relation']->isOwningSide()) { + if ($targetQComp['relation']->isOwningSide) { $sourceToRelationJoinColumns = $assoc->getSourceToRelationKeyColumns(); foreach ($sourceToRelationJoinColumns as $sourceColumn => $relationColumn) { $sql .= "$sourceTableAlias.$sourceColumn = $joinTableAlias.$relationColumn"; @@ -296,7 +301,7 @@ class SqlWalker $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; - if ($targetQComp['relation']->isOwningSide()) { + if ($targetQComp['relation']->isOwningSide) { $targetToRelationJoinColumns = $assoc->getTargetToRelationKeyColumns(); foreach ($targetToRelationJoinColumns as $targetColumn => $relationColumn) { $sql .= "$targetTableAlias.$targetColumn = $joinTableAlias.$relationColumn"; @@ -313,6 +318,10 @@ class SqlWalker if ($discrSql) { $sql .= ' AND ' . $discrSql; } + + if ($targetQComp['metadata']->isInheritanceTypeJoined()) { + $sql .= $this->_generateClassTableInheritanceJoins($targetQComp['metadata'], $joinedDqlAlias); + } return $sql; } @@ -379,7 +388,7 @@ class SqlWalker $sql .= $this->walkFunction($expr) . ' AS ' . $columnAlias; $this->_resultSetMapping->addScalarResult($columnAlias, $resultAlias); } else { - // $expr is an IdentificationVariable + // IdentificationVariable $dqlAlias = $expr; $queryComp = $this->_queryComponents[$dqlAlias]; @@ -389,29 +398,54 @@ class SqlWalker $this->_selectedClasses[$dqlAlias] = $class; } - $sqlTableAlias = $this->getSqlTableAlias($class->getTableName() . $dqlAlias); + $beginning = true; + if ($class->isInheritanceTypeJoined()) { + // Select all fields from the queried class + foreach ($class->fieldMappings as $fieldName => $mapping) { + if (isset($mapping['inherited'])) { + $tableName = $this->_em->getClassMetadata($mapping['inherited'])->primaryTable['name']; + } else { + $tableName = $class->primaryTable['name']; + } + if ($beginning) $beginning = false; else $sql .= ', '; + $sqlTableAlias = $this->getSqlTableAlias($tableName . $dqlAlias); + $columnAlias = $this->getSqlColumnAlias($mapping['columnName']); + $sql .= $sqlTableAlias . '.' . $mapping['columnName'] . ' AS ' . $columnAlias; + $this->_resultSetMapping->addFieldResult($dqlAlias, $columnAlias, $fieldName); + } - // Gather all fields - $fieldMappings = $class->fieldMappings; - foreach ($class->subClasses as $subclassName) { - $fieldMappings = array_merge( + // Add any additional fields of subclasses (not inherited fields) + foreach ($class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + foreach ($subClass->fieldMappings as $fieldName => $mapping) { + if (isset($mapping['inherited'])) { + continue; + } + if ($beginning) $beginning = false; else $sql .= ', '; + $sqlTableAlias = $this->getSqlTableAlias($subClass->primaryTable['name'] . $dqlAlias); + $columnAlias = $this->getSqlColumnAlias($mapping['columnName']); + $sql .= $sqlTableAlias . '.' . $mapping['columnName'] . ' AS ' . $columnAlias; + $this->_resultSetMapping->addFieldResult($dqlAlias, $columnAlias, $fieldName); + } + } + } else { + $fieldMappings = $class->fieldMappings; + foreach ($class->subClasses as $subclassName) { + $fieldMappings = array_merge( $fieldMappings, $this->_em->getClassMetadata($subclassName)->fieldMappings - ); - } - - $beginning = true; - foreach ($fieldMappings as $fieldName => $fieldMapping) { - if ($beginning) { - $beginning = false; - } else { - $sql .= ', '; + ); + } + $sqlTableAlias = $this->getSqlTableAlias($class->getTableName() . $dqlAlias); + foreach ($fieldMappings as $fieldName => $mapping) { + if ($beginning) $beginning = false; else $sql .= ', '; + $columnAlias = $this->getSqlColumnAlias($mapping['columnName']); + $sql .= $sqlTableAlias . '.' . $mapping['columnName'] . ' AS ' . $columnAlias; + $this->_resultSetMapping->addFieldResult($dqlAlias, $columnAlias, $fieldName); } - $columnAlias = $this->getSqlColumnAlias($fieldMapping['columnName']); - $sql .= $sqlTableAlias . '.' . $fieldMapping['columnName'] . ' AS ' . $columnAlias; - $this->_resultSetMapping->addFieldResult($dqlAlias, $columnAlias, $fieldName); } } + return $sql; } @@ -1075,4 +1109,45 @@ class SqlWalker { return $columnName . $this->_aliasCounter++; } + + /** + * Generates the SQL JOINs, that are necessary for Class Table Inheritance, + * for the given class. + * + * @param ClassMetadata $class + * @param string $dqlAlias + */ + private function _generateClassTableInheritanceJoins($class, $dqlAlias) + { + $sql = ''; + + $baseTableAlias = $this->getSqlTableAlias($class->primaryTable['name'] . $dqlAlias); + $idColumns = $class->getIdentifierColumnNames(); + + // INNER JOIN parent class tables + foreach ($class->parentClasses as $parentClassName) { + $parentClass = $this->_em->getClassMetadata($parentClassName); + $tableAlias = $this->getSqlTableAlias($parentClass->primaryTable['name'] . $dqlAlias); + $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; + } + } + + // LEFT JOIN subclass tables + foreach ($class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $tableAlias = $this->getSqlTableAlias($subClass->primaryTable['name'] . $dqlAlias); + $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; + } + } + + return $sql; + } } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index e7cc36e42..820764477 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -89,11 +89,12 @@ class SchemaTool } $options = array(); // table options - $columns = $this->_gatherColumns($class, $options); // table columns - - $this->_gatherRelationsSql($class, $sql, $columns, $foreignKeyConstraints); + $columns = array(); // table columns if ($class->isInheritanceTypeSingleTable()) { + $columns = $this->_gatherColumns($class, $options); + $this->_gatherRelationsSql($class, $sql, $columns, $foreignKeyConstraints); + // Add the discriminator column $discrColumnDef = $this->_getDiscriminatorColumnDefinition($class); $columns[$discrColumnDef['name']] = $discrColumnDef; @@ -110,15 +111,46 @@ class SchemaTool $processedClasses[$subClassName] = true; } } else if ($class->isInheritanceTypeJoined()) { - //TODO + // Add all non-inherited fields as columns + foreach ($class->fieldMappings as $fieldName => $mapping) { + if ( ! isset($mapping['inherited'])) { + $columns[$mapping['columnName']] = $this->_gatherColumn($class, $mapping, $options); + } + } + + $this->_gatherRelationsSql($class, $sql, $columns, $foreignKeyConstraints); + + // Add the discriminator column only to the root table + if ($class->name == $class->rootEntityName) { + $discrColumnDef = $this->_getDiscriminatorColumnDefinition($class); + $columns[$discrColumnDef['name']] = $discrColumnDef; + } else { + // Add an ID FK column to child tables + $idMapping = $class->fieldMappings[$class->identifier[0]]; + $idColumn = $this->_gatherColumn($class, $idMapping, $options); + unset($idColumn['autoincrement']); + $columns[$idMapping['columnName']] = $idColumn; + // Add a FK constraint on the ID column + $constraint = array(); + $constraint['tableName'] = $class->getTableName(); + $constraint['foreignTable'] = $this->_em->getClassMetadata($class->rootEntityName)->getTableName(); + $constraint['local'] = array($idMapping['columnName']); + $constraint['foreign'] = array($idMapping['columnName']); + $constraint['onDelete'] = 'CASCADE'; + $foreignKeyConstraints[] = $constraint; + } + } else if ($class->isInheritanceTypeTablePerClass()) { //TODO + } else { + $columns = $this->_gatherColumns($class, $options); + $this->_gatherRelationsSql($class, $sql, $columns, $foreignKeyConstraints); } $sql = array_merge($sql, $this->_platform->getCreateTableSql($class->getTableName(), $columns, $options)); $processedClasses[$class->name] = true; - if ($class->isIdGeneratorSequence()) { + if ($class->isIdGeneratorSequence() && $class->name == $class->rootEntityName) { $seqDef = $class->getSequenceGeneratorDefinition(); $sequences[] = $this->_platform->getCreateSequenceSql( $seqDef['sequenceName'], @@ -152,33 +184,49 @@ class SchemaTool ); } + /** + * Gathers the column definitions of all field mappings found in the given class. + * + * @param ClassMetadata $class + * @param array $options + * @return array + */ private function _gatherColumns($class, array &$options) { $columns = array(); foreach ($class->fieldMappings as $fieldName => $mapping) { - $column = array(); - $column['name'] = $mapping['columnName']; - $column['type'] = Type::getType($mapping['type']); - $column['length'] = $mapping['length']; - $column['notnull'] = ! $mapping['nullable']; - if ($class->isIdentifier($fieldName)) { - $column['primary'] = true; - $options['primary'][] = $mapping['columnName']; - if ($class->isIdGeneratorIdentity()) { - $column['autoincrement'] = true; - } - } - $columns[$mapping['columnName']] = $column; + $columns[$mapping['columnName']] = $this->_gatherColumn($class, $mapping, $options); } return $columns; } + private function _gatherColumn($class, array $mapping, array &$options) + { + $column = array(); + $column['name'] = $mapping['columnName']; + $column['type'] = Type::getType($mapping['type']); + $column['length'] = $mapping['length']; + $column['notnull'] = ! $mapping['nullable']; + if ($class->isIdentifier($mapping['fieldName'])) { + $column['primary'] = true; + $options['primary'][] = $mapping['columnName']; + if ($class->isIdGeneratorIdentity()) { + $column['autoincrement'] = true; + } + } + + return $column; + } + private function _gatherRelationsSql($class, array &$sql, array &$columns, array &$constraints) { - foreach ($class->associationMappings as $mapping) { - $foreignClass = $this->_em->getClassMetadata($mapping->getTargetEntityName()); - if ($mapping->isOneToOne() && $mapping->isOwningSide()) { + foreach ($class->associationMappings as $fieldName => $mapping) { + if (isset($class->inheritedAssociationFields[$fieldName])) { + continue; + } + $foreignClass = $this->_em->getClassMetadata($mapping->targetEntityName); + if ($mapping->isOneToOne() && $mapping->isOwningSide) { $constraint = array(); $constraint['tableName'] = $class->getTableName(); $constraint['foreignTable'] = $foreignClass->getTableName(); @@ -193,10 +241,10 @@ class SchemaTool $constraint['foreign'][] = $joinColumn['referencedColumnName']; } $constraints[] = $constraint; - } else if ($mapping->isOneToMany() && $mapping->isOwningSide()) { + } else if ($mapping->isOneToMany() && $mapping->isOwningSide) { //... create join table, one-many through join table supported later throw DoctrineException::updateMe("Not yet implemented."); - } else if ($mapping->isManyToMany() && $mapping->isOwningSide()) { + } else if ($mapping->isManyToMany() && $mapping->isOwningSide) { // create join table $joinTableColumns = array(); $joinTableOptions = array(); diff --git a/tests/Doctrine/Tests/Models/Company/CompanyEmployee.php b/tests/Doctrine/Tests/Models/Company/CompanyEmployee.php index e2a4551b8..fd39d6c3f 100644 --- a/tests/Doctrine/Tests/Models/Company/CompanyEmployee.php +++ b/tests/Doctrine/Tests/Models/Company/CompanyEmployee.php @@ -4,30 +4,35 @@ namespace Doctrine\Tests\Models\Company; /** * @DoctrineEntity - * @DoctrineTable(name="company_employee") - * @DoctrineInheritanceType("joined") - * @DoctrineDiscriminatorColumn(name="dtype", type="string", length=20) - * @DoctrineDiscriminatorMap({ - "emp" = "Doctrine\Tests\Models\Company\CompanyEmployee", - "man" = "Doctrine\Tests\Models\Company\CompanyManager"}) - * @DoctrineSubclasses({"Doctrine\Tests\Models\Company\CompanyManager"}) + * @DoctrineTable(name="company_employees") + * @DoctrineDiscriminatorValue("employee") + * @DoctrineSubClasses({"Doctrine\Tests\Models\Company\CompanyManager"}) */ -class CompanyEmployee +class CompanyEmployee extends CompanyPerson { /** - * @DoctrineId * @DoctrineColumn(type="integer") - * @DoctrineGeneratedValue(strategy="auto") */ - public $id; - - /** - * @DoctrineColumn(type="double") - */ - public $salary; + private $salary; /** * @DoctrineColumn(type="string", length=255) */ - public $department; + private $department; + + public function getSalary() { + return $this->salary; + } + + public function setSalary($salary) { + $this->salary = $salary; + } + + public function getDepartment() { + return $this->department; + } + + public function setDepartment($dep) { + $this->department = $dep; + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Company/CompanyManager.php b/tests/Doctrine/Tests/Models/Company/CompanyManager.php index 03fcc4a09..38ade3e80 100644 --- a/tests/Doctrine/Tests/Models/Company/CompanyManager.php +++ b/tests/Doctrine/Tests/Models/Company/CompanyManager.php @@ -4,11 +4,21 @@ namespace Doctrine\Tests\Models\Company; /** * @DoctrineEntity + * @DoctrineTable(name="company_managers") + * @DoctrineDiscriminatorValue("manager") */ class CompanyManager extends CompanyEmployee { - /* - * @DoctrineColumn(type="string", length="255") + /** + * @DoctrineColumn(type="string", length="250") */ - public $title; + private $title; + + public function getTitle() { + return $this->title; + } + + public function setTitle($title) { + $this->title = $title; + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Company/CompanyPerson.php b/tests/Doctrine/Tests/Models/Company/CompanyPerson.php new file mode 100644 index 000000000..1a4a75d87 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Company/CompanyPerson.php @@ -0,0 +1,62 @@ +id; + } + + public function getName() { + return $this->name; + } + + public function setName($name) { + $this->name = $name; + } + + public function getSpouse() { + return $this->spouse; + } + + public function setSpouse(CompanyPerson $spouse) { + if ($spouse !== $this->spouse) { + $this->spouse = $spouse; + $this->spouse->setSpouse($this); + } + } +} + diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index a1e833f0d..9409abc2c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -22,6 +22,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\BasicFunctionalTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\NativeQueryTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\SingleTableInheritanceTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ClassTableInheritanceTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\DetachedEntityTest'); return $suite; diff --git a/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php new file mode 100644 index 000000000..4311a0c70 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/ClassTableInheritanceTest.php @@ -0,0 +1,92 @@ +useModelSet('company'); + parent::setUp(); + } + + public function testCRUD() + { + + $person = new CompanyPerson; + $person->setName('Roman S. Borschel'); + + $this->_em->save($person); + + $employee = new CompanyEmployee; + $employee->setName('Roman S. Borschel'); + $employee->setSalary(100000); + $employee->setDepartment('IT'); + + $this->_em->save($employee); + + $employee->setName('Guilherme Blanco'); + $this->_em->flush(); + + $this->_em->clear(); + + $query = $this->_em->createQuery("select p from Doctrine\Tests\Models\Company\CompanyPerson p order by p.id asc"); + + $entities = $query->getResultList(); + + $this->assertEquals(2, count($entities)); + $this->assertTrue($entities[0] instanceof CompanyPerson); + $this->assertTrue($entities[1] instanceof CompanyEmployee); + $this->assertTrue(is_numeric($entities[0]->getId())); + $this->assertTrue(is_numeric($entities[1]->getId())); + $this->assertEquals('Roman S. Borschel', $entities[0]->getName()); + $this->assertEquals('Guilherme Blanco', $entities[1]->getName()); + $this->assertEquals(100000, $entities[1]->getSalary()); + + $this->_em->clear(); + + $query = $this->_em->createQuery("select p from Doctrine\Tests\Models\Company\CompanyEmployee p"); + + $entities = $query->getResultList(); + + $this->assertEquals(1, count($entities)); + $this->assertTrue($entities[0] instanceof CompanyEmployee); + $this->assertTrue(is_numeric($entities[0]->getId())); + $this->assertEquals('Guilherme Blanco', $entities[0]->getName()); + $this->assertEquals(100000, $entities[0]->getSalary()); + + $this->_em->clear(); + /* + $query = $this->_em->createQuery("select r,o from Doctrine\Tests\ORM\Functional\RelatedEntity r join r.owner o"); + + $entities = $query->getResultList(); + $this->assertEquals(1, count($entities)); + $this->assertTrue($entities[0] instanceof RelatedEntity); + $this->assertTrue(is_numeric($entities[0]->getId())); + $this->assertEquals('theRelatedOne', $entities[0]->getName()); + $this->assertTrue($entities[0]->getOwner() instanceof ChildEntity); + $this->assertEquals('thedata', $entities[0]->getOwner()->getData()); + $this->assertSame($entities[0], $entities[0]->getOwner()->getRelatedEntity()); + + $query = $this->_em->createQuery("update Doctrine\Tests\ORM\Functional\ChildEntity e set e.data = 'newdata'"); + + $affected = $query->execute(); + $this->assertEquals(1, $affected); + + $query = $this->_em->createQuery("delete Doctrine\Tests\ORM\Functional\ParentEntity e"); + + $affected = $query->execute(); + $this->assertEquals(2, $affected); + */ + } +} diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index 26acd2c59..6fa101d16 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -34,13 +34,17 @@ class OrmFunctionalTestCase extends OrmTestCase 'Doctrine\Tests\Models\CMS\CmsArticle' ), 'forum' => array(), - 'company' => array(), + 'company' => array( + 'Doctrine\Tests\Models\Company\CompanyPerson', + 'Doctrine\Tests\Models\Company\CompanyEmployee', + 'Doctrine\Tests\Models\Company\CompanyManager' + ), 'ecommerce' => array() ); protected function useModelSet($setName) { - $this->_usedModelSets[] = $setName; + $this->_usedModelSets[$setName] = true; } /** @@ -49,7 +53,7 @@ class OrmFunctionalTestCase extends OrmTestCase protected function tearDown() { $conn = $this->sharedFixture['conn']; - if (in_array('cms', $this->_usedModelSets)) { + if (isset($this->_usedModelSets['cms'])) { $conn->exec('DELETE FROM cms_users_groups'); $conn->exec('DELETE FROM cms_groups'); $conn->exec('DELETE FROM cms_addresses'); @@ -57,6 +61,12 @@ class OrmFunctionalTestCase extends OrmTestCase $conn->exec('DELETE FROM cms_articles'); $conn->exec('DELETE FROM cms_users'); } + if (isset($this->_usedModelSets['company'])) { + $conn->exec('DELETE FROM company_managers'); + $conn->exec('DELETE FROM company_employees'); + $conn->exec('DELETE FROM company_persons'); + } + $this->_em->clear(); } @@ -79,7 +89,7 @@ class OrmFunctionalTestCase extends OrmTestCase } $classes = array(); - foreach ($this->_usedModelSets as $setName) { + foreach ($this->_usedModelSets as $setName => $bool) { if ( ! isset(self::$_tablesCreated[$setName]) || $forceCreateTables) { foreach (self::$_modelSets[$setName] as $className) { $classes[] = $this->_em->getClassMetadata($className); @@ -88,7 +98,14 @@ class OrmFunctionalTestCase extends OrmTestCase } } if ($classes) { - $this->_schemaTool->createSchema($classes); + try { + $this->_schemaTool->createSchema($classes); + } catch (\Exception $e) { + // Suppress "xxx already exists" messages + if (stripos($e->getMessage(), 'already exists') === false) { + throw $e; + } + } } }