From 91e47027720bbf387e724a5468bdc7284a8ef8b5 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Sat, 25 Feb 2012 23:13:46 -0200 Subject: [PATCH] named native query metadata --- .../ORM/Mapping/ClassMetadataInfo.php | 112 +++++++++++++++ lib/Doctrine/ORM/Mapping/MappingException.php | 34 ++++- .../Tests/ORM/Mapping/ClassMetadataTest.php | 131 ++++++++++++++++++ 3 files changed, 270 insertions(+), 7 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 80498b737..eb5b014bc 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -49,16 +49,19 @@ class ClassMetadataInfo implements ClassMetadata * and therefore does not need an inheritance mapping type. */ const INHERITANCE_TYPE_NONE = 1; + /** * JOINED means the class will be persisted according to the rules of * Class Table Inheritance. */ const INHERITANCE_TYPE_JOINED = 2; + /** * SINGLE_TABLE means the class will be persisted according to the rules of * Single Table Inheritance. */ const INHERITANCE_TYPE_SINGLE_TABLE = 3; + /** * TABLE_PER_CLASS means the class will be persisted according to the rules * of Concrete Table Inheritance. @@ -71,17 +74,20 @@ class ClassMetadataInfo implements ClassMetadata * Offers full portability. */ const GENERATOR_TYPE_AUTO = 1; + /** * SEQUENCE means a separate sequence object will be used. Platforms that do * not have native sequence support may emulate it. Full portability is currently * not guaranteed. */ const GENERATOR_TYPE_SEQUENCE = 2; + /** * TABLE means a separate table is used for id generation. * Offers full portability. */ const GENERATOR_TYPE_TABLE = 3; + /** * IDENTITY means an identity column is used for id generation. The database * will fill in the id column on insertion. Platforms that do not support @@ -89,11 +95,13 @@ class ClassMetadataInfo implements ClassMetadata * not guaranteed. */ const GENERATOR_TYPE_IDENTITY = 4; + /** * NONE means the class does not have a generated id. That means the class * must have a natural, manually assigned id. */ const GENERATOR_TYPE_NONE = 5; + /** * UUID means that a UUID/GUID expression is used for id generation. Full * portability is currently not guaranteed. @@ -111,53 +119,64 @@ class ClassMetadataInfo implements ClassMetadata * This is the default change tracking policy. */ const CHANGETRACKING_DEFERRED_IMPLICIT = 1; + /** * DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time * by doing a property-by-property comparison with the original data. This will * be done only for entities that were explicitly saved (through persist() or a cascade). */ const CHANGETRACKING_DEFERRED_EXPLICIT = 2; + /** * NOTIFY means that Doctrine relies on the entities sending out notifications * when their properties change. Such entity classes must implement * the NotifyPropertyChanged interface. */ const CHANGETRACKING_NOTIFY = 3; + /** * Specifies that an association is to be fetched when it is first accessed. */ const FETCH_LAZY = 2; + /** * Specifies that an association is to be fetched when the owner of the * association is fetched. */ const FETCH_EAGER = 3; + /** * Specifies that an association is to be fetched lazy (on first access) and that * commands such as Collection#count, Collection#slice are issued directly against * the database if the collection is not yet initialized. */ const FETCH_EXTRA_LAZY = 4; + /** * Identifies a one-to-one association. */ const ONE_TO_ONE = 1; + /** * Identifies a many-to-one association. */ const MANY_TO_ONE = 2; + /** * Identifies a one-to-many association. */ const ONE_TO_MANY = 4; + /** * Identifies a many-to-many association. */ const MANY_TO_MANY = 8; + /** * Combined bitmask for to-one (single-valued) associations. */ const TO_ONE = 3; + /** * Combined bitmask for to-many (collection-valued) associations. */ @@ -237,6 +256,21 @@ class ClassMetadataInfo implements ClassMetadata */ public $namedQueries = array(); + /** + * READ-ONLY: The named native queries allowed to be called directly from Repository. + * + * A native SQL named query definition has the following structure: + *
+     * array(
+     *     'name'                => ,
+     *     'query'               => ,
+     *     'resultClass'         => ,
+     *     'resultSetMapping'    => 
+     * )
+     * 
+ */ + public $namedNativeQueries = array(); + /** * READ-ONLY: The field names of all fields that are part of the identifier/primary key * of the mapped entity class. @@ -1051,6 +1085,33 @@ class ClassMetadataInfo implements ClassMetadata return $this->namedQueries; } + /** + * Gets the named native query. + * + * @see ClassMetadataInfo::$namedNativeQueries + * @throws MappingException + * @param string $queryName The query name + * @return array + */ + public function getNamedNativeQuery($queryName) + { + if ( ! isset($this->namedNativeQueries[$queryName])) { + throw MappingException::queryNotFound($this->name, $queryName); + } + + return $this->namedNativeQueries[$queryName]; + } + + /** + * Gets all named native queries of the class. + * + * @return array + */ + public function getNamedNativeQueries() + { + return $this->namedNativeQueries; + } + /** * Validates & completes the given field mapping. * @@ -1826,10 +1887,18 @@ class ClassMetadataInfo implements ClassMetadata */ public function addNamedQuery(array $queryMapping) { + if (!isset($queryMapping['name'])) { + throw MappingException::nameIsMandatoryQueryMapping($this->name); + } + if (isset($this->namedQueries[$queryMapping['name']])) { throw MappingException::duplicateQueryMapping($this->name, $queryMapping['name']); } + if (!isset($queryMapping['query'])) { + throw MappingException::emptyQueryMapping($this->name, $queryMapping['name']); + } + $name = $queryMapping['name']; $query = $queryMapping['query']; $dql = str_replace('__CLASS__', $this->name, $query); @@ -1840,6 +1909,38 @@ class ClassMetadataInfo implements ClassMetadata ); } + /** + * INTERNAL: + * Adds a named native query to this class. + * + * @throws MappingException + * @param array $queryMapping + */ + public function addNamedNativeQuery(array $queryMapping) + { + if (!isset($queryMapping['name'])) { + throw MappingException::nameIsMandatoryQueryMapping($this->name); + } + + if (isset($this->namedNativeQueries[$queryMapping['name']])) { + throw MappingException::duplicateQueryMapping($this->name, $queryMapping['name']); + } + + if (!isset($queryMapping['query'])) { + throw MappingException::emptyQueryMapping($this->name, $queryMapping['name']); + } + + if (!isset($queryMapping['resultClass']) && !isset($queryMapping['resultSetMapping'])) { + throw MappingException::missingQueryMapping($this->name, $queryMapping['name']); + } + + if (isset($queryMapping['resultClass']) && $queryMapping['resultClass'] === '__CLASS__') { + $queryMapping['resultClass'] = $this->name; + } + + $this->namedNativeQueries[$queryMapping['name']] = $queryMapping; + } + /** * Adds a one-to-one mapping. * @@ -2061,6 +2162,17 @@ class ClassMetadataInfo implements ClassMetadata return isset($this->namedQueries[$queryName]); } + /** + * Checks whether the class has a named native query with the given query name. + * + * @param string $fieldName + * @return boolean + */ + public function hasNamedNativeQuery($queryName) + { + return isset($this->namedNativeQueries[$queryName]); + } + /** * Checks whether the class has a mapped association with the given field name. * diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index 3e8022280..8d5ffba0e 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -93,6 +93,21 @@ class MappingException extends \Doctrine\ORM\ORMException return new self("No query found named '$queryName' on class '$className'."); } + public static function emptyQueryMapping($entity, $queryName) + { + return new self('Query named "'.$queryName.'" in "'.$entity.'" could not be empty.'); + } + + public static function nameIsMandatoryQueryMapping($className) + { + return new self("Query name on entity class '$className' is not defined."); + } + + public static function missingQueryMapping($entity, $queryName) + { + return new self('Query named "'.$queryName.'" in "'.$entity.' requires a result class or result set mapping.'); + } + public static function oneToManyRequiresMappedBy($fieldName) { return new self("OneToMany mapping on field '$fieldName' requires the 'mappedBy' attribute."); @@ -178,27 +193,31 @@ class MappingException extends \Doctrine\ORM\ORMException } /** - * * @param string $entity The entity's name * @param string $fieldName The name of the field that was already declared */ - public static function duplicateFieldMapping($entity, $fieldName) { + public static function duplicateFieldMapping($entity, $fieldName) + { return new self('Property "'.$fieldName.'" in "'.$entity.'" was already declared, but it must be declared only once'); } - public static function duplicateAssociationMapping($entity, $fieldName) { + public static function duplicateAssociationMapping($entity, $fieldName) + { return new self('Property "'.$fieldName.'" in "'.$entity.'" was already declared, but it must be declared only once'); } - public static function duplicateQueryMapping($entity, $queryName) { + public static function duplicateQueryMapping($entity, $queryName) + { return new self('Query named "'.$queryName.'" in "'.$entity.'" was already declared, but it must be declared only once'); } - public static function singleIdNotAllowedOnCompositePrimaryKey($entity) { + public static function singleIdNotAllowedOnCompositePrimaryKey($entity) + { return new self('Single id is not allowed on composite primary key in entity '.$entity); } - public static function unsupportedOptimisticLockingType($entity, $fieldName, $unsupportedType) { + public static function unsupportedOptimisticLockingType($entity, $fieldName, $unsupportedType) + { return new self('Locking type "'.$unsupportedType.'" (specified in "'.$entity.'", field "'.$fieldName.'") ' .'is not supported by Doctrine.' ); @@ -224,7 +243,8 @@ class MappingException extends \Doctrine\ORM\ORMException * @param string $owningClass The class that declares the discriminator map. * @return self */ - public static function invalidClassInDiscriminatorMap($className, $owningClass) { + public static function invalidClassInDiscriminatorMap($className, $owningClass) + { return new self( "Entity class '$className' used in the discriminator map of class '$owningClass' ". "does not exist." diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php index 4d7ec352f..11f96d712 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php @@ -525,6 +525,59 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $this->assertFalse($cm->hasNamedQuery('userById')); } + /** + * @group DDC-1663 + */ + public function testRetrieveOfNamedNativeQuery() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); + + $cm->addNamedNativeQuery(array( + 'name' => 'find-all', + 'query' => 'SELECT * FROM cms_users', + 'resultSetMapping' => 'result-mapping-name', + 'resultClass' => 'Doctrine\Tests\Models\CMS\CmsUser', + )); + + $cm->addNamedNativeQuery(array( + 'name' => 'find-by-id', + 'query' => 'SELECT * FROM cms_users WHERE id = ?', + 'resultClass' => '__CLASS__', + 'resultSetMapping' => 'result-mapping-name', + )); + + $mapping = $cm->getNamedNativeQuery('find-all'); + $this->assertEquals('SELECT * FROM cms_users', $mapping['query']); + $this->assertEquals('result-mapping-name', $mapping['resultSetMapping']); + $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $mapping['resultClass']); + + $mapping = $cm->getNamedNativeQuery('find-by-id'); + $this->assertEquals('SELECT * FROM cms_users WHERE id = ?', $mapping['query']); + $this->assertEquals('result-mapping-name', $mapping['resultSetMapping']); + $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $mapping['resultClass']); + } + + /** + * @group DDC-1663 + */ + public function testExistanceOfNamedNativeQuery() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); + + + $cm->addNamedNativeQuery(array( + 'name' => 'find-all', + 'query' => 'SELECT * FROM cms_users', + 'resultClass' => 'Doctrine\Tests\Models\CMS\CmsUser', + 'resultSetMapping' => 'result-mapping-name' + )); + + $this->assertTrue($cm->hasNamedNativeQuery('find-all')); + $this->assertFalse($cm->hasNamedNativeQuery('find-by-id')); + } + public function testRetrieveOfNamedQuery() { $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); @@ -539,6 +592,26 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $this->assertEquals('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = ?1', $cm->getNamedQuery('userById')); } + /** + * @group DDC-1663 + */ + public function testRetrievalOfNamedNativeQueries() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); + + $this->assertEquals(0, count($cm->getNamedNativeQueries())); + + $cm->addNamedNativeQuery(array( + 'name' => 'find-all', + 'query' => 'SELECT * FROM cms_users', + 'resultClass' => 'Doctrine\Tests\Models\CMS\CmsUser', + 'resultSetMapping' => 'result-mapping-name' + )); + + $this->assertEquals(1, count($cm->getNamedNativeQueries())); + } + public function testNamingCollisionNamedQueryShouldThrowException() { $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); @@ -558,6 +631,32 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase )); } + /** + * @group DDC-1663 + */ + public function testNamingCollisionNamedNativeQueryShouldThrowException() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); + + + $this->setExpectedException('Doctrine\ORM\Mapping\MappingException'); + + $cm->addNamedNativeQuery(array( + 'name' => 'find-all', + 'query' => 'SELECT * FROM cms_users', + 'resultClass' => 'Doctrine\Tests\Models\CMS\CmsUser', + 'resultSetMapping' => 'result-mapping-name' + )); + + $cm->addNamedNativeQuery(array( + 'name' => 'find-all', + 'query' => 'SELECT * FROM cms_users', + 'resultClass' => 'Doctrine\Tests\Models\CMS\CmsUser', + 'resultSetMapping' => 'result-mapping-name' + )); + } + /** * @group DDC-1068 */ @@ -596,6 +695,38 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $cm->validateAssocations(); } + /** + * @group DDC-1663 + * + * @expectedException \Doctrine\ORM\Mapping\MappingException + * @expectedExceptionMessage Query name on entity class 'Doctrine\Tests\Models\CMS\CmsUser' is not defined. + */ + public function testNameIsMandatoryForNamedQueryMappingException() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); + $cm->addNamedQuery(array( + 'query' => 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u', + )); + } + + /** + * @group DDC-1663 + * + * @expectedException \Doctrine\ORM\Mapping\MappingException + * @expectedExceptionMessage Query name on entity class 'Doctrine\Tests\Models\CMS\CmsUser' is not defined. + */ + public function testNameIsMandatoryForNameNativeQueryMappingException() + { + $cm = new ClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $cm->initializeReflection(new \Doctrine\Common\Persistence\Mapping\RuntimeReflectionService); + $cm->addNamedQuery(array( + 'query' => 'SELECT * FROM cms_users', + 'resultClass' => 'Doctrine\Tests\Models\CMS\CmsUser', + 'resultSetMapping' => 'result-mapping-name' + )); + } + /** * @expectedException \Doctrine\ORM\Mapping\MappingException * @expectedExceptionMessage Discriminator column name on entity class 'Doctrine\Tests\Models\CMS\CmsUser' is not defined.