From ae7be288e160d6bdc22875e8cd3d40bfdd0b9d28 Mon Sep 17 00:00:00 2001 From: romanb Date: Sun, 3 May 2009 10:58:16 +0000 Subject: [PATCH] [2.0] Work on single table inheritance with more functional tests. --- lib/Doctrine/Common/NotifyPropertyChanged.php | 5 + .../DBAL/Platforms/AbstractPlatform.php | 2 +- lib/Doctrine/DBAL/Platforms/MySqlPlatform.php | 2 +- .../DBAL/Platforms/SqlitePlatform.php | 2 +- lib/Doctrine/ORM/AbstractQuery.php | 13 +- .../Internal/Hydration/AbstractHydrator.php | 1 + lib/Doctrine/ORM/Mapping/ClassMetadata.php | 2 +- lib/Doctrine/ORM/NativeQuery.php | 4 +- lib/Doctrine/ORM/PersistentCollection.php | 40 +++-- lib/Doctrine/ORM/Query/SqlWalker.php | 104 +++++++++++-- lib/Doctrine/ORM/Tools/SchemaTool.php | 144 +++++++++--------- lib/Doctrine/ORM/UnitOfWork.php | 4 - .../Functional/SingleTableInheritanceTest.php | 105 ++++++++++++- .../ORM/Query/DeleteSqlGenerationTest.php | 52 +++---- .../ORM/Query/UpdateSqlGenerationTest.php | 4 +- 15 files changed, 335 insertions(+), 149 deletions(-) diff --git a/lib/Doctrine/Common/NotifyPropertyChanged.php b/lib/Doctrine/Common/NotifyPropertyChanged.php index dbc8306d8..4f3e942ee 100644 --- a/lib/Doctrine/Common/NotifyPropertyChanged.php +++ b/lib/Doctrine/Common/NotifyPropertyChanged.php @@ -30,6 +30,11 @@ namespace Doctrine\Common; */ interface NotifyPropertyChanged { + /** + * Adds a listener that wants to be notified about property changes. + * + * @param PropertyChangedListener $listener + */ public function addPropertyChangedListener(PropertyChangedListener $listener); } diff --git a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php index 855e5c7cc..2346f1b56 100644 --- a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -1005,7 +1005,7 @@ abstract class AbstractPlatform $queryFields = $this->getFieldDeclarationListSql($columns); if (isset($options['primary']) && ! empty($options['primary'])) { - $queryFields .= ', PRIMARY KEY(' . implode(', ', array_values($options['primary'])) . ')'; + $queryFields .= ', PRIMARY KEY(' . implode(', ', array_unique(array_values($options['primary']))) . ')'; } if (isset($options['indexes']) && ! empty($options['indexes'])) { diff --git a/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php b/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php index 649cbe5fb..6652af58c 100644 --- a/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php @@ -641,7 +641,7 @@ class MySqlPlatform extends AbstractPlatform // attach all primary keys if (isset($options['primary']) && ! empty($options['primary'])) { - $keyColumns = array_values($options['primary']); + $keyColumns = array_unique(array_values($options['primary'])); $keyColumns = array_map(array($this, 'quoteIdentifier'), $keyColumns); $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; } diff --git a/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php b/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php index 13e97e6db..f25a8c0f7 100644 --- a/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php +++ b/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php @@ -504,7 +504,7 @@ class SqlitePlatform extends AbstractPlatform } if ( ! $autoinc && isset($options['primary']) && ! empty($options['primary'])) { - $keyColumns = array_values($options['primary']); + $keyColumns = array_unique(array_values($options['primary'])); $keyColumns = array_map(array($this, 'quoteIdentifier'), $keyColumns); $queryFields.= ', PRIMARY KEY('.implode(', ', $keyColumns).')'; } diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 99608412e..8045368f5 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -131,11 +131,7 @@ abstract class AbstractQuery } /** - * Frees the resources used by the query object. It especially breaks a - * cyclic reference between the query object and it's parsers. This enables - * PHP's current GC to reclaim the memory. - * This method can therefore be used to reduce memory usage when creating a lot - * of query objects during a request. + * Frees the resources used by the query object. */ public function free() { @@ -413,7 +409,7 @@ abstract class AbstractQuery * Alias for getSingleResult(HYDRATE_SINGLE_SCALAR). * * @return mixed - * @throws QueryException If the query result is not unique. + * @throws QueryException If the query result is not unique. */ public function getSingleScalarResult() { @@ -462,9 +458,8 @@ abstract class AbstractQuery /** * Executes the query. * - * @param string $params Parameters to be sent to query. - * @param integer $hydrationMode Doctrine processing mode to be used during hydration process. - * One of the Query::HYDRATE_* constants. + * @param string $params Any additional query parameters. + * @param integer $hydrationMode Processing mode to be used during the hydration process. * @return mixed */ public function execute($params = array(), $hydrationMode = null) diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 55fa31206..1cbd131a3 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -21,6 +21,7 @@ namespace Doctrine\ORM\Internal\Hydration; +use Doctrine\Common\DoctrineException; use \PDO; /** diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index 6bf105327..dd386a96e 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -869,7 +869,7 @@ final class ClassMetadata */ public function hasField($fieldName) { - return isset($this->_columnNames[$fieldName]); + return isset($this->_reflectionProperties[$fieldName]); } /** diff --git a/lib/Doctrine/ORM/NativeQuery.php b/lib/Doctrine/ORM/NativeQuery.php index 19a5cca36..3b138ff61 100644 --- a/lib/Doctrine/ORM/NativeQuery.php +++ b/lib/Doctrine/ORM/NativeQuery.php @@ -53,7 +53,7 @@ class NativeQuery extends AbstractQuery } /** - * Gets the SQL query/queries that correspond to this DQL query. + * Gets the SQL query. * * @return mixed The built sql query or an array of all sql queries. * @override @@ -64,7 +64,7 @@ class NativeQuery extends AbstractQuery } /** - * Executed the query. + * Executes the query. * * @param array $params * @return Statement The Statement handle. diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index 8406e10e2..60f0629ee 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -21,6 +21,7 @@ namespace Doctrine\ORM; +use Doctrine\Common\DoctrineException; use Doctrine\ORM\Mapping\AssociationMapping; /** @@ -127,7 +128,7 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection $this->_ownerClass = $em->getClassMetadata($type); if ($keyField !== null) { if ( ! $this->_ownerClass->hasField($keyField)) { - \Doctrine\Common\DoctrineException::updateMe("Invalid field '$keyField' can't be used as key."); + throw DoctrineException::updateMe("Invalid field '$keyField' can't be used as key."); } $this->_keyField = $keyField; } @@ -167,12 +168,12 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection $this->_owner = $entity; $this->_association = $assoc; if ($assoc->isInverseSide()) { - // for sure bidirectional + // for sure bi-directional $this->_backRefFieldName = $assoc->getMappedByFieldName(); } else { $targetClass = $this->_em->getClassMetadata($assoc->getTargetEntityName()); if ($targetClass->hasInverseAssociationMapping($assoc->getSourceFieldName())) { - // bidirectional + // bi-directional $this->_backRefFieldName = $targetClass->getInverseAssociationMapping( $assoc->getSourceFieldName())->getSourceFieldName(); } @@ -240,7 +241,7 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection * * @param mixed $value * @param string $key - * @return TRUE + * @return boolean Always TRUE. * @override */ public function add($value) @@ -255,7 +256,6 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection ->setValue($value, $this->_owner); } } else { - //TODO: Register collection as dirty with the UoW if necessary $this->_changed(); } @@ -263,7 +263,7 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection } /** - * Adds all entities of the other collection to this collection. + * Adds all elements of the other collection to this collection. * * @param object $otherCollection * @todo Impl @@ -280,11 +280,13 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection /** * INTERNAL: * Sets a flag that indicates whether the collection is currently being hydrated. - * This has the following consequences: + * + * If the flag is set to TRUE, this has the following consequences: + * * 1) During hydration, bidirectional associations are completed automatically * by setting the back reference. * 2) During hydration no change notifications are reported to the UnitOfWork. - * I.e. that means add() etc. do not cause the collection to be scheduled + * That means add() etc. do not cause the collection to be scheduled * for an update. * * @param boolean $bool @@ -295,11 +297,8 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection } /** - * INTERNAL: Takes a snapshot from this collection. - * - * Snapshots are used for diff processing, for example - * when a fetched collection has three elements, then two of those - * are being removed the diff would contain one element. + * INTERNAL: + * Tells this collection to take a snapshot of its current state. */ public function takeSnapshot() { @@ -385,12 +384,23 @@ final class PersistentCollection extends \Doctrine\Common\Collections\Collection //$this->_em->getUnitOfWork()->scheduleCollectionUpdate($this); }*/ } - + + /** + * Gets a boolean flag indicating whether this colleciton is dirty which means + * its state needs to be synchronized with the database. + * + * @return boolean TRUE if the collection is dirty, FALSE otherwise. + */ public function isDirty() { return $this->_isDirty; } - + + /** + * Sets a boolean flag, indicating whether this collection is dirty. + * + * @param boolean $dirty Whether the collection should be marked dirty or not. + */ public function setDirty($dirty) { $this->_isDirty = $dirty; diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 872cba4ee..3cbc87845 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -36,8 +36,6 @@ use Doctrine\Common\DoctrineException; */ class SqlWalker { - //const SQLALIAS_SEPARATOR = '__'; - private $_resultSetMapping; private $_aliasCounter = 0; private $_tableAliasCounter = 0; @@ -47,9 +45,16 @@ class SqlWalker private $_query; private $_dqlToSqlAliasMap = array(); /** Map of all components/classes that appear in the DQL query. */ - private $_queryComponents = array(); + private $_queryComponents; /** A list of classes that appear in non-scalar SelectExpressions. */ private $_selectedClasses = array(); + /** + * The DQL alias of the root class of the currently traversed query. + * TODO: May need to be turned into a stack for usage in subqueries + */ + private $_currentRootAlias; + /** Flag that indicates whether to generate SQL table aliases in the SQL. */ + private $_useSqlTableAliases = true; /** * Initializes a new SqlWalker instance with the given Query and ParserResult. @@ -72,6 +77,9 @@ class SqlWalker }*/ } + /** + * @return Connection + */ public function getConnection() { return $this->_em->getConnection(); @@ -86,7 +94,12 @@ class SqlWalker { $sql = $this->walkSelectClause($AST->getSelectClause()); $sql .= $this->walkFromClause($AST->getFromClause()); - $sql .= $AST->getWhereClause() ? $this->walkWhereClause($AST->getWhereClause()) : ''; + if ($whereClause = $AST->getWhereClause()) { + $sql .= $this->walkWhereClause($whereClause); + } else if ($discSql = $this->_generateDiscriminatorColumnConditionSql($this->_currentRootAlias)) { + $sql .= ' WHERE ' . $discSql; + } + //$sql .= $AST->getWhereClause() ? $this->walkWhereClause($AST->getWhereClause()) : ''; $sql .= $AST->getGroupByClause() ? $this->walkGroupByClause($AST->getGroupByClause()) : ''; $sql .= $AST->getHavingClause() ? $this->walkHavingClause($AST->getHavingClause()) : ''; $sql .= $AST->getOrderByClause() ? $this->walkOrderByClause($AST->getOrderByClause()) : ''; @@ -142,6 +155,8 @@ class SqlWalker $rangeDecl = $firstIdentificationVarDecl->getRangeVariableDeclaration(); $dqlAlias = $rangeDecl->getAliasIdentificationVariable(); + $this->_currentRootAlias = $dqlAlias; + $sql .= $rangeDecl->getClassMetadata()->getTableName() . ' ' . $this->getSqlTableAlias($rangeDecl->getClassMetadata()->getTableName()); @@ -254,6 +269,11 @@ class SqlWalker } else { // ManyToMany //TODO } + + $discrSql = $this->_generateDiscriminatorColumnConditionSql($joinedDqlAlias); + if ($discrSql) { + $sql .= ' AND ' . $discrSql; + } return $sql; } @@ -291,8 +311,7 @@ class SqlWalker $columnName = $class->getColumnName($fieldName); $columnAlias = $this->getSqlColumnAlias($columnName); $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; - - // Register column in ResultSetMapping + $this->_resultSetMapping->addFieldResult($dqlAlias, $columnAlias, $fieldName); } else if ($expr->isSimpleStateFieldAssociationPathExpression()) { @@ -321,6 +340,8 @@ class SqlWalker $sql .= $this->walkFunction($expr) . ' AS ' . $columnAlias; $this->_resultSetMapping->addScalarResult($columnAlias, $resultAlias); } else { + // $expr is an IdentificationVariable + $dqlAlias = $expr; $queryComp = $this->_queryComponents[$dqlAlias]; $class = $queryComp['metadata']; @@ -331,6 +352,7 @@ class SqlWalker $sqlTableAlias = $this->getSqlTableAlias($class->getTableName()); + // Gather all fields $fieldMappings = $class->getFieldMappings(); foreach ($class->getSubclasses() as $subclassName) { $fieldMappings = array_merge( @@ -348,7 +370,6 @@ class SqlWalker } $columnAlias = $this->getSqlColumnAlias($fieldMapping['columnName']); $sql .= $sqlTableAlias . '.' . $fieldMapping['columnName'] . ' AS ' . $columnAlias; - $this->_resultSetMapping->addFieldResult($dqlAlias, $columnAlias, $fieldName); } } @@ -378,12 +399,15 @@ class SqlWalker */ public function walkSubselect($subselect) { + $useAliasesBefore = $this->_useSqlTableAliases; + $this->_useSqlTableAliases = true; $sql = $this->walkSimpleSelectClause($subselect->getSimpleSelectClause()); $sql .= $this->walkSubselectFromClause($subselect->getSubselectFromClause()); $sql .= $subselect->getWhereClause() ? $this->walkWhereClause($subselect->getWhereClause()) : ''; $sql .= $subselect->getGroupByClause() ? $this->walkGroupByClause($subselect->getGroupByClause()) : ''; $sql .= $subselect->getHavingClause() ? $this->walkHavingClause($subselect->getHavingClause()) : ''; $sql .= $subselect->getOrderByClause() ? $this->walkOrderByClause($subselect->getOrderByClause()) : ''; + $this->_useSqlTableAliases = $useAliasesBefore; return $sql; } @@ -512,8 +536,13 @@ class SqlWalker */ public function walkUpdateStatement(AST\UpdateStatement $AST) { + $this->_useSqlTableAliases = false; // TODO: Ask platform instead? $sql = $this->walkUpdateClause($AST->getUpdateClause()); - $sql .= $AST->getWhereClause() ? $this->walkWhereClause($AST->getWhereClause()) : ''; + if ($whereClause = $AST->getWhereClause()) { + $sql .= $this->walkWhereClause($whereClause); + } else if ($discSql = $this->_generateDiscriminatorColumnConditionSql($this->_currentRootAlias)) { + $sql .= ' WHERE ' . $discSql; + } return $sql; } @@ -525,8 +554,13 @@ class SqlWalker */ public function walkDeleteStatement(AST\DeleteStatement $AST) { + $this->_useSqlTableAliases = false; // TODO: Ask platform instead? $sql = $this->walkDeleteClause($AST->getDeleteClause()); - $sql .= $AST->getWhereClause() ? $this->walkWhereClause($AST->getWhereClause()) : ''; + if ($whereClause = $AST->getWhereClause()) { + $sql .= $this->walkWhereClause($whereClause); + } else if ($discSql = $this->_generateDiscriminatorColumnConditionSql($this->_currentRootAlias)) { + $sql .= ' WHERE ' . $discSql; + } return $sql; } @@ -541,9 +575,11 @@ class SqlWalker $sql = 'DELETE FROM '; $class = $this->_em->getClassMetadata($deleteClause->getAbstractSchemaName()); $sql .= $class->getTableName(); - if ($deleteClause->getAliasIdentificationVariable()) { + if ($this->_useSqlTableAliases) { $sql .= ' ' . $this->getSqlTableAlias($class->getTableName()); } + $this->_currentRootAlias = $deleteClause->getAliasIdentificationVariable(); + return $sql; } @@ -558,9 +594,11 @@ class SqlWalker $sql = 'UPDATE '; $class = $this->_em->getClassMetadata($updateClause->getAbstractSchemaName()); $sql .= $class->getTableName(); - if ($updateClause->getAliasIdentificationVariable()) { + if ($this->_useSqlTableAliases) { $sql .= ' ' . $this->getSqlTableAlias($class->getTableName()); } + $this->_currentRootAlias = $updateClause->getAliasIdentificationVariable(); + $sql .= ' SET ' . implode(', ', array_map(array($this, 'walkUpdateItem'), $updateClause->getUpdateItems())); @@ -581,9 +619,10 @@ class SqlWalker $this->_parserResult->getDefaultQueryComponentAlias(); $qComp = $this->_queryComponents[$dqlAlias]; - $sql .= $this->getSqlTableAlias($qComp['metadata']->getTableName()) . '.' - . $qComp['metadata']->getColumnName($updateItem->getField()) - . ' = '; + if ($this->_useSqlTableAliases) { + $sql .= $this->getSqlTableAlias($qComp['metadata']->getTableName()) . '.'; + } + $sql .= $qComp['metadata']->getColumnName($updateItem->getField()) . ' = '; $newValue = $updateItem->getNewValue(); @@ -610,8 +649,38 @@ class SqlWalker { $sql = ' WHERE '; $condExpr = $whereClause->getConditionalExpression(); + $sql .= implode(' OR ', array_map(array($this, 'walkConditionalTerm'), $condExpr->getConditionalTerms())); + + $discrSql = $this->_generateDiscriminatorColumnConditionSql($this->_currentRootAlias); + if ($discrSql) { + $sql .= ' AND ' . $discrSql; + } + + return $sql; + } + + private function _generateDiscriminatorColumnConditionSql($dqlAlias) + { + $sql = ''; + if ($dqlAlias) { + $class = $this->_queryComponents[$dqlAlias]['metadata']; + if ($class->isInheritanceTypeSingleTable()) { + $conn = $this->_em->getConnection(); + $values = array($conn->quote($class->getDiscriminatorValue())); + foreach ($class->getSubclasses() as $subclassName) { + $values[] = $conn->quote($this->_em->getClassMetadata($subclassName)->getDiscriminatorValue()); + } + $discrColumn = $class->getDiscriminatorColumn(); + if ($this->_useSqlTableAliases) { + $sql .= $this->getSqlTableAlias($class->getTableName()) . '.'; + } + $sql .= $discrColumn['name'] . ' IN (' . implode(', ', $values) . ')'; + } else if ($class->isInheritanceTypeJoined()) { + //TODO + } + } return $sql; } @@ -918,8 +987,11 @@ class SqlWalker } } - $sqlTableAlias = $this->getSqlTableAlias($class->getTableName()); - $sql .= $sqlTableAlias . '.' . $class->getColumnName($fieldName); + if ($this->_useSqlTableAliases) { + $sql .= $this->getSqlTableAlias($class->getTableName()) . '.'; + } + + $sql .= $class->getColumnName($fieldName); } else if ($pathExpr->isSimpleStateFieldAssociationPathExpression()) { throw DoctrineException::updateMe("Not yet implemented."); } else { diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index 1952394a4..4f5547c3c 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -87,74 +87,11 @@ class SchemaTool continue; } - $columns = $this->_gatherColumns($class); // table columns $options = array(); // table options + $columns = $this->_gatherColumns($class, $options); // table columns - foreach ($class->getAssociationMappings() as $mapping) { - $foreignClass = $this->_em->getClassMetadata($mapping->getTargetEntityName()); - if ($mapping->isOneToOne() && $mapping->isOwningSide()) { - $constraint = array(); - $constraint['tableName'] = $class->getTableName(); - $constraint['foreignTable'] = $foreignClass->getTableName(); - $constraint['local'] = array(); - $constraint['foreign'] = array(); - foreach ($mapping->getJoinColumns() as $joinColumn) { - $column = array(); - $column['name'] = $joinColumn['name']; - $column['type'] = $foreignClass->getTypeOfColumn($joinColumn['referencedColumnName']); - $columns[$joinColumn['name']] = $column; - $constraint['local'][] = $joinColumn['name']; - $constraint['foreign'][] = $joinColumn['referencedColumnName']; - } - $foreignKeyConstraints[] = $constraint; - } 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()) { - // create join table - $joinTableColumns = array(); - $joinTableOptions = array(); - $joinTable = $mapping->getJoinTable(); - $constraint1 = array(); - $constraint1['tableName'] = $joinTable['name']; - $constraint1['foreignTable'] = $class->getTableName(); - $constraint1['local'] = array(); - $constraint1['foreign'] = array(); - foreach ($joinTable['joinColumns'] as $joinColumn) { - $column = array(); - $column['primary'] = true; - $joinTableOptions['primary'][] = $joinColumn['name']; - $column['name'] = $joinColumn['name']; - $column['type'] = $class->getTypeOfColumn($joinColumn['referencedColumnName']); - $joinTableColumns[$joinColumn['name']] = $column; - $constraint1['local'][] = $joinColumn['name']; - $constraint1['foreign'][] = $joinColumn['referencedColumnName']; - } - $foreignKeyConstraints[] = $constraint1; - - $constraint2 = array(); - $constraint2['tableName'] = $joinTable['name']; - $constraint2['foreignTable'] = $foreignClass->getTableName(); - $constraint2['local'] = array(); - $constraint2['foreign'] = array(); - foreach ($joinTable['inverseJoinColumns'] as $inverseJoinColumn) { - $column = array(); - $column['primary'] = true; - $joinTableOptions['primary'][] = $inverseJoinColumn['name']; - $column['name'] = $inverseJoinColumn['name']; - $column['type'] = $this->_em->getClassMetadata($mapping->getTargetEntityName()) - ->getTypeOfColumn($inverseJoinColumn['referencedColumnName']); - $joinTableColumns[$inverseJoinColumn['name']] = $column; - $constraint2['local'][] = $inverseJoinColumn['name']; - $constraint2['foreign'][] = $inverseJoinColumn['referencedColumnName']; - } - $foreignKeyConstraints[] = $constraint2; - - $sql = array_merge($sql, $this->_platform->getCreateTableSql( - $joinTable['name'], $joinTableColumns, $joinTableOptions)); - } - } - + $this->_gatherRelationsSql($class, $sql, $columns, $foreignKeyConstraints); + if ($class->isInheritanceTypeSingleTable()) { // Add the discriminator column $discrColumnDef = $this->_getDiscriminatorColumnDefinition($class); @@ -166,7 +103,9 @@ class SchemaTool $processedClasses[$parentClassName] = true; } foreach ($class->getSubclasses() as $subClassName) { - $columns = array_merge($columns, $this->_gatherColumns($this->_em->getClassMetadata($subClassName))); + $subClass = $this->_em->getClassMetadata($subClassName); + $columns = array_merge($columns, $this->_gatherColumns($subClass, $options)); + $this->_gatherRelationsSql($subClass, $sql, $columns, $foreignKeyConstraints); $processedClasses[$subClassName] = true; } } else if ($class->isInheritanceTypeJoined()) { @@ -200,7 +139,7 @@ class SchemaTool ); } - private function _gatherColumns($class) + private function _gatherColumns($class, array &$options) { $columns = array(); foreach ($class->getFieldMappings() as $fieldName => $mapping) { @@ -218,9 +157,78 @@ class SchemaTool } $columns[$mapping['columnName']] = $column; } + return $columns; } + private function _gatherRelationsSql($class, array &$sql, array &$columns, array &$constraints) + { + foreach ($class->getAssociationMappings() as $mapping) { + $foreignClass = $this->_em->getClassMetadata($mapping->getTargetEntityName()); + if ($mapping->isOneToOne() && $mapping->isOwningSide()) { + $constraint = array(); + $constraint['tableName'] = $class->getTableName(); + $constraint['foreignTable'] = $foreignClass->getTableName(); + $constraint['local'] = array(); + $constraint['foreign'] = array(); + foreach ($mapping->getJoinColumns() as $joinColumn) { + $column = array(); + $column['name'] = $joinColumn['name']; + $column['type'] = $foreignClass->getTypeOfColumn($joinColumn['referencedColumnName']); + $columns[$joinColumn['name']] = $column; + $constraint['local'][] = $joinColumn['name']; + $constraint['foreign'][] = $joinColumn['referencedColumnName']; + } + $constraints[] = $constraint; + } 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()) { + // create join table + $joinTableColumns = array(); + $joinTableOptions = array(); + $joinTable = $mapping->getJoinTable(); + $constraint1 = array(); + $constraint1['tableName'] = $joinTable['name']; + $constraint1['foreignTable'] = $class->getTableName(); + $constraint1['local'] = array(); + $constraint1['foreign'] = array(); + foreach ($joinTable['joinColumns'] as $joinColumn) { + $column = array(); + $column['primary'] = true; + $joinTableOptions['primary'][] = $joinColumn['name']; + $column['name'] = $joinColumn['name']; + $column['type'] = $class->getTypeOfColumn($joinColumn['referencedColumnName']); + $joinTableColumns[$joinColumn['name']] = $column; + $constraint1['local'][] = $joinColumn['name']; + $constraint1['foreign'][] = $joinColumn['referencedColumnName']; + } + $constraints[] = $constraint1; + + $constraint2 = array(); + $constraint2['tableName'] = $joinTable['name']; + $constraint2['foreignTable'] = $foreignClass->getTableName(); + $constraint2['local'] = array(); + $constraint2['foreign'] = array(); + foreach ($joinTable['inverseJoinColumns'] as $inverseJoinColumn) { + $column = array(); + $column['primary'] = true; + $joinTableOptions['primary'][] = $inverseJoinColumn['name']; + $column['name'] = $inverseJoinColumn['name']; + $column['type'] = $this->_em->getClassMetadata($mapping->getTargetEntityName()) + ->getTypeOfColumn($inverseJoinColumn['referencedColumnName']); + $joinTableColumns[$inverseJoinColumn['name']] = $column; + $constraint2['local'][] = $inverseJoinColumn['name']; + $constraint2['foreign'][] = $inverseJoinColumn['referencedColumnName']; + } + $constraints[] = $constraint2; + + $sql = array_merge($sql, $this->_platform->getCreateTableSql( + $joinTable['name'], $joinTableColumns, $joinTableOptions)); + } + } + } + public function dropSchema(array $classes) { //TODO diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 41cbe000c..886b75497 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -333,10 +333,6 @@ class UnitOfWork implements PropertyChangedListener $oid = spl_object_hash($entity); $state = $this->getEntityState($entity); - if ($state == self::STATE_MANAGED && ($entity instanceof \Doctrine\Common\NotifyPropertyChanged)) { - continue; // entity notifies us, no need to calculate changes - } - // Look for changes in the entity itself by comparing against the // original data we have. if ($state == self::STATE_MANAGED || $state == self::STATE_NEW) { diff --git a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php index 05da85173..3f5ccf194 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SingleTableInheritanceTest.php @@ -15,21 +15,28 @@ class SingleTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase parent::setUp(); $this->_schemaTool->createSchema(array( $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\ParentEntity'), - $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\ChildEntity') + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\ChildEntity'), + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\RelatedEntity') )); } - public function testInsert() + public function testCRUD() { $parent = new ParentEntity; $parent->setData('foobar'); $this->_em->save($parent); - + + $relatedEntity = new RelatedEntity; + $relatedEntity->setName('theRelatedOne'); + + $this->_em->save($relatedEntity); + $child = new ChildEntity; $child->setData('thedata'); $child->setNumber(1234); + $child->setRelatedEntity($relatedEntity); $this->_em->save($child); @@ -43,9 +50,45 @@ class SingleTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals(2, count($entities)); $this->assertTrue($entities[0] instanceof ParentEntity); $this->assertTrue($entities[1] instanceof ChildEntity); + $this->assertTrue(is_numeric($entities[0]->getId())); + $this->assertTrue(is_numeric($entities[1]->getId())); $this->assertEquals('foobar', $entities[0]->getData()); $this->assertEquals('thedata', $entities[1]->getData()); $this->assertEquals(1234, $entities[1]->getNumber()); + + $this->_em->clear(); + + $query = $this->_em->createQuery("select e from Doctrine\Tests\ORM\Functional\ChildEntity e"); + + $entities = $query->getResultList(); + $this->assertEquals(1, count($entities)); + $this->assertTrue($entities[0] instanceof ChildEntity); + $this->assertTrue(is_numeric($entities[0]->getId())); + $this->assertEquals('thedata', $entities[0]->getData()); + $this->assertEquals(1234, $entities[0]->getNumber()); + + $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); } } @@ -91,6 +134,11 @@ class ChildEntity extends ParentEntity { * @DoctrineColumn(type="integer", nullable=true) */ private $number; + /** + * @DoctrineOneToOne(targetEntity="RelatedEntity") + * @DoctrineJoinColumn(name="related_entity_id", referencedColumnName="id") + */ + private $relatedEntity; public function getNumber() { return $this->number; @@ -99,5 +147,56 @@ class ChildEntity extends ParentEntity { public function setNumber($number) { $this->number = $number; } + + public function getRelatedEntity() { + return $this->relatedEntity; + } + + public function setRelatedEntity($relatedEntity) { + $this->relatedEntity = $relatedEntity; + $relatedEntity->setOwner($this); + } } +/** + * @DoctrineEntity + */ +class RelatedEntity { + /** + * @DoctrineId + * @DoctrineColumn(type="integer") + * @DoctrineGeneratedValue(strategy="auto") + */ + private $id; + /** + * @DoctrineColumn(type="varchar", length=50) + */ + private $name; + /** + * @DoctrineOneToOne(targetEntity="ChildEntity", mappedBy="relatedEntity") + */ + private $owner; + + public function getId() { + return $this->id; + } + + public function getName() { + return $this->name; + } + + public function setName($name) { + $this->name = $name; + } + + public function getOwner() { + return $this->owner; + } + + public function setOwner($owner) { + $this->owner = $owner; + if ($owner->getRelatedEntity() !== $this) { + $owner->setRelatedEntity($this); + } + } +} diff --git a/tests/Doctrine/Tests/ORM/Query/DeleteSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/DeleteSqlGenerationTest.php index 0d589cfe0..0a79fd20b 100644 --- a/tests/Doctrine/Tests/ORM/Query/DeleteSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/DeleteSqlGenerationTest.php @@ -60,11 +60,11 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( 'DELETE Doctrine\Tests\Models\CMS\CmsUser u', - 'DELETE FROM cms_users c0_' + 'DELETE FROM cms_users' ); $this->assertSqlGeneration( 'DELETE FROM Doctrine\Tests\Models\CMS\CmsUser u', - 'DELETE FROM cms_users c0_' + 'DELETE FROM cms_users' ); } @@ -72,7 +72,7 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( 'DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = ?1', - 'DELETE FROM cms_users c0_ WHERE c0_.id = ?' + 'DELETE FROM cms_users WHERE id = ?' ); } @@ -80,12 +80,12 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( 'DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.username = ?1 OR u.name = ?2', - 'DELETE FROM cms_users c0_ WHERE c0_.username = ? OR c0_.name = ?' + 'DELETE FROM cms_users WHERE username = ? OR name = ?' ); $this->assertSqlGeneration( 'DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = ?1 OR ( u.username = ?2 OR u.name = ?3)', - 'DELETE FROM cms_users c0_ WHERE c0_.id = ? OR (c0_.username = ? OR c0_.name = ?)' + 'DELETE FROM cms_users WHERE id = ? OR (username = ? OR name = ?)' ); //$this->assertSqlGeneration( @@ -98,7 +98,7 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( "delete from Doctrine\Tests\Models\CMS\CmsUser u where u.username = ?1", - "DELETE FROM cms_users c0_ WHERE c0_.username = ?" + "DELETE FROM cms_users WHERE username = ?" ); } @@ -106,7 +106,7 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.username = ?1 AND u.name = ?2", - "DELETE FROM cms_users c0_ WHERE c0_.username = ? AND c0_.name = ?" + "DELETE FROM cms_users WHERE username = ? AND name = ?" ); } @@ -114,17 +114,17 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE NOT u.id != ?1", - "DELETE FROM cms_users c0_ WHERE NOT c0_.id <> ?" + "DELETE FROM cms_users WHERE NOT id <> ?" ); $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE NOT ( u.id != ?1 )", - "DELETE FROM cms_users c0_ WHERE NOT (c0_.id <> ?)" + "DELETE FROM cms_users WHERE NOT (id <> ?)" ); $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE NOT ( u.id != ?1 AND u.username = ?2 )", - "DELETE FROM cms_users c0_ WHERE NOT (c0_.id <> ? AND c0_.username = ?)" + "DELETE FROM cms_users WHERE NOT (id <> ? AND username = ?)" ); } @@ -135,32 +135,32 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase // id = ? was already tested (see testDeleteWithWhere()) $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id > ?1", - "DELETE FROM cms_users c0_ WHERE c0_.id > ?" + "DELETE FROM cms_users WHERE id > ?" ); $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id >= ?1", - "DELETE FROM cms_users c0_ WHERE c0_.id >= ?" + "DELETE FROM cms_users WHERE id >= ?" ); $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id < ?1", - "DELETE FROM cms_users c0_ WHERE c0_.id < ?" + "DELETE FROM cms_users WHERE id < ?" ); $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id <= ?1", - "DELETE FROM cms_users c0_ WHERE c0_.id <= ?" + "DELETE FROM cms_users WHERE id <= ?" ); $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id <> ?1", - "DELETE FROM cms_users c0_ WHERE c0_.id <> ?" + "DELETE FROM cms_users WHERE id <> ?" ); $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id != ?1", - "DELETE FROM cms_users c0_ WHERE c0_.id <> ?" + "DELETE FROM cms_users WHERE id <> ?" ); } @@ -168,12 +168,12 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id NOT BETWEEN ?1 AND ?2", - "DELETE FROM cms_users c0_ WHERE c0_.id NOT BETWEEN ? AND ?" + "DELETE FROM cms_users WHERE id NOT BETWEEN ? AND ?" ); $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id BETWEEN ?1 AND ?2 AND u.username != ?3", - "DELETE FROM cms_users c0_ WHERE c0_.id BETWEEN ? AND ? AND c0_.username <> ?" + "DELETE FROM cms_users WHERE id BETWEEN ? AND ? AND username <> ?" ); } @@ -182,12 +182,12 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase // "WHERE" Expression LikeExpression $this->assertSqlGeneration( 'DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.username NOT LIKE ?1', - 'DELETE FROM cms_users c0_ WHERE c0_.username NOT LIKE ?' + 'DELETE FROM cms_users WHERE username NOT LIKE ?' ); $this->assertSqlGeneration( "DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.username LIKE ?1 ESCAPE '\\'", - "DELETE FROM cms_users c0_ WHERE c0_.username LIKE ? ESCAPE '\\'" + "DELETE FROM cms_users WHERE username LIKE ? ESCAPE '\\'" ); } @@ -196,12 +196,12 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase // "WHERE" Expression NullComparisonExpression $this->assertSqlGeneration( 'DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name IS NULL', - 'DELETE FROM cms_users c0_ WHERE c0_.name IS NULL' + 'DELETE FROM cms_users WHERE name IS NULL' ); $this->assertSqlGeneration( 'DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name IS NOT NULL', - 'DELETE FROM cms_users c0_ WHERE c0_.name IS NOT NULL' + 'DELETE FROM cms_users WHERE name IS NOT NULL' ); } @@ -209,12 +209,12 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( 'DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE 1 = 1', - 'DELETE FROM cms_users c0_ WHERE 1 = 1' + 'DELETE FROM cms_users WHERE 1 = 1' ); $this->assertSqlGeneration( 'DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE ?1 = 1', - 'DELETE FROM cms_users c0_ WHERE ? = 1' + 'DELETE FROM cms_users WHERE ? = 1' ); } @@ -222,12 +222,12 @@ class DeleteSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( 'DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id IN ( ?1, ?2, ?3, ?4 )', - 'DELETE FROM cms_users c0_ WHERE c0_.id IN (?, ?, ?, ?)' + 'DELETE FROM cms_users WHERE id IN (?, ?, ?, ?)' ); $this->assertSqlGeneration( 'DELETE Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id NOT IN ( ?1, ?2 )', - 'DELETE FROM cms_users c0_ WHERE c0_.id NOT IN (?, ?)' + 'DELETE FROM cms_users WHERE id NOT IN (?, ?)' ); } diff --git a/tests/Doctrine/Tests/ORM/Query/UpdateSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/UpdateSqlGenerationTest.php index 6646891f9..bb5f155bf 100644 --- a/tests/Doctrine/Tests/ORM/Query/UpdateSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/UpdateSqlGenerationTest.php @@ -60,11 +60,11 @@ class UpdateSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { $this->assertSqlGeneration( 'UPDATE Doctrine\Tests\Models\CMS\CmsUser u SET u.name = ?1', - 'UPDATE cms_users c0_ SET c0_.name = ?' + 'UPDATE cms_users SET name = ?' ); $this->assertSqlGeneration( 'UPDATE Doctrine\Tests\Models\CMS\CmsUser u SET u.name = ?1, u.username = ?2', - 'UPDATE cms_users c0_ SET c0_.name = ?, c0_.username = ?' + 'UPDATE cms_users SET name = ?, username = ?' ); }