Skip to content

Commit 6995815

Browse files
authored
Merge pull request #11653 from beberlei/GH-8471-RevertPartialObjects2
[GH-8471] Undeprecate PARTIAL for objects in DQL
2 parents 60c2454 + cf8f5f9 commit 6995815

26 files changed

+626
-72
lines changed

docs/en/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Advanced Topics
7474
* :doc:`Improving Performance <reference/improving-performance>`
7575
* :doc:`Caching <reference/caching>`
7676
* :doc:`Partial Hydration <reference/partial-hydration>`
77+
* :doc:`Partial Objects <reference/partial-objects>`
7778
* :doc:`Change Tracking Policies <reference/change-tracking-policies>`
7879
* :doc:`Best Practices <reference/best-practices>`
7980
* :doc:`Metadata Drivers <reference/metadata-drivers>`

docs/en/reference/dql-doctrine-query-language.rst

+21-3
Original file line numberDiff line numberDiff line change
@@ -533,14 +533,23 @@ back. Instead, you receive only arrays as a flat rectangular result
533533
set, similar to how you would if you were just using SQL directly
534534
and joining some data.
535535

536-
If you want to select a partial number of fields for hydration entity in
537-
the context of array hydration and joins you can use the ``partial`` DQL keyword:
536+
If you want to select partial objects or fields in array hydration you can use the ``partial``
537+
DQL keyword:
538+
539+
.. code-block:: php
540+
541+
<?php
542+
$query = $em->createQuery('SELECT partial u.{id, username} FROM CmsUser u');
543+
$users = $query->getResult(); // array of partially loaded CmsUser objects
544+
545+
You can use the partial syntax when joining as well:
538546

539547
.. code-block:: php
540548
541549
<?php
542550
$query = $em->createQuery('SELECT partial u.{id, username}, partial a.{id, name} FROM CmsUser u JOIN u.articles a');
543-
$users = $query->getArrayResult(); // array of partially loaded CmsUser and CmsArticle fields
551+
$usersArray = $query->getArrayResult(); // array of partially loaded CmsUser and CmsArticle fields
552+
$users = $query->getResult(); // array of partially loaded CmsUser objects
544553
545554
"NEW" Operator Syntax
546555
^^^^^^^^^^^^^^^^^^^^^
@@ -1427,6 +1436,15 @@ exist mostly internal query hints that are not be consumed in
14271436
userland. However the following few hints are to be used in
14281437
userland:
14291438

1439+
1440+
- ``Query::HINT_FORCE_PARTIAL_LOAD`` - Allows to hydrate objects
1441+
although not all their columns are fetched. This query hint can be
1442+
used to handle memory consumption problems with large result-sets
1443+
that contain char or binary data. Doctrine has no way of implicitly
1444+
reloading this data. Partially loaded objects have to be passed to
1445+
``EntityManager::refresh()`` if they are to be reloaded fully from
1446+
the database. This query hint is deprecated and will be removed
1447+
in the future (\ `Details <https://github.com/doctrine/orm/issues/8471>`_)
14301448
- ``Query::HINT_REFRESH`` - This query is used internally by
14311449
``EntityManager::refresh()`` and can be used in userland as well.
14321450
If you specify this hint and a query returns the data for an entity

docs/en/reference/partial-hydration.rst

-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
Partial Hydration
22
=================
33

4-
.. note::
5-
6-
Creating Partial Objects through DQL was possible in ORM 2,
7-
but is only supported for array hydration as of ORM 3.
8-
94
Partial hydration of entities is allowed in the array hydrator, when
105
only a subset of the fields of an entity are loaded from the database
116
and the nested results are still created based on the entity relationship structure.

docs/en/reference/partial-objects.rst

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
Partial Objects
2+
===============
3+
4+
A partial object is an object whose state is not fully initialized
5+
after being reconstituted from the database and that is
6+
disconnected from the rest of its data. The following section will
7+
describe why partial objects are problematic and what the approach
8+
of Doctrine to this problem is.
9+
10+
.. note::
11+
12+
The partial object problem in general does not apply to
13+
methods or queries where you do not retrieve the query result as
14+
objects. Examples are: ``Query#getArrayResult()``,
15+
``Query#getScalarResult()``, ``Query#getSingleScalarResult()``,
16+
etc.
17+
18+
.. warning::
19+
20+
Use of partial objects is tricky. Fields that are not retrieved
21+
from the database will not be updated by the UnitOfWork even if they
22+
get changed in your objects. You can only promote a partial object
23+
to a fully-loaded object by calling ``EntityManager#refresh()``
24+
or a DQL query with the refresh flag.
25+
26+
27+
What is the problem?
28+
--------------------
29+
30+
In short, partial objects are problematic because they are usually
31+
objects with broken invariants. As such, code that uses these
32+
partial objects tends to be very fragile and either needs to "know"
33+
which fields or methods can be safely accessed or add checks around
34+
every field access or method invocation. The same holds true for
35+
the internals, i.e. the method implementations, of such objects.
36+
You usually simply assume the state you need in the method is
37+
available, after all you properly constructed this object before
38+
you pushed it into the database, right? These blind assumptions can
39+
quickly lead to null reference errors when working with such
40+
partial objects.
41+
42+
It gets worse with the scenario of an optional association (0..1 to
43+
1). When the associated field is NULL, you don't know whether this
44+
object does not have an associated object or whether it was simply
45+
not loaded when the owning object was loaded from the database.
46+
47+
These are reasons why many ORMs do not allow partial objects at all
48+
and instead you always have to load an object with all its fields
49+
(associations being proxied). One secure way to allow partial
50+
objects is if the programming language/platform allows the ORM tool
51+
to hook deeply into the object and instrument it in such a way that
52+
individual fields (not only associations) can be loaded lazily on
53+
first access. This is possible in Java, for example, through
54+
bytecode instrumentation. In PHP though this is not possible, so
55+
there is no way to have "secure" partial objects in an ORM with
56+
transparent persistence.
57+
58+
Doctrine, by default, does not allow partial objects. That means,
59+
any query that only selects partial object data and wants to
60+
retrieve the result as objects (i.e. ``Query#getResult()``) will
61+
raise an exception telling you that partial objects are dangerous.
62+
If you want to force a query to return you partial objects,
63+
possibly as a performance tweak, you can use the ``partial``
64+
keyword as follows:
65+
66+
.. code-block:: php
67+
68+
<?php
69+
$q = $em->createQuery("select partial u.{id,name} from MyApp\Domain\User u");
70+
71+
You can also get a partial reference instead of a proxy reference by
72+
calling:
73+
74+
.. code-block:: php
75+
76+
<?php
77+
$reference = $em->getPartialReference('MyApp\Domain\User', 1);
78+
79+
Partial references are objects with only the identifiers set as they
80+
are passed to the second argument of the ``getPartialReference()`` method.
81+
All other fields are null.
82+
83+
When should I force partial objects?
84+
------------------------------------
85+
86+
Mainly for optimization purposes, but be careful of premature
87+
optimization as partial objects lead to potentially more fragile
88+
code.

docs/en/sidebar.rst

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
reference/native-sql
3939
reference/change-tracking-policies
4040
reference/partial-hydration
41+
reference/partial-objects
4142
reference/attributes-reference
4243
reference/xml-mapping
4344
reference/php-mapping

src/Cache/DefaultQueryCache.php

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Doctrine\ORM\PersistentCollection;
1717
use Doctrine\ORM\Query;
1818
use Doctrine\ORM\Query\ResultSetMapping;
19+
use Doctrine\ORM\Query\SqlWalker;
1920
use Doctrine\ORM\UnitOfWork;
2021

2122
use function array_map;
@@ -210,6 +211,10 @@ public function put(QueryCacheKey $key, ResultSetMapping $rsm, mixed $result, ar
210211
throw FeatureNotImplemented::nonSelectStatements();
211212
}
212213

214+
if (($hints[SqlWalker::HINT_PARTIAL] ?? false) === true || ($hints[Query::HINT_FORCE_PARTIAL_LOAD] ?? false) === true) {
215+
throw FeatureNotImplemented::partialEntities();
216+
}
217+
213218
if (! ($key->cacheMode & Cache::MODE_PUT)) {
214219
return false;
215220
}

src/Cache/Exception/FeatureNotImplemented.php

+5
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ public static function nonSelectStatements(): self
2020
{
2121
return new self('Second-level cache query supports only select statements.');
2222
}
23+
24+
public static function partialEntities(): self
25+
{
26+
return new self('Second level cache does not support partial entities.');
27+
}
2328
}

src/Query.php

+8
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ class Query extends AbstractQuery
7474
*/
7575
public const HINT_REFRESH_ENTITY = 'doctrine.refresh.entity';
7676

77+
/**
78+
* The forcePartialLoad query hint forces a particular query to return
79+
* partial objects.
80+
*
81+
* @todo Rename: HINT_OPTIMIZE
82+
*/
83+
public const HINT_FORCE_PARTIAL_LOAD = 'doctrine.forcePartialLoad';
84+
7785
/**
7886
* The includeMetaColumns query hint causes meta columns like foreign keys and
7987
* discriminator columns to be selected and returned as part of the query result.

src/Query/Parser.php

-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use Doctrine\ORM\EntityManagerInterface;
1010
use Doctrine\ORM\Exception\DuplicateFieldException;
1111
use Doctrine\ORM\Exception\NoMatchingPropertyException;
12-
use Doctrine\ORM\Internal\Hydration\HydrationException;
1312
use Doctrine\ORM\Mapping\AssociationMapping;
1413
use Doctrine\ORM\Mapping\ClassMetadata;
1514
use Doctrine\ORM\Query;
@@ -1702,10 +1701,6 @@ public function JoinAssociationDeclaration(): AST\JoinAssociationDeclaration
17021701
*/
17031702
public function PartialObjectExpression(): AST\PartialObjectExpression
17041703
{
1705-
if ($this->query->getHydrationMode() === Query::HYDRATE_OBJECT) {
1706-
throw HydrationException::partialObjectHydrationDisallowed();
1707-
}
1708-
17091704
$this->match(TokenType::T_PARTIAL);
17101705

17111706
$partialFieldSet = [];

src/Query/QueryException.php

+9
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ public static function iterateWithFetchJoinCollectionNotAllowed(AssociationMappi
8888
);
8989
}
9090

91+
public static function partialObjectsAreDangerous(): self
92+
{
93+
return new self(
94+
'Loading partial objects is dangerous. Fetch full objects or consider ' .
95+
'using a different fetch mode. If you really want partial objects, ' .
96+
'set the doctrine.forcePartialLoad query hint to TRUE.',
97+
);
98+
}
99+
91100
/**
92101
* @param string[] $assoc
93102
* @psalm-param array<string, string> $assoc

src/Query/SqlWalker.php

+24-16
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,11 @@ private function generateClassTableInheritanceJoins(
335335
$sql .= implode(' AND ', array_filter($sqlParts));
336336
}
337337

338+
// Ignore subclassing inclusion if partial objects is disallowed
339+
if ($this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
340+
return $sql;
341+
}
342+
338343
// LEFT JOIN child class tables
339344
foreach ($class->subClasses as $subClassName) {
340345
$subClass = $this->em->getClassMetadata($subClassName);
@@ -669,7 +674,8 @@ public function walkSelectClause(AST\SelectClause $selectClause): string
669674
$this->query->setHint(self::HINT_DISTINCT, true);
670675
}
671676

672-
$addMetaColumns = $this->query->getHydrationMode() === Query::HYDRATE_OBJECT
677+
$addMetaColumns = ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD) &&
678+
$this->query->getHydrationMode() === Query::HYDRATE_OBJECT
673679
|| $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS);
674680

675681
foreach ($this->selectedClasses as $selectedClass) {
@@ -1408,28 +1414,30 @@ public function walkSelectExpression(AST\SelectExpression $selectExpression): st
14081414
// 1) on Single Table Inheritance: always, since its marginal overhead
14091415
// 2) on Class Table Inheritance only if partial objects are disallowed,
14101416
// since it requires outer joining subtables.
1411-
foreach ($class->subClasses as $subClassName) {
1412-
$subClass = $this->em->getClassMetadata($subClassName);
1413-
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
1417+
if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
1418+
foreach ($class->subClasses as $subClassName) {
1419+
$subClass = $this->em->getClassMetadata($subClassName);
1420+
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
14141421

1415-
foreach ($subClass->fieldMappings as $fieldName => $mapping) {
1416-
if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) {
1417-
continue;
1418-
}
1422+
foreach ($subClass->fieldMappings as $fieldName => $mapping) {
1423+
if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) {
1424+
continue;
1425+
}
14191426

1420-
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
1421-
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
1427+
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
1428+
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
14221429

1423-
$col = $sqlTableAlias . '.' . $quotedColumnName;
1430+
$col = $sqlTableAlias . '.' . $quotedColumnName;
14241431

1425-
$type = Type::getType($mapping->type);
1426-
$col = $type->convertToPHPValueSQL($col, $this->platform);
1432+
$type = Type::getType($mapping->type);
1433+
$col = $type->convertToPHPValueSQL($col, $this->platform);
14271434

1428-
$sqlParts[] = $col . ' AS ' . $columnAlias;
1435+
$sqlParts[] = $col . ' AS ' . $columnAlias;
14291436

1430-
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
1437+
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
14311438

1432-
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
1439+
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
1440+
}
14331441
}
14341442
}
14351443

src/UnitOfWork.php

-6
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
use Doctrine\ORM\Exception\ORMException;
2929
use Doctrine\ORM\Exception\UnexpectedAssociationValue;
3030
use Doctrine\ORM\Id\AssignedGenerator;
31-
use Doctrine\ORM\Internal\Hydration\HydrationException;
3231
use Doctrine\ORM\Internal\HydrationCompleteHandler;
3332
use Doctrine\ORM\Internal\StronglyConnectedComponents;
3433
use Doctrine\ORM\Internal\TopologicalSort;
@@ -44,7 +43,6 @@
4443
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
4544
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
4645
use Doctrine\ORM\Proxy\InternalProxy;
47-
use Doctrine\ORM\Query\SqlWalker;
4846
use Doctrine\ORM\Utility\IdentifierFlattener;
4947
use Doctrine\Persistence\PropertyChangedListener;
5048
use Exception;
@@ -2356,10 +2354,6 @@ public function isCollectionScheduledForDeletion(PersistentCollection $coll): bo
23562354
*/
23572355
public function createEntity(string $className, array $data, array &$hints = []): object
23582356
{
2359-
if (isset($hints[SqlWalker::HINT_PARTIAL])) {
2360-
throw HydrationException::partialObjectHydrationDisallowed();
2361-
}
2362-
23632357
$class = $this->em->getClassMetadata($className);
23642358

23652359
$id = $this->identifierFlattener->flattenIdentifier($class, $data);

0 commit comments

Comments
 (0)