From cd978fb8c95204e4dc4fbab2eb8804e993213bb0 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 20 Jun 2010 19:34:09 +0200 Subject: [PATCH] DDC-616 Made Database Reverse Engineering a Two-Step Approach, first collect details on all tables once and try to detect which tables are many-to-many tables. Then build metadata from this information. This allows to support even many-to-many tables in reverse engineering correctly --- .../ORM/Mapping/Driver/DatabaseDriver.php | 140 +++++++++++++++--- .../ORM/Functional/DatabaseDriverTest.php | 37 +++-- 2 files changed, 148 insertions(+), 29 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php index 99b4fa77d..bbbdc9b17 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php @@ -38,8 +38,20 @@ use Doctrine\Common\Cache\ArrayCache, */ class DatabaseDriver implements Driver { - /** The SchemaManager. */ + /** + * @var AbstractSchemaManager + */ private $_sm; + + /** + * @var array + */ + private $tables = null; + + /** + * @var array + */ + private $manyToManyTables = array(); /** * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading @@ -51,22 +63,69 @@ class DatabaseDriver implements Driver { $this->_sm = $schemaManager; } + + private function reverseEngineerMappingFromDatabase() + { + if ($this->tables !== null) { + return; + } + + foreach ($this->_sm->listTableNames() as $tableName) { + $tableName = strtolower($tableName); + $tables[$tableName] = $this->_sm->listTableDetails($tableName); + } + + $this->tables = array(); + foreach ($tables AS $name => $table) { + /* @var $table Table */ + if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $foreignKeys = $table->getForeignKeys(); + } else { + $foreignKeys = array(); + } + + $allForeignKeyColumns = array(); + foreach ($foreignKeys AS $foreignKey) { + $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); + } + + $pkColumns = $table->getPrimaryKey()->getColumns(); + sort($pkColumns); + sort($allForeignKeyColumns); + + if ($pkColumns == $allForeignKeyColumns) { + if (count($table->getForeignKeys()) != 2) { + throw new \InvalidArgumentException("ManyToMany tables with more or less than two foreign keys are not supported by the Database Reverese Engineering Driver."); + } + + $this->manyToManyTables[$name] = $table; + } else { + $this->tables[$name] = $table; + } + } + } /** * {@inheritdoc} */ public function loadMetadataForClass($className, ClassMetadataInfo $metadata) { - $tableName = $className; - $className = Inflector::classify(strtolower($tableName)); + $this->reverseEngineerMappingFromDatabase(); + + $tableName = Inflector::tableize($className); $metadata->name = $className; $metadata->table['name'] = $tableName; - $columns = $this->_sm->listTableColumns($tableName); + if (!isset($this->tables[$tableName])) { + throw new \InvalidArgumentException("Unknown table " . $tableName . " referred from class " . $className); + } + + $columns = $this->tables[$tableName]->getColumns(); + $indexes = $this->tables[$tableName]->getIndexes(); if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { - $foreignKeys = $this->_sm->listTableForeignKeys($tableName); + $foreignKeys = $this->tables[$tableName]->getForeignKeys(); } else { $foreignKeys = array(); } @@ -76,8 +135,6 @@ class DatabaseDriver implements Driver $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); } - $indexes = $this->_sm->listTableIndexes($tableName); - $ids = array(); $fieldMappings = array(); foreach ($columns as $column) { @@ -121,15 +178,64 @@ class DatabaseDriver implements Driver $metadata->mapField($fieldMapping); } - foreach ($foreignKeys as $foreignKey) { - $cols = $foreignKey->getColumns(); - $localColumn = current($cols); + foreach ($this->manyToManyTables AS $manyTable) { + foreach ($manyTable->getForeignKeys() AS $foreignKey) { + if ($tableName == strtolower($foreignKey->getForeignTableName())) { + $myFk = $foreignKey; + foreach ($manyTable->getForeignKeys() AS $foreignKey) { + if ($foreignKey != $myFk) { + $otherFk = $foreignKey; + break; + } + } + $localColumn = current($myFk->getColumns()); + $associationMapping = array(); + $associationMapping['fieldName'] = Inflector::camelize(str_replace('_id', '', strtolower(current($otherFk->getColumns())))); + $associationMapping['targetEntity'] = Inflector::classify(strtolower($otherFk->getForeignTableName())); + if (current($manyTable->getColumns())->getName() == $localColumn) { + $associationMapping['inversedBy'] = Inflector::camelize(str_replace('_id', '', strtolower(current($myFk->getColumns())))); + $associationMapping['joinTable'] = array( + 'name' => strtolower($manyTable->getName()), + 'joinColumns' => array(), + 'inverseJoinColumns' => array(), + ); + + $fkCols = $myFk->getForeignColumns(); + $cols = $myFk->getColumns(); + for ($i = 0; $i < count($cols); $i++) { + $associationMapping['joinTable']['joinColumns'][] = array( + 'name' => $cols[$i], + 'referencedColumnName' => $fkCols[$i], + ); + } + + $fkCols = $otherFk->getForeignColumns(); + $cols = $otherFk->getColumns(); + for ($i = 0; $i < count($cols); $i++) { + $associationMapping['joinTable']['inverseJoinColumns'][] = array( + 'name' => $cols[$i], + 'referencedColumnName' => $fkCols[$i], + ); + } + } else { + $associationMapping['mappedBy'] = Inflector::camelize(str_replace('_id', '', strtolower(current($myFk->getColumns())))); + } + $metadata->mapManyToMany($associationMapping); + break; + } + } + } + + foreach ($foreignKeys as $foreignKey) { + $foreignTable = $foreignKey->getForeignTableName(); + $cols = $foreignKey->getColumns(); $fkCols = $foreignKey->getForeignColumns(); + $localColumn = current($cols); $associationMapping = array(); $associationMapping['fieldName'] = Inflector::camelize(str_replace('_id', '', strtolower($localColumn))); - $associationMapping['targetEntity'] = Inflector::classify($foreignKey->getForeignTableName()); + $associationMapping['targetEntity'] = Inflector::classify($foreignTable); for ($i = 0; $i < count($cols); $i++) { $associationMapping['joinColumns'][] = array( @@ -137,7 +243,6 @@ class DatabaseDriver implements Driver 'referencedColumnName' => $fkCols[$i], ); } - $metadata->mapManyToOne($associationMapping); } } @@ -153,19 +258,14 @@ class DatabaseDriver implements Driver /** * Return all the class names supported by this driver. * - * IMPORTANT: This method must return an array of table names because we need - * to know the table name after we inflect it to create the entity class name. + * IMPORTANT: This method must return an array of class not tables names. * * @return array */ public function getAllClassNames() { - $classes = array(); - - foreach ($this->_sm->listTableNames() as $tableName) { - $classes[] = $tableName; - } + $this->reverseEngineerMappingFromDatabase(); - return $classes; + return array_map(array('Doctrine\Common\Util\Inflector', 'classify'), array_keys($this->tables)); } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php index 28488f6a9..6f02328dc 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php @@ -16,6 +16,7 @@ class DatabaseDriverTest extends \Doctrine\Tests\OrmFunctionalTestCase public function setUp() { + $this->useModelSet('cms'); parent::setUp(); $this->_sm = $this->_em->getConnection()->getSchemaManager(); @@ -36,8 +37,8 @@ class DatabaseDriverTest extends \Doctrine\Tests\OrmFunctionalTestCase $metadatas = $this->extractClassMetadata(array("DbdriverFoo")); - $this->assertArrayHasKey('dbdriver_foo', $metadatas); - $metadata = $metadatas['dbdriver_foo']; + $this->assertArrayHasKey('DbdriverFoo', $metadatas); + $metadata = $metadatas['DbdriverFoo']; $this->assertArrayHasKey('id', $metadata->fieldMappings); $this->assertEquals('id', $metadata->fieldMappings['id']['fieldName']); @@ -74,8 +75,8 @@ class DatabaseDriverTest extends \Doctrine\Tests\OrmFunctionalTestCase $metadatas = $this->extractClassMetadata(array("DbdriverBar", "DbdriverBaz")); - $this->assertArrayHasKey('dbdriver_baz', $metadatas); - $bazMetadata = $metadatas['dbdriver_baz']; + $this->assertArrayHasKey('DbdriverBaz', $metadatas); + $bazMetadata = $metadatas['DbdriverBaz']; $this->assertArrayNotHasKey('barId', $bazMetadata->fieldMappings, "The foreign Key field should not be inflected as 'barId' field, its an association."); $this->assertArrayHasKey('id', $bazMetadata->fieldMappings); @@ -86,6 +87,24 @@ class DatabaseDriverTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertType('Doctrine\ORM\Mapping\OneToOneMapping', $bazMetadata->associationMappings['bar']); } + public function testDetectManyToManyTables() + { + if (!$this->_em->getConnection()->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $this->markTestSkipped('Platform does not support foreign keys.'); + } + + $metadatas = $this->extractClassMetadata(array("CmsUsers", "CmsGroups")); + + $this->assertArrayHasKey('CmsUsers', $metadatas, 'CmsUsers entity was not detected.'); + $this->assertArrayHasKey('CmsGroups', $metadatas, 'CmsGroups entity was not detected.'); + + $this->assertEquals(1, count($metadatas['CmsUsers']->associationMappings)); + $this->assertArrayHasKey('group', $metadatas['CmsUsers']->associationMappings); + $this->assertEquals(1, count($metadatas['CmsGroups']->associationMappings)); + $this->assertArrayHasKey('user', $metadatas['CmsGroups']->associationMappings); + } + + /** * * @param string $className @@ -97,13 +116,13 @@ class DatabaseDriverTest extends \Doctrine\Tests\OrmFunctionalTestCase $metadatas = array(); $driver = new \Doctrine\ORM\Mapping\Driver\DatabaseDriver($this->_sm); - foreach ($driver->getAllClassNames() as $dbTableName) { - if (!in_array(strtolower(Inflector::classify($dbTableName)), $classNames)) { + foreach ($driver->getAllClassNames() as $className) { + if (!in_array(strtolower($className), $classNames)) { continue; } - $class = new ClassMetadataInfo($dbTableName); - $driver->loadMetadataForClass($dbTableName, $class); - $metadatas[strtolower($dbTableName)] = $class; + $class = new ClassMetadataInfo($className); + $driver->loadMetadataForClass($className, $class); + $metadatas[$className] = $class; } if (count($metadatas) != count($classNames)) {