From 4a3c7f05bf4319e17f355dcea2fdfa19d232ed5a Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 9 Oct 2024 17:13:58 +0200 Subject: [PATCH 01/10] Revert "Remove unused exception" This reverts commit 689da1f251957f49f1126b92bcaf8fb12df6f56e. --- src/Query/QueryException.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Query/QueryException.php b/src/Query/QueryException.php index ae945b167f..5c82b20a7a 100644 --- a/src/Query/QueryException.php +++ b/src/Query/QueryException.php @@ -88,6 +88,15 @@ public static function iterateWithFetchJoinCollectionNotAllowed(AssociationMappi ); } + public static function partialObjectsAreDangerous(): self + { + return new self( + 'Loading partial objects is dangerous. Fetch full objects or consider ' . + 'using a different fetch mode. If you really want partial objects, ' . + 'set the doctrine.forcePartialLoad query hint to TRUE.', + ); + } + /** * @param string[] $assoc * @psalm-param array $assoc From f71725575c605cb77ecc29c7131d5b39ab9e03ef Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 9 Oct 2024 17:34:59 +0200 Subject: [PATCH 02/10] Revert undprecate PARTIAL for objects in DQL. --- docs/en/index.rst | 1 + .../reference/dql-doctrine-query-language.rst | 24 ++++- docs/en/reference/partial-objects.rst | 98 +++++++++++++++++++ docs/en/sidebar.rst | 1 + src/Cache/DefaultQueryCache.php | 5 + src/Cache/Exception/FeatureNotImplemented.php | 5 + src/Decorator/EntityManagerDecorator.php | 1 + src/EntityManager.php | 1 + src/Query.php | 8 ++ src/Query/Parser.php | 5 +- src/Query/SqlWalker.php | 40 +++++--- src/UnitOfWork.php | 17 +++- ...PartialObjectHydrationPerformanceBench.php | 84 ++++++++++++++++ ...PartialObjectHydrationPerformanceBench.php | 72 ++++++++++++++ .../OneToOneUnidirectionalAssociationTest.php | 14 +++ .../ORM/Functional/PostLoadEventTest.php | 21 ++++ .../SecondLevelCacheQueryCacheTest.php | 25 +++++ .../ORM/Functional/Ticket/DDC163Test.php | 2 +- .../ORM/Functional/Ticket/DDC2519Test.php | 76 ++++++++++++++ .../ORM/Functional/Ticket/GH8443Test.php | 32 ++++++ .../Tests/ORM/Functional/ValueObjectsTest.php | 54 +++++++++- .../ORM/Query/LanguageRecognitionTest.php | 2 - tests/Tests/ORM/Query/ParserTest.php | 9 -- .../ORM/Query/SelectSqlGenerationTest.php | 82 +++++++++++++--- .../LimitSubqueryOutputWalkerTest.php | 2 +- tests/Tests/ORM/UnitOfWorkTest.php | 9 -- 26 files changed, 629 insertions(+), 61 deletions(-) create mode 100644 docs/en/reference/partial-objects.rst create mode 100644 tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php create mode 100644 tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php create mode 100644 tests/Tests/ORM/Functional/Ticket/DDC2519Test.php diff --git a/docs/en/index.rst b/docs/en/index.rst index 4d23062cd0..32f8c07017 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -74,6 +74,7 @@ Advanced Topics * :doc:`Improving Performance ` * :doc:`Caching ` * :doc:`Partial Hydration ` +* :doc:`Partial Objects ` * :doc:`Change Tracking Policies ` * :doc:`Best Practices ` * :doc:`Metadata Drivers ` diff --git a/docs/en/reference/dql-doctrine-query-language.rst b/docs/en/reference/dql-doctrine-query-language.rst index ab3cb13888..29d1a163d7 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -533,14 +533,23 @@ back. Instead, you receive only arrays as a flat rectangular result set, similar to how you would if you were just using SQL directly and joining some data. -If you want to select a partial number of fields for hydration entity in -the context of array hydration and joins you can use the ``partial`` DQL keyword: +If you want to select partial objects or fields in array hydration you can use the ``partial`` +DQL keyword: + +.. code-block:: php + + createQuery('SELECT partial u.{id, username} FROM CmsUser u'); + $users = $query->getResult(); // array of partially loaded CmsUser objects + +You use the partial syntax when joining as well: .. code-block:: php createQuery('SELECT partial u.{id, username}, partial a.{id, name} FROM CmsUser u JOIN u.articles a'); - $users = $query->getArrayResult(); // array of partially loaded CmsUser and CmsArticle fields + $usersArray = $query->getArrayResult(); // array of partially loaded CmsUser and CmsArticle fields + $users = $query->getResult(); // array of partially loaded CmsUser objects "NEW" Operator Syntax ^^^^^^^^^^^^^^^^^^^^^ @@ -1370,6 +1379,15 @@ exist mostly internal query hints that are not be consumed in userland. However the following few hints are to be used in userland: + +- ``Query::HINT_FORCE_PARTIAL_LOAD`` - Allows to hydrate objects + although not all their columns are fetched. This query hint can be + used to handle memory consumption problems with large result-sets + that contain char or binary data. Doctrine has no way of implicitly + reloading this data. Partially loaded objects have to be passed to + ``EntityManager::refresh()`` if they are to be reloaded fully from + the database. This query hint is deprecated and will be removed + in the future (\ `Details `_) - ``Query::HINT_REFRESH`` - This query is used internally by ``EntityManager::refresh()`` and can be used in userland as well. If you specify this hint and a query returns the data for an entity diff --git a/docs/en/reference/partial-objects.rst b/docs/en/reference/partial-objects.rst new file mode 100644 index 0000000000..51f173adf6 --- /dev/null +++ b/docs/en/reference/partial-objects.rst @@ -0,0 +1,98 @@ +Partial Objects +=============== + + +.. note:: + + Creating Partial Objects through DQL is deprecated and + will be removed in the future, use data transfer object + support in DQL instead. (\ `Details + `_) + +A partial object is an object whose state is not fully initialized +after being reconstituted from the database and that is +disconnected from the rest of its data. The following section will +describe why partial objects are problematic and what the approach +of Doctrine2 to this problem is. + +.. note:: + + The partial object problem in general does not apply to + methods or queries where you do not retrieve the query result as + objects. Examples are: ``Query#getArrayResult()``, + ``Query#getScalarResult()``, ``Query#getSingleScalarResult()``, + etc. + +.. warning:: + + Use of partial objects is tricky. Fields that are not retrieved + from the database will not be updated by the UnitOfWork even if they + get changed in your objects. You can only promote a partial object + to a fully-loaded object by calling ``EntityManager#refresh()`` + or a DQL query with the refresh flag. + + +What is the problem? +-------------------- + +In short, partial objects are problematic because they are usually +objects with broken invariants. As such, code that uses these +partial objects tends to be very fragile and either needs to "know" +which fields or methods can be safely accessed or add checks around +every field access or method invocation. The same holds true for +the internals, i.e. the method implementations, of such objects. +You usually simply assume the state you need in the method is +available, after all you properly constructed this object before +you pushed it into the database, right? These blind assumptions can +quickly lead to null reference errors when working with such +partial objects. + +It gets worse with the scenario of an optional association (0..1 to +1). When the associated field is NULL, you don't know whether this +object does not have an associated object or whether it was simply +not loaded when the owning object was loaded from the database. + +These are reasons why many ORMs do not allow partial objects at all +and instead you always have to load an object with all its fields +(associations being proxied). One secure way to allow partial +objects is if the programming language/platform allows the ORM tool +to hook deeply into the object and instrument it in such a way that +individual fields (not only associations) can be loaded lazily on +first access. This is possible in Java, for example, through +bytecode instrumentation. In PHP though this is not possible, so +there is no way to have "secure" partial objects in an ORM with +transparent persistence. + +Doctrine, by default, does not allow partial objects. That means, +any query that only selects partial object data and wants to +retrieve the result as objects (i.e. ``Query#getResult()``) will +raise an exception telling you that partial objects are dangerous. +If you want to force a query to return you partial objects, +possibly as a performance tweak, you can use the ``partial`` +keyword as follows: + +.. code-block:: php + + createQuery("select partial u.{id,name} from MyApp\Domain\User u"); + +You can also get a partial reference instead of a proxy reference by +calling: + +.. code-block:: php + + getPartialReference('MyApp\Domain\User', 1); + +Partial references are objects with only the identifiers set as they +are passed to the second argument of the ``getPartialReference()`` method. +All other fields are null. + +When should I force partial objects? +------------------------------------ + +Mainly for optimization purposes, but be careful of premature +optimization as partial objects lead to potentially more fragile +code. + + diff --git a/docs/en/sidebar.rst b/docs/en/sidebar.rst index 7ccdfb3a05..f52801c6b3 100644 --- a/docs/en/sidebar.rst +++ b/docs/en/sidebar.rst @@ -38,6 +38,7 @@ reference/native-sql reference/change-tracking-policies reference/partial-hydration + reference/partial-objects reference/attributes-reference reference/xml-mapping reference/php-mapping diff --git a/src/Cache/DefaultQueryCache.php b/src/Cache/DefaultQueryCache.php index f3bb8ac95c..08e703cd4b 100644 --- a/src/Cache/DefaultQueryCache.php +++ b/src/Cache/DefaultQueryCache.php @@ -16,6 +16,7 @@ use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Query; use Doctrine\ORM\Query\ResultSetMapping; +use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\UnitOfWork; use function array_map; @@ -210,6 +211,10 @@ public function put(QueryCacheKey $key, ResultSetMapping $rsm, mixed $result, ar throw FeatureNotImplemented::nonSelectStatements(); } + if (($hints[SqlWalker::HINT_PARTIAL] ?? false) === true || ($hints[Query::HINT_FORCE_PARTIAL_LOAD] ?? false) === true) { + throw FeatureNotImplemented::partialEntities(); + } + if (! ($key->cacheMode & Cache::MODE_PUT)) { return false; } diff --git a/src/Cache/Exception/FeatureNotImplemented.php b/src/Cache/Exception/FeatureNotImplemented.php index 8767d57419..7bae90b775 100644 --- a/src/Cache/Exception/FeatureNotImplemented.php +++ b/src/Cache/Exception/FeatureNotImplemented.php @@ -20,4 +20,9 @@ public static function nonSelectStatements(): self { return new self('Second-level cache query supports only select statements.'); } + + public static function partialEntities(): self + { + return new self('Second level cache does not support partial entities.'); + } } diff --git a/src/Decorator/EntityManagerDecorator.php b/src/Decorator/EntityManagerDecorator.php index 6f1b041968..212e01e511 100644 --- a/src/Decorator/EntityManagerDecorator.php +++ b/src/Decorator/EntityManagerDecorator.php @@ -8,6 +8,7 @@ use Doctrine\Common\EventManager; use Doctrine\DBAL\Connection; use Doctrine\DBAL\LockMode; +use Doctrine\Deprecations\Deprecation; use Doctrine\ORM\Cache; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManagerInterface; diff --git a/src/EntityManager.php b/src/EntityManager.php index 8045ac2f5e..9de636e5b7 100644 --- a/src/EntityManager.php +++ b/src/EntityManager.php @@ -9,6 +9,7 @@ use Doctrine\Common\EventManager; use Doctrine\DBAL\Connection; use Doctrine\DBAL\LockMode; +use Doctrine\Deprecations\Deprecation; use Doctrine\ORM\Exception\EntityManagerClosed; use Doctrine\ORM\Exception\InvalidHydrationMode; use Doctrine\ORM\Exception\MissingIdentifierField; diff --git a/src/Query.php b/src/Query.php index a869316d3e..d8a88e8fe8 100644 --- a/src/Query.php +++ b/src/Query.php @@ -70,6 +70,14 @@ class Query extends AbstractQuery */ public const HINT_REFRESH_ENTITY = 'doctrine.refresh.entity'; + /** + * The forcePartialLoad query hint forces a particular query to return + * partial objects. + * + * @todo Rename: HINT_OPTIMIZE + */ + public const HINT_FORCE_PARTIAL_LOAD = 'doctrine.forcePartialLoad'; + /** * The includeMetaColumns query hint causes meta columns like foreign keys and * discriminator columns to be selected and returned as part of the query result. diff --git a/src/Query/Parser.php b/src/Query/Parser.php index 875783c87d..12fe0c9769 100644 --- a/src/Query/Parser.php +++ b/src/Query/Parser.php @@ -5,6 +5,7 @@ namespace Doctrine\ORM\Query; use Doctrine\Common\Lexer\Token; +use Doctrine\Deprecations\Deprecation; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Internal\Hydration\HydrationException; use Doctrine\ORM\Mapping\AssociationMapping; @@ -1674,10 +1675,6 @@ public function JoinAssociationDeclaration(): AST\JoinAssociationDeclaration */ public function PartialObjectExpression(): AST\PartialObjectExpression { - if ($this->query->getHydrationMode() === Query::HYDRATE_OBJECT) { - throw HydrationException::partialObjectHydrationDisallowed(); - } - $this->match(TokenType::T_PARTIAL); $partialFieldSet = []; diff --git a/src/Query/SqlWalker.php b/src/Query/SqlWalker.php index 46296e719e..9beeff337b 100644 --- a/src/Query/SqlWalker.php +++ b/src/Query/SqlWalker.php @@ -321,6 +321,11 @@ private function generateClassTableInheritanceJoins( $sql .= implode(' AND ', array_filter($sqlParts)); } + // Ignore subclassing inclusion if partial objects is disallowed + if ($this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { + return $sql; + } + // LEFT JOIN child class tables foreach ($class->subClasses as $subClassName) { $subClass = $this->em->getClassMetadata($subClassName); @@ -659,7 +664,8 @@ public function walkSelectClause(AST\SelectClause $selectClause): string $this->query->setHint(self::HINT_DISTINCT, true); } - $addMetaColumns = $this->query->getHydrationMode() === Query::HYDRATE_OBJECT + $addMetaColumns = ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD) && + $this->query->getHydrationMode() === Query::HYDRATE_OBJECT || $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS); foreach ($this->selectedClasses as $selectedClass) { @@ -1398,28 +1404,30 @@ public function walkSelectExpression(AST\SelectExpression $selectExpression): st // 1) on Single Table Inheritance: always, since its marginal overhead // 2) on Class Table Inheritance only if partial objects are disallowed, // since it requires outer joining subtables. - foreach ($class->subClasses as $subClassName) { - $subClass = $this->em->getClassMetadata($subClassName); - $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); + if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { + foreach ($class->subClasses as $subClassName) { + $subClass = $this->em->getClassMetadata($subClassName); + $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); - foreach ($subClass->fieldMappings as $fieldName => $mapping) { - if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) { - continue; - } + foreach ($subClass->fieldMappings as $fieldName => $mapping) { + if (isset($mapping->inherited) || ($partialFieldSet && !in_array($fieldName, $partialFieldSet, true))) { + continue; + } - $columnAlias = $this->getSQLColumnAlias($mapping->columnName); - $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform); + $columnAlias = $this->getSQLColumnAlias($mapping->columnName); + $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform); - $col = $sqlTableAlias . '.' . $quotedColumnName; + $col = $sqlTableAlias . '.' . $quotedColumnName; - $type = Type::getType($mapping->type); - $col = $type->convertToPHPValueSQL($col, $this->platform); + $type = Type::getType($mapping->type); + $col = $type->convertToPHPValueSQL($col, $this->platform); - $sqlParts[] = $col . ' AS ' . $columnAlias; + $sqlParts[] = $col . ' AS ' . $columnAlias; - $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; + $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; - $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); + $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); + } } } diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 85a33620aa..811aa895b4 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -12,6 +12,7 @@ use Doctrine\DBAL; use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; use Doctrine\DBAL\LockMode; +use Doctrine\Deprecations\Deprecation; use Doctrine\ORM\Cache\Persister\CachedPersister; use Doctrine\ORM\Event\ListenersInvoker; use Doctrine\ORM\Event\OnClearEventArgs; @@ -2355,10 +2356,6 @@ public function isCollectionScheduledForDeletion(PersistentCollection $coll): bo */ public function createEntity(string $className, array $data, array &$hints = []): object { - if (isset($hints[SqlWalker::HINT_PARTIAL])) { - throw HydrationException::partialObjectHydrationDisallowed(); - } - $class = $this->em->getClassMetadata($className); $id = $this->identifierFlattener->flattenIdentifier($class, $data); @@ -2417,6 +2414,18 @@ public function createEntity(string $className, array $data, array &$hints = []) unset($this->eagerLoadingEntities[$class->rootEntityName]); } + // Properly initialize any unfetched associations, if partial objects are not allowed. + if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/issues/8471', + 'Partial Objects are deprecated (here entity %s)', + $className, + ); + + return $entity; + } + foreach ($class->associationMappings as $field => $assoc) { // Check if the association is not among the fetch-joined associations already. if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) { diff --git a/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php b/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php new file mode 100644 index 0000000000..6a1638afcf --- /dev/null +++ b/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php @@ -0,0 +1,84 @@ + '1', + 'u__status' => 'developer', + 'u__username' => 'romanb', + 'u__name' => 'Roman', + 'sclr0' => 'ROMANB', + 'p__phonenumber' => '42', + ], + [ + 'u__id' => '1', + 'u__status' => 'developer', + 'u__username' => 'romanb', + 'u__name' => 'Roman', + 'sclr0' => 'ROMANB', + 'p__phonenumber' => '43', + ], + [ + 'u__id' => '2', + 'u__status' => 'developer', + 'u__username' => 'romanb', + 'u__name' => 'Roman', + 'sclr0' => 'JWAGE', + 'p__phonenumber' => '91', + ], + ]; + + for ($i = 4; $i < 2000; ++$i) { + $resultSet[] = [ + 'u__id' => $i, + 'u__status' => 'developer', + 'u__username' => 'jwage', + 'u__name' => 'Jonathan', + 'sclr0' => 'JWAGE' . $i, + 'p__phonenumber' => '91', + ]; + } + + $this->result = ArrayResultFactory::createFromArray($resultSet); + $this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([])); + $this->rsm = new ResultSetMapping(); + + $this->rsm->addEntityResult(CmsUser::class, 'u'); + $this->rsm->addJoinedEntityResult(CmsPhonenumber::class, 'p', 'u', 'phonenumbers'); + $this->rsm->addFieldResult('u', 'u__id', 'id'); + $this->rsm->addFieldResult('u', 'u__status', 'status'); + $this->rsm->addFieldResult('u', 'u__username', 'username'); + $this->rsm->addFieldResult('u', 'u__name', 'name'); + $this->rsm->addScalarResult('sclr0', 'nameUpper'); + $this->rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber'); + } + + public function benchHydration(): void + { + $this->hydrator->hydrateAll($this->result, $this->rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + } +} diff --git a/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php b/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php new file mode 100644 index 0000000000..b894068eb2 --- /dev/null +++ b/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php @@ -0,0 +1,72 @@ + '1', + 'u__status' => 'developer', + 'u__username' => 'romanb', + 'u__name' => 'Roman', + ], + [ + 'u__id' => '1', + 'u__status' => 'developer', + 'u__username' => 'romanb', + 'u__name' => 'Roman', + ], + [ + 'u__id' => '2', + 'u__status' => 'developer', + 'u__username' => 'romanb', + 'u__name' => 'Roman', + ], + ]; + + for ($i = 4; $i < 10000; ++$i) { + $resultSet[] = [ + 'u__id' => $i, + 'u__status' => 'developer', + 'u__username' => 'jwage', + 'u__name' => 'Jonathan', + ]; + } + + $this->result = ArrayResultFactory::createFromArray($resultSet); + $this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([])); + $this->rsm = new ResultSetMapping(); + + $this->rsm->addEntityResult(CmsUser::class, 'u'); + $this->rsm->addFieldResult('u', 'u__id', 'id'); + $this->rsm->addFieldResult('u', 'u__status', 'status'); + $this->rsm->addFieldResult('u', 'u__username', 'username'); + $this->rsm->addFieldResult('u', 'u__name', 'name'); + } + + public function benchHydration(): void + { + $this->hydrator->hydrateAll($this->result, $this->rsm, [Query::HINT_FORCE_PARTIAL_LOAD => true]); + } +} diff --git a/tests/Tests/ORM/Functional/OneToOneUnidirectionalAssociationTest.php b/tests/Tests/ORM/Functional/OneToOneUnidirectionalAssociationTest.php index cdb34bfcdb..b2baaaa254 100644 --- a/tests/Tests/ORM/Functional/OneToOneUnidirectionalAssociationTest.php +++ b/tests/Tests/ORM/Functional/OneToOneUnidirectionalAssociationTest.php @@ -5,6 +5,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query; use Doctrine\Tests\Models\ECommerce\ECommerceProduct; use Doctrine\Tests\Models\ECommerce\ECommerceShipping; use Doctrine\Tests\OrmFunctionalTestCase; @@ -78,6 +79,19 @@ public function testLazyLoadsObjects(): void self::assertEquals(1, $product->getShipping()->getDays()); } + public function testDoesNotLazyLoadObjectsIfConfigurationDoesNotAllowIt(): void + { + $this->createFixture(); + + $query = $this->_em->createQuery('select p from Doctrine\Tests\Models\ECommerce\ECommerceProduct p'); + $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); + + $result = $query->getResult(); + $product = $result[0]; + + self::assertNull($product->getShipping()); + } + protected function createFixture(): void { $product = new ECommerceProduct(); diff --git a/tests/Tests/ORM/Functional/PostLoadEventTest.php b/tests/Tests/ORM/Functional/PostLoadEventTest.php index 839a831398..d76a044fd0 100644 --- a/tests/Tests/ORM/Functional/PostLoadEventTest.php +++ b/tests/Tests/ORM/Functional/PostLoadEventTest.php @@ -136,6 +136,27 @@ public function testLoadedProxyEntityShouldTriggerEvent(): void $userProxy->getName(); } + public function testLoadedProxyPartialShouldTriggerEvent(): void + { + $eventManager = $this->_em->getEventManager(); + + // Should not be invoked during getReference call + $mockListener = $this->createMock(PostLoadListener::class); + + // CmsUser (partially loaded), CmsAddress (inverse ToOne), 2 CmsPhonenumber + $mockListener + ->expects(self::exactly(4)) + ->method('postLoad') + ->will(self::returnValue(true)); + + $eventManager->addEventListener([Events::postLoad], $mockListener); + + $query = $this->_em->createQuery('SELECT PARTIAL u.{id, name}, p FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.phonenumbers p WHERE u.id = :id'); + + $query->setParameter('id', $this->userId); + $query->getResult(); + } + public function testLoadedProxyAssociationToOneShouldTriggerEvent(): void { $user = $this->_em->find(CmsUser::class, $this->userId); diff --git a/tests/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php index a3350aa5e6..e2dec64934 100644 --- a/tests/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php +++ b/tests/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -1086,6 +1086,31 @@ public function testHintClearEntityRegionDeleteStatement(): void self::assertFalse($this->cache->containsEntity(Country::class, $this->countries[1]->getId())); } + public function testCacheablePartialQueryException(): void + { + $this->expectException(CacheException::class); + $this->expectExceptionMessage('Second level cache does not support partial entities.'); + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->_em->createQuery('SELECT PARTIAL c.{id} FROM Doctrine\Tests\Models\Cache\Country c') + ->setCacheable(true) + ->getResult(); + } + + public function testCacheableForcePartialLoadHintQueryException(): void + { + $this->expectException(CacheException::class); + $this->expectExceptionMessage('Second level cache does not support partial entities.'); + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->_em->createQuery('SELECT c FROM Doctrine\Tests\Models\Cache\Country c') + ->setCacheable(true) + ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) + ->getResult(); + } + public function testNonCacheableQueryDeleteStatementException(): void { $this->expectException(CacheException::class); diff --git a/tests/Tests/ORM/Functional/Ticket/DDC163Test.php b/tests/Tests/ORM/Functional/Ticket/DDC163Test.php index 83aebb4dca..6c0c7c94fe 100644 --- a/tests/Tests/ORM/Functional/Ticket/DDC163Test.php +++ b/tests/Tests/ORM/Functional/Ticket/DDC163Test.php @@ -46,7 +46,7 @@ public function testQueryWithOrConditionUsingTwoRelationOnSameEntity(): void $this->_em->flush(); $this->_em->clear(); - $dql = 'SELECT person.name as person_name, spouse.name as spouse_name,friend.name as friend_name + $dql = 'SELECT PARTIAL person.{id,name}, PARTIAL spouse.{id,name}, PARTIAL friend.{id,name} FROM Doctrine\Tests\Models\Company\CompanyPerson person LEFT JOIN person.spouse spouse LEFT JOIN person.friends friend diff --git a/tests/Tests/ORM/Functional/Ticket/DDC2519Test.php b/tests/Tests/ORM/Functional/Ticket/DDC2519Test.php new file mode 100644 index 0000000000..b8dfba13b9 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/DDC2519Test.php @@ -0,0 +1,76 @@ +useModelSet('legacy'); + + parent::setUp(); + + $this->loadFixture(); + } + + #[Group('DDC-2519')] + public function testIssue(): void + { + $dql = 'SELECT PARTIAL l.{_source, _target} FROM Doctrine\Tests\Models\Legacy\LegacyUserReference l'; + $result = $this->_em->createQuery($dql)->getResult(); + + self::assertCount(2, $result); + self::assertInstanceOf(LegacyUserReference::class, $result[0]); + self::assertInstanceOf(LegacyUserReference::class, $result[1]); + + self::assertInstanceOf(LegacyUser::class, $result[0]->source()); + self::assertInstanceOf(LegacyUser::class, $result[0]->target()); + self::assertInstanceOf(LegacyUser::class, $result[1]->source()); + self::assertInstanceOf(LegacyUser::class, $result[1]->target()); + + self::assertTrue($this->isUninitializedObject($result[0]->target())); + self::assertTrue($this->isUninitializedObject($result[0]->source())); + self::assertTrue($this->isUninitializedObject($result[1]->target())); + self::assertTrue($this->isUninitializedObject($result[1]->source())); + + self::assertNotNull($result[0]->source()->getId()); + self::assertNotNull($result[0]->target()->getId()); + self::assertNotNull($result[1]->source()->getId()); + self::assertNotNull($result[1]->target()->getId()); + } + + public function loadFixture(): void + { + $user1 = new LegacyUser(); + $user1->username = 'FabioBatSilva'; + $user1->name = 'Fabio B. Silva'; + + $user2 = new LegacyUser(); + $user2->username = 'doctrinebot'; + $user2->name = 'Doctrine Bot'; + + $user3 = new LegacyUser(); + $user3->username = 'test'; + $user3->name = 'Tester'; + + $this->_em->persist($user1); + $this->_em->persist($user2); + $this->_em->persist($user3); + + $this->_em->flush(); + + $this->_em->persist(new LegacyUserReference($user1, $user2, 'foo')); + $this->_em->persist(new LegacyUserReference($user1, $user3, 'bar')); + + $this->_em->flush(); + $this->_em->clear(); + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/GH8443Test.php b/tests/Tests/ORM/Functional/Ticket/GH8443Test.php index 0b5352a6ea..44695f1856 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH8443Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH8443Test.php @@ -14,6 +14,9 @@ use Doctrine\ORM\Mapping\JoinColumn; use Doctrine\ORM\Mapping\OneToOne; use Doctrine\ORM\Mapping\Table; +use Doctrine\ORM\Query; +use Doctrine\Tests\Models\Company\CompanyManager; +use Doctrine\Tests\Models\Company\CompanyPerson; use Doctrine\Tests\OrmFunctionalTestCase; use PHPUnit\Framework\Attributes\Group; @@ -30,6 +33,35 @@ protected function setUp(): void $this->createSchemaForModels(GH8443Foo::class); } + #[Group('GH-8443')] + public function testJoinRootEntityWithForcePartialLoad(): void + { + $person = new CompanyPerson(); + $person->setName('John'); + + $manager = new CompanyManager(); + $manager->setName('Adam'); + $manager->setSalary(1000); + $manager->setDepartment('IT'); + $manager->setTitle('manager'); + + $manager->setSpouse($person); + + $this->_em->persist($person); + $this->_em->persist($manager); + $this->_em->flush(); + $this->_em->clear(); + + $manager = $this->_em->createQuery( + "SELECT m from Doctrine\Tests\Models\Company\CompanyManager m + JOIN m.spouse s + WITH s.name = 'John'", + )->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true)->getSingleResult(); + $this->_em->refresh($manager); + + $this->assertEquals('John', $manager->getSpouse()->getName()); + } + #[Group('GH-8443')] public function testJoinRootEntityWithOnlyOneEntityInHierarchy(): void { diff --git a/tests/Tests/ORM/Functional/ValueObjectsTest.php b/tests/Tests/ORM/Functional/ValueObjectsTest.php index 6656d916ee..3186e71d35 100644 --- a/tests/Tests/ORM/Functional/ValueObjectsTest.php +++ b/tests/Tests/ORM/Functional/ValueObjectsTest.php @@ -209,6 +209,58 @@ public function testDqlOnEmbeddedObjectsField(): void self::assertNull($this->_em->find(DDC93Person::class, $person->id)); } + public function testPartialDqlOnEmbeddedObjectsField(): void + { + $person = new DDC93Person('Karl', new DDC93Address('Foo', '12345', 'Gosport', new DDC93Country('England'))); + $this->_em->persist($person); + $this->_em->flush(); + $this->_em->clear(); + + // Prove that the entity was persisted correctly. + $dql = 'SELECT p FROM ' . __NAMESPACE__ . '\\DDC93Person p WHERE p.name = :name'; + + $person = $this->_em->createQuery($dql) + ->setParameter('name', 'Karl') + ->getSingleResult(); + + self::assertEquals('Gosport', $person->address->city); + self::assertEquals('Foo', $person->address->street); + self::assertEquals('12345', $person->address->zip); + self::assertEquals('England', $person->address->country->name); + + // Clear the EM and prove that the embeddable can be the subject of a partial query. + $this->_em->clear(); + + $dql = 'SELECT PARTIAL p.{id,address.city} FROM ' . __NAMESPACE__ . '\\DDC93Person p WHERE p.name = :name'; + + $person = $this->_em->createQuery($dql) + ->setParameter('name', 'Karl') + ->getSingleResult(); + + // Selected field must be equal, all other fields must be null. + self::assertEquals('Gosport', $person->address->city); + self::assertNull($person->address->street); + self::assertNull($person->address->zip); + self::assertNull($person->address->country); + self::assertNull($person->name); + + // Clear the EM and prove that the embeddable can be the subject of a partial query regardless of attributes positions. + $this->_em->clear(); + + $dql = 'SELECT PARTIAL p.{address.city, id} FROM ' . __NAMESPACE__ . '\\DDC93Person p WHERE p.name = :name'; + + $person = $this->_em->createQuery($dql) + ->setParameter('name', 'Karl') + ->getSingleResult(); + + // Selected field must be equal, all other fields must be null. + self::assertEquals('Gosport', $person->address->city); + self::assertNull($person->address->street); + self::assertNull($person->address->zip); + self::assertNull($person->address->country); + self::assertNull($person->name); + } + public function testDqlWithNonExistentEmbeddableField(): void { $this->expectException(QueryException::class); @@ -224,7 +276,7 @@ public function testPartialDqlWithNonExistentEmbeddableField(): void $this->expectExceptionMessage("no mapped field named 'address.asdfasdf'"); $this->_em->createQuery('SELECT PARTIAL p.{id,address.asdfasdf} FROM ' . __NAMESPACE__ . '\\DDC93Person p') - ->getArrayResult(); + ->execute(); } public function testEmbeddableWithInheritance(): void diff --git a/tests/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Tests/ORM/Query/LanguageRecognitionTest.php index ec57b1f138..de212ebdda 100644 --- a/tests/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Tests/ORM/Query/LanguageRecognitionTest.php @@ -532,13 +532,11 @@ public function testUnknownAbstractSchemaName(): void public function testCorrectPartialObjectLoad(): void { - $this->hydrationMode = AbstractQuery::HYDRATE_ARRAY; $this->assertValidDQL('SELECT PARTIAL u.{id,name} FROM Doctrine\Tests\Models\CMS\CmsUser u'); } public function testIncorrectPartialObjectLoadBecauseOfMissingIdentifier(): void { - $this->hydrationMode = AbstractQuery::HYDRATE_ARRAY; $this->assertInvalidDQL('SELECT PARTIAL u.{name} FROM Doctrine\Tests\Models\CMS\CmsUser u'); } diff --git a/tests/Tests/ORM/Query/ParserTest.php b/tests/Tests/ORM/Query/ParserTest.php index 6290bbc4da..ed75be517b 100644 --- a/tests/Tests/ORM/Query/ParserTest.php +++ b/tests/Tests/ORM/Query/ParserTest.php @@ -116,15 +116,6 @@ public function testNullLookahead(): void $parser->match(TokenType::T_SELECT); } - public function testPartialExpressionWithObjectHydratorThrows(): void - { - $this->expectException(HydrationException::class); - $this->expectExceptionMessage('Hydration of entity objects is not allowed when DQL PARTIAL keyword is used.'); - - $parser = $this->createParser(CmsUser::class); - $parser->PartialObjectExpression(); - } - private function createParser(string $dql): Parser { $query = new Query($this->getTestEntityManager()); diff --git a/tests/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Tests/ORM/Query/SelectSqlGenerationTest.php index 3969fe5163..6b7d958546 100644 --- a/tests/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -1335,8 +1335,6 @@ public function testIdentityFunctionWithCompositePrimaryKey(): void #[Group('DDC-2519')] public function testPartialWithAssociationIdentifier(): void { - $this->hydrationMode = ORMQuery::HYDRATE_ARRAY; - $this->assertSqlGeneration( 'SELECT PARTIAL l.{_source, _target} FROM Doctrine\Tests\Models\Legacy\LegacyUserReference l', 'SELECT l0_.iUserIdSource AS iUserIdSource_0, l0_.iUserIdTarget AS iUserIdTarget_1 FROM legacy_users_reference l0_', @@ -1382,58 +1380,122 @@ public function testIdentityFunctionDoesNotAcceptStateField(): void } #[Group('DDC-1389')] - public function testInheritanceTypeJoinInRootClass(): void + public function testInheritanceTypeJoinInRootClassWithDisabledForcePartialLoad(): void { $this->assertSqlGeneration( 'SELECT p FROM Doctrine\Tests\Models\Company\CompanyPerson p', 'SELECT c0_.id AS id_0, c0_.name AS name_1, c1_.title AS title_2, c2_.salary AS salary_3, c2_.department AS department_4, c2_.startDate AS startDate_5, c0_.discr AS discr_6, c0_.spouse_id AS spouse_id_7, c1_.car_id AS car_id_8 FROM company_persons c0_ LEFT JOIN company_managers c1_ ON c0_.id = c1_.id LEFT JOIN company_employees c2_ ON c0_.id = c2_.id', + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => false], ); } #[Group('DDC-1389')] - public function testInheritanceTypeJoinInChildClass(): void + public function testInheritanceTypeJoinInRootClassWithEnabledForcePartialLoad(): void + { + $this->assertSqlGeneration( + 'SELECT p FROM Doctrine\Tests\Models\Company\CompanyPerson p', + 'SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_', + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => true], + ); + } + + #[Group('DDC-1389')] + public function testInheritanceTypeJoinInChildClassWithDisabledForcePartialLoad(): void { $this->assertSqlGeneration( 'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e', 'SELECT c0_.id AS id_0, c0_.name AS name_1, c1_.salary AS salary_2, c1_.department AS department_3, c1_.startDate AS startDate_4, c2_.title AS title_5, c0_.discr AS discr_6, c0_.spouse_id AS spouse_id_7, c2_.car_id AS car_id_8 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id LEFT JOIN company_managers c2_ ON c1_.id = c2_.id', + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => false], + ); + } + + #[Group('DDC-1389')] + public function testInheritanceTypeJoinInChildClassWithEnabledForcePartialLoad(): void + { + $this->assertSqlGeneration( + 'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e', + 'SELECT c0_.id AS id_0, c0_.name AS name_1, c1_.salary AS salary_2, c1_.department AS department_3, c1_.startDate AS startDate_4, c0_.discr AS discr_5 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id', + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => true], ); } #[Group('DDC-1389')] - public function testInheritanceTypeJoinInLeafClass(): void + public function testInheritanceTypeJoinInLeafClassWithDisabledForcePartialLoad(): void { $this->assertSqlGeneration( 'SELECT m FROM Doctrine\Tests\Models\Company\CompanyManager m', 'SELECT c0_.id AS id_0, c0_.name AS name_1, c1_.salary AS salary_2, c1_.department AS department_3, c1_.startDate AS startDate_4, c2_.title AS title_5, c0_.discr AS discr_6, c0_.spouse_id AS spouse_id_7, c2_.car_id AS car_id_8 FROM company_managers c2_ INNER JOIN company_employees c1_ ON c2_.id = c1_.id INNER JOIN company_persons c0_ ON c2_.id = c0_.id', + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => false], + ); + } + + #[Group('DDC-1389')] + public function testInheritanceTypeJoinInLeafClassWithEnabledForcePartialLoad(): void + { + $this->assertSqlGeneration( + 'SELECT m FROM Doctrine\Tests\Models\Company\CompanyManager m', + 'SELECT c0_.id AS id_0, c0_.name AS name_1, c1_.salary AS salary_2, c1_.department AS department_3, c1_.startDate AS startDate_4, c2_.title AS title_5, c0_.discr AS discr_6 FROM company_managers c2_ INNER JOIN company_employees c1_ ON c2_.id = c1_.id INNER JOIN company_persons c0_ ON c2_.id = c0_.id', + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => true], ); } #[Group('DDC-1389')] - public function testInheritanceTypeSingleTableInRootClass(): void + public function testInheritanceTypeSingleTableInRootClassWithDisabledForcePartialLoad(): void { $this->assertSqlGeneration( 'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c', "SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.fixPrice AS fixPrice_2, c0_.hoursWorked AS hoursWorked_3, c0_.pricePerHour AS pricePerHour_4, c0_.maxPrice AS maxPrice_5, c0_.discr AS discr_6, c0_.salesPerson_id AS salesPerson_id_7 FROM company_contracts c0_ WHERE c0_.discr IN ('fix', 'flexible', 'flexultra')", + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => false], + ); + } + + #[Group('DDC-1389')] + public function testInheritanceTypeSingleTableInRootClassWithEnabledForcePartialLoad(): void + { + $this->assertSqlGeneration( + 'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c', + "SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.fixPrice AS fixPrice_2, c0_.hoursWorked AS hoursWorked_3, c0_.pricePerHour AS pricePerHour_4, c0_.maxPrice AS maxPrice_5, c0_.discr AS discr_6 FROM company_contracts c0_ WHERE c0_.discr IN ('fix', 'flexible', 'flexultra')", + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => true], ); } #[Group('DDC-1389')] public function testInheritanceTypeSingleTableInChildClassWithDisabledForcePartialLoad(): void { - $this->hydrationMode = ORMQuery::HYDRATE_ARRAY; + $this->assertSqlGeneration( + 'SELECT fc FROM Doctrine\Tests\Models\Company\CompanyFlexContract fc', + "SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.hoursWorked AS hoursWorked_2, c0_.pricePerHour AS pricePerHour_3, c0_.maxPrice AS maxPrice_4, c0_.discr AS discr_5, c0_.salesPerson_id AS salesPerson_id_6 FROM company_contracts c0_ WHERE c0_.discr IN ('flexible', 'flexultra')", + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => false], + ); + } + #[Group('DDC-1389')] + public function testInheritanceTypeSingleTableInChildClassWithEnabledForcePartialLoad(): void + { $this->assertSqlGeneration( 'SELECT fc FROM Doctrine\Tests\Models\Company\CompanyFlexContract fc', "SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.hoursWorked AS hoursWorked_2, c0_.pricePerHour AS pricePerHour_3, c0_.maxPrice AS maxPrice_4, c0_.discr AS discr_5 FROM company_contracts c0_ WHERE c0_.discr IN ('flexible', 'flexultra')", + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => true], ); } #[Group('DDC-1389')] - public function testInheritanceTypeSingleTableInLeafClass(): void + public function testInheritanceTypeSingleTableInLeafClassWithDisabledForcePartialLoad(): void { $this->assertSqlGeneration( 'SELECT fuc FROM Doctrine\Tests\Models\Company\CompanyFlexUltraContract fuc', "SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.hoursWorked AS hoursWorked_2, c0_.pricePerHour AS pricePerHour_3, c0_.maxPrice AS maxPrice_4, c0_.discr AS discr_5, c0_.salesPerson_id AS salesPerson_id_6 FROM company_contracts c0_ WHERE c0_.discr IN ('flexultra')", + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => false], + ); + } + + #[Group('DDC-1389')] + public function testInheritanceTypeSingleTableInLeafClassWithEnabledForcePartialLoad(): void + { + $this->assertSqlGeneration( + 'SELECT fuc FROM Doctrine\Tests\Models\Company\CompanyFlexUltraContract fuc', + "SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.hoursWorked AS hoursWorked_2, c0_.pricePerHour AS pricePerHour_3, c0_.maxPrice AS maxPrice_4, c0_.discr AS discr_5 FROM company_contracts c0_ WHERE c0_.discr IN ('flexultra')", + [ORMQuery::HINT_FORCE_PARTIAL_LOAD => true], ); } @@ -1712,8 +1774,6 @@ public function testCustomTypeValueSqlForAllFields(): void public function testCustomTypeValueSqlForPartialObject(): void { - $this->hydrationMode = ORMQuery::HYDRATE_ARRAY; - if (DBALType::hasType('negative_to_positive')) { DBALType::overrideType('negative_to_positive', NegativeToPositiveType::class); } else { @@ -1722,7 +1782,7 @@ public function testCustomTypeValueSqlForPartialObject(): void $this->assertSqlGeneration( 'SELECT partial p.{id, customInteger} FROM Doctrine\Tests\Models\CustomType\CustomTypeParent p', - 'SELECT c0_.id AS id_0, -(c0_.customInteger) AS customInteger_1 FROM customtype_parents c0_', + 'SELECT c0_.id AS id_0, -(c0_.customInteger) AS customInteger_1, c0_.child_id AS child_id_2 FROM customtype_parents c0_', ); } diff --git a/tests/Tests/ORM/Tools/Pagination/LimitSubqueryOutputWalkerTest.php b/tests/Tests/ORM/Tools/Pagination/LimitSubqueryOutputWalkerTest.php index 0f5bac25b7..0e14402b5a 100644 --- a/tests/Tests/ORM/Tools/Pagination/LimitSubqueryOutputWalkerTest.php +++ b/tests/Tests/ORM/Tools/Pagination/LimitSubqueryOutputWalkerTest.php @@ -137,7 +137,7 @@ public function testCountQueryWithComplexScalarOrderByItemWithoutJoin(): void ); } - public function testCountQueryWithComplexScalarOrderByItemJoined(): void + public function testCountQueryWithComplexScalarOrderByItemJoinedWithoutPartial(): void { $this->entityManager = $this->createTestEntityManagerWithPlatform(new MySQLPlatform()); diff --git a/tests/Tests/ORM/UnitOfWorkTest.php b/tests/Tests/ORM/UnitOfWorkTest.php index 8de0eb03e2..dba967fe87 100644 --- a/tests/Tests/ORM/UnitOfWorkTest.php +++ b/tests/Tests/ORM/UnitOfWorkTest.php @@ -651,15 +651,6 @@ public function testItThrowsWhenApplicationProvidedIdsCollide(): void $this->_unitOfWork->persist($phone2); } - - public function testItThrowsWhenCreateEntityWithSqlWalkerPartialQueryHint(): void - { - $this->expectException(HydrationException::class); - $this->expectExceptionMessage('Hydration of entity objects is not allowed when DQL PARTIAL keyword is used.'); - - $hints = [SqlWalker::HINT_PARTIAL => true]; - $this->_unitOfWork->createEntity(VersionedAssignedIdentifierEntity::class, ['id' => 1], $hints); - } } From 7ef1f0a379dc122a252fe90aba5d556199025831 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 9 Oct 2024 22:14:01 +0200 Subject: [PATCH 03/10] Phpcs fixes --- src/Decorator/EntityManagerDecorator.php | 1 - src/EntityManager.php | 1 - src/Query/Parser.php | 2 -- src/Query/SqlWalker.php | 8 ++++---- src/UnitOfWork.php | 2 -- tests/Tests/ORM/Query/ParserTest.php | 1 - tests/Tests/ORM/UnitOfWorkTest.php | 2 -- 7 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/Decorator/EntityManagerDecorator.php b/src/Decorator/EntityManagerDecorator.php index 212e01e511..6f1b041968 100644 --- a/src/Decorator/EntityManagerDecorator.php +++ b/src/Decorator/EntityManagerDecorator.php @@ -8,7 +8,6 @@ use Doctrine\Common\EventManager; use Doctrine\DBAL\Connection; use Doctrine\DBAL\LockMode; -use Doctrine\Deprecations\Deprecation; use Doctrine\ORM\Cache; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManagerInterface; diff --git a/src/EntityManager.php b/src/EntityManager.php index 9de636e5b7..8045ac2f5e 100644 --- a/src/EntityManager.php +++ b/src/EntityManager.php @@ -9,7 +9,6 @@ use Doctrine\Common\EventManager; use Doctrine\DBAL\Connection; use Doctrine\DBAL\LockMode; -use Doctrine\Deprecations\Deprecation; use Doctrine\ORM\Exception\EntityManagerClosed; use Doctrine\ORM\Exception\InvalidHydrationMode; use Doctrine\ORM\Exception\MissingIdentifierField; diff --git a/src/Query/Parser.php b/src/Query/Parser.php index 12fe0c9769..76e6757052 100644 --- a/src/Query/Parser.php +++ b/src/Query/Parser.php @@ -5,9 +5,7 @@ namespace Doctrine\ORM\Query; use Doctrine\Common\Lexer\Token; -use Doctrine\Deprecations\Deprecation; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Internal\Hydration\HydrationException; use Doctrine\ORM\Mapping\AssociationMapping; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; diff --git a/src/Query/SqlWalker.php b/src/Query/SqlWalker.php index 9beeff337b..155461ab2d 100644 --- a/src/Query/SqlWalker.php +++ b/src/Query/SqlWalker.php @@ -1406,21 +1406,21 @@ public function walkSelectExpression(AST\SelectExpression $selectExpression): st // since it requires outer joining subtables. if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { foreach ($class->subClasses as $subClassName) { - $subClass = $this->em->getClassMetadata($subClassName); + $subClass = $this->em->getClassMetadata($subClassName); $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); foreach ($subClass->fieldMappings as $fieldName => $mapping) { - if (isset($mapping->inherited) || ($partialFieldSet && !in_array($fieldName, $partialFieldSet, true))) { + if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) { continue; } - $columnAlias = $this->getSQLColumnAlias($mapping->columnName); + $columnAlias = $this->getSQLColumnAlias($mapping->columnName); $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform); $col = $sqlTableAlias . '.' . $quotedColumnName; $type = Type::getType($mapping->type); - $col = $type->convertToPHPValueSQL($col, $this->platform); + $col = $type->convertToPHPValueSQL($col, $this->platform); $sqlParts[] = $col . ' AS ' . $columnAlias; diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 811aa895b4..ef31a6bbcd 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -29,7 +29,6 @@ use Doctrine\ORM\Exception\ORMException; use Doctrine\ORM\Exception\UnexpectedAssociationValue; use Doctrine\ORM\Id\AssignedGenerator; -use Doctrine\ORM\Internal\Hydration\HydrationException; use Doctrine\ORM\Internal\HydrationCompleteHandler; use Doctrine\ORM\Internal\StronglyConnectedComponents; use Doctrine\ORM\Internal\TopologicalSort; @@ -45,7 +44,6 @@ use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister; use Doctrine\ORM\Persisters\Entity\SingleTablePersister; use Doctrine\ORM\Proxy\InternalProxy; -use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Utility\IdentifierFlattener; use Doctrine\Persistence\PropertyChangedListener; use Exception; diff --git a/tests/Tests/ORM/Query/ParserTest.php b/tests/Tests/ORM/Query/ParserTest.php index ed75be517b..430177b1fc 100644 --- a/tests/Tests/ORM/Query/ParserTest.php +++ b/tests/Tests/ORM/Query/ParserTest.php @@ -4,7 +4,6 @@ namespace Doctrine\Tests\ORM\Query; -use Doctrine\ORM\Internal\Hydration\HydrationException; use Doctrine\ORM\Query; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\QueryException; diff --git a/tests/Tests/ORM/UnitOfWorkTest.php b/tests/Tests/ORM/UnitOfWorkTest.php index dba967fe87..a9112fa5d5 100644 --- a/tests/Tests/ORM/UnitOfWorkTest.php +++ b/tests/Tests/ORM/UnitOfWorkTest.php @@ -13,7 +13,6 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\ORM\EntityNotFoundException; use Doctrine\ORM\Exception\EntityIdentityCollisionException; -use Doctrine\ORM\Internal\Hydration\HydrationException; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; @@ -23,7 +22,6 @@ use Doctrine\ORM\Mapping\Version; use Doctrine\ORM\OptimisticLockException; use Doctrine\ORM\ORMInvalidArgumentException; -use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\UnitOfWork; use Doctrine\Tests\Mocks\EntityManagerMock; use Doctrine\Tests\Mocks\EntityPersisterMock; From 5f4ecfd1d8a03522b3742d2c12f4403e55320a22 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 10 Oct 2024 10:38:33 +0200 Subject: [PATCH 04/10] Fix imports --- ...ixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php | 2 +- .../SimpleQueryPartialObjectHydrationPerformanceBench.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php b/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php index 6a1638afcf..822a019c7d 100644 --- a/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php +++ b/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php @@ -8,8 +8,8 @@ use Doctrine\ORM\Internal\Hydration\ObjectHydrator; use Doctrine\ORM\Query; use Doctrine\ORM\Query\ResultSetMapping; -use Doctrine\Performance\ArrayResultFactory; use Doctrine\Performance\EntityManagerFactory; +use Doctrine\Tests\Mocks\ArrayResultFactory; use Doctrine\Tests\Models\CMS\CmsPhonenumber; use Doctrine\Tests\Models\CMS\CmsUser; use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods; diff --git a/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php b/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php index b894068eb2..72c2ec73d9 100644 --- a/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php +++ b/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php @@ -8,8 +8,8 @@ use Doctrine\ORM\Internal\Hydration\ObjectHydrator; use Doctrine\ORM\Query; use Doctrine\ORM\Query\ResultSetMapping; -use Doctrine\Performance\ArrayResultFactory; use Doctrine\Performance\EntityManagerFactory; +use Doctrine\Tests\Mocks\ArrayResultFactory; use Doctrine\Tests\Models\CMS\CmsUser; use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods; From da7854f586507872e6db421d7273e7666ecd71bf Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 10 Oct 2024 10:41:04 +0200 Subject: [PATCH 05/10] Fix imports --- ...ixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php | 2 +- .../SimpleQueryPartialObjectHydrationPerformanceBench.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php b/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php index 822a019c7d..ef1d1b4bdc 100644 --- a/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php +++ b/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php @@ -63,7 +63,7 @@ public function init(): void ]; } - $this->result = ArrayResultFactory::createFromArray($resultSet); + $this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet); $this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([])); $this->rsm = new ResultSetMapping(); diff --git a/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php b/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php index 72c2ec73d9..a5c7867072 100644 --- a/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php +++ b/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php @@ -54,7 +54,7 @@ public function init(): void ]; } - $this->result = ArrayResultFactory::createFromArray($resultSet); + $this->result = ArrayResultFactory::createWrapperResultFromArray($resultSet); $this->hydrator = new ObjectHydrator(EntityManagerFactory::getEntityManager([])); $this->rsm = new ResultSetMapping(); From f5fb400d0f13fc58431ac06185e53edac4bea704 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Oct 2024 02:32:15 +0200 Subject: [PATCH 06/10] Address review comments. --- docs/en/reference/partial-hydration.rst | 5 ----- docs/en/reference/partial-objects.rst | 10 ---------- src/UnitOfWork.php | 12 ------------ 3 files changed, 27 deletions(-) diff --git a/docs/en/reference/partial-hydration.rst b/docs/en/reference/partial-hydration.rst index 16879c45c5..fda14b98ef 100644 --- a/docs/en/reference/partial-hydration.rst +++ b/docs/en/reference/partial-hydration.rst @@ -1,11 +1,6 @@ Partial Hydration ================= -.. note:: - - Creating Partial Objects through DQL was possible in ORM 2, - but is only supported for array hydration as of ORM 3. - Partial hydration of entities is allowed in the array hydrator, when only a subset of the fields of an entity are loaded from the database and the nested results are still created based on the entity relationship structure. diff --git a/docs/en/reference/partial-objects.rst b/docs/en/reference/partial-objects.rst index 51f173adf6..3835123257 100644 --- a/docs/en/reference/partial-objects.rst +++ b/docs/en/reference/partial-objects.rst @@ -1,14 +1,6 @@ Partial Objects =============== - -.. note:: - - Creating Partial Objects through DQL is deprecated and - will be removed in the future, use data transfer object - support in DQL instead. (\ `Details - `_) - A partial object is an object whose state is not fully initialized after being reconstituted from the database and that is disconnected from the rest of its data. The following section will @@ -94,5 +86,3 @@ When should I force partial objects? Mainly for optimization purposes, but be careful of premature optimization as partial objects lead to potentially more fragile code. - - diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index ef31a6bbcd..751d36ee21 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -2412,18 +2412,6 @@ public function createEntity(string $className, array $data, array &$hints = []) unset($this->eagerLoadingEntities[$class->rootEntityName]); } - // Properly initialize any unfetched associations, if partial objects are not allowed. - if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { - Deprecation::trigger( - 'doctrine/orm', - 'https://github.com/doctrine/orm/issues/8471', - 'Partial Objects are deprecated (here entity %s)', - $className, - ); - - return $entity; - } - foreach ($class->associationMappings as $field => $assoc) { // Check if the association is not among the fetch-joined associations already. if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) { From 516b5931934a07d3071bb2cd4b833f1d0729a22c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Oct 2024 15:41:31 +0200 Subject: [PATCH 07/10] Fix phpcs --- src/UnitOfWork.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index df2996ce3b..4cf3e9e2d9 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -12,7 +12,6 @@ use Doctrine\DBAL; use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; use Doctrine\DBAL\LockMode; -use Doctrine\Deprecations\Deprecation; use Doctrine\ORM\Cache\Persister\CachedPersister; use Doctrine\ORM\Event\ListenersInvoker; use Doctrine\ORM\Event\OnClearEventArgs; From c7e5605d116cca2229db3ee012eb78a420cd174c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Oct 2024 15:43:46 +0200 Subject: [PATCH 08/10] Fix test --- src/Query/Parser.php | 1 - tests/Tests/ORM/UnitOfWorkTest.php | 9 --------- 2 files changed, 10 deletions(-) diff --git a/src/Query/Parser.php b/src/Query/Parser.php index 3b1779e430..bcfad85c42 100644 --- a/src/Query/Parser.php +++ b/src/Query/Parser.php @@ -9,7 +9,6 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Exception\DuplicateFieldException; use Doctrine\ORM\Exception\NoMatchingPropertyException; -use Doctrine\ORM\Internal\Hydration\HydrationException; use Doctrine\ORM\Mapping\AssociationMapping; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; diff --git a/tests/Tests/ORM/UnitOfWorkTest.php b/tests/Tests/ORM/UnitOfWorkTest.php index 900ee21830..2a421c6687 100644 --- a/tests/Tests/ORM/UnitOfWorkTest.php +++ b/tests/Tests/ORM/UnitOfWorkTest.php @@ -691,15 +691,6 @@ public function rollBack(): void self::assertSame('Commit failed', $e->getPrevious()->getMessage()); } } - - public function testItThrowsWhenCreateEntityWithSqlWalkerPartialQueryHint(): void - { - $this->expectException(HydrationException::class); - $this->expectExceptionMessage('Hydration of entity objects is not allowed when DQL PARTIAL keyword is used.'); - - $hints = [SqlWalker::HINT_PARTIAL => true]; - $this->_unitOfWork->createEntity(VersionedAssignedIdentifierEntity::class, ['id' => 1], $hints); - } } From 7c9b74221fd2c0e3245fb43a607a0ec113dd037e Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Oct 2024 16:31:02 +0200 Subject: [PATCH 09/10] fix phpbench tests. --- ...ueryFetchJoinPartialObjectHydrationPerformanceBench.php | 7 +++++++ .../SimpleQueryPartialObjectHydrationPerformanceBench.php | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php b/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php index ef1d1b4bdc..0694e8fff6 100644 --- a/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php +++ b/tests/Performance/Hydration/MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php @@ -10,6 +10,7 @@ use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\Performance\EntityManagerFactory; use Doctrine\Tests\Mocks\ArrayResultFactory; +use Doctrine\Tests\Models\CMS\CmsAddress; use Doctrine\Tests\Models\CMS\CmsPhonenumber; use Doctrine\Tests\Models\CMS\CmsUser; use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods; @@ -33,6 +34,7 @@ public function init(): void 'u__name' => 'Roman', 'sclr0' => 'ROMANB', 'p__phonenumber' => '42', + 'a__id' => '1', ], [ 'u__id' => '1', @@ -41,6 +43,7 @@ public function init(): void 'u__name' => 'Roman', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43', + 'a__id' => '1', ], [ 'u__id' => '2', @@ -49,6 +52,7 @@ public function init(): void 'u__name' => 'Roman', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91', + 'a__id' => '1', ], ]; @@ -60,6 +64,7 @@ public function init(): void 'u__name' => 'Jonathan', 'sclr0' => 'JWAGE' . $i, 'p__phonenumber' => '91', + 'a__id' => '1', ]; } @@ -75,6 +80,8 @@ public function init(): void $this->rsm->addFieldResult('u', 'u__name', 'name'); $this->rsm->addScalarResult('sclr0', 'nameUpper'); $this->rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber'); + $this->rsm->addJoinedEntityResult(CmsAddress::class, 'a', 'u', 'address'); + $this->rsm->addFieldResult('a', 'a__id', 'id'); } public function benchHydration(): void diff --git a/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php b/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php index a5c7867072..14ed606508 100644 --- a/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php +++ b/tests/Performance/Hydration/SimpleQueryPartialObjectHydrationPerformanceBench.php @@ -10,6 +10,7 @@ use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\Performance\EntityManagerFactory; use Doctrine\Tests\Mocks\ArrayResultFactory; +use Doctrine\Tests\Models\CMS\CmsAddress; use Doctrine\Tests\Models\CMS\CmsUser; use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods; @@ -30,18 +31,21 @@ public function init(): void 'u__status' => 'developer', 'u__username' => 'romanb', 'u__name' => 'Roman', + 'a__id' => '1', ], [ 'u__id' => '1', 'u__status' => 'developer', 'u__username' => 'romanb', 'u__name' => 'Roman', + 'a__id' => '1', ], [ 'u__id' => '2', 'u__status' => 'developer', 'u__username' => 'romanb', 'u__name' => 'Roman', + 'a__id' => '1', ], ]; @@ -51,6 +55,7 @@ public function init(): void 'u__status' => 'developer', 'u__username' => 'jwage', 'u__name' => 'Jonathan', + 'a__id' => '1', ]; } @@ -63,6 +68,8 @@ public function init(): void $this->rsm->addFieldResult('u', 'u__status', 'status'); $this->rsm->addFieldResult('u', 'u__username', 'username'); $this->rsm->addFieldResult('u', 'u__name', 'name'); + $this->rsm->addJoinedEntityResult(CmsAddress::class, 'a', 'u', 'address'); + $this->rsm->addFieldResult('a', 'a__id', 'id'); } public function benchHydration(): void From cf8f5f9f935455976f637051dacdcc7f5b5d42e1 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Oct 2024 21:01:04 +0200 Subject: [PATCH 10/10] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Grégoire Paris --- docs/en/reference/dql-doctrine-query-language.rst | 2 +- docs/en/reference/partial-objects.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/reference/dql-doctrine-query-language.rst b/docs/en/reference/dql-doctrine-query-language.rst index 889013a3ba..e668c08fd8 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -542,7 +542,7 @@ DQL keyword: $query = $em->createQuery('SELECT partial u.{id, username} FROM CmsUser u'); $users = $query->getResult(); // array of partially loaded CmsUser objects -You use the partial syntax when joining as well: +You can use the partial syntax when joining as well: .. code-block:: php diff --git a/docs/en/reference/partial-objects.rst b/docs/en/reference/partial-objects.rst index 3835123257..3123c083f3 100644 --- a/docs/en/reference/partial-objects.rst +++ b/docs/en/reference/partial-objects.rst @@ -5,7 +5,7 @@ A partial object is an object whose state is not fully initialized after being reconstituted from the database and that is disconnected from the rest of its data. The following section will describe why partial objects are problematic and what the approach -of Doctrine2 to this problem is. +of Doctrine to this problem is. .. note::