@@ -27,215 +27,166 @@ with a huge collection. [Here are some examples to index LIKE
2727filters] ( http://use-the-index-luke.com/sql/where-clause/searching-for-ranges/like-performance-tuning ) depending on your
2828database driver.
2929
30- ### Unserialized Properties Hydratation
30+ ### Eager loading
3131
32- Even though we're selecting only partial results (serialized properties) with Doctrine, it'll try to hydrate some
33- relations with lazy joins (for example ` OneToOne ` relations). It's recommended to take a look at the Symfony Profiler,
34- check what the generated SQL queries are doing in background and see if those may impact performance.
32+ By default Doctrine comes with [ lazy loading] ( http://doctrine-orm.readthedocs.io/en/latest/reference/working-with-objects.html#by-lazy-loading ) .
33+ Usually a killer time-saving feature and also a performance killer with large applications.
3534
36- To force Doctrine to only hydrate partial values you need to use the
37- [ ` Query::HINT_FORCE_PARTIAL_LOAD ` ] ( http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#query-hints ) .
38- Be careful, using this query hint will force the use of partial selects. Some properties might not be available even if
39- you expect them. If you want to be sure that Doctrine fetches them, use eager joins and make sure that properties are
40- serializable.
35+ Fortunately, Doctrine proposes another approach to remedy this problem: [ eager loading] ( http://doctrine-orm.readthedocs.io/en/latest/reference/working-with-objects.html#by-eager-loading ) .
36+ This can easily be enabled for a relation: ` @ORM\ManyToOne(fetch="EAGER") ` .
4137
42- To do this in API Platform you'd have to build a
43- [ ` QueryResultCollectionExtension ` ] ( https://github.com/api-platform/core/blob/master/src/Bridge/Doctrine/Orm/Extension/QueryResultCollectionExtensionInterface.php )
44- or a
45- [ ` QueryResultItemExtension ` ] ( https://github.com/api-platform/core/blob/master/src/Bridge/Doctrine/Orm/Extension/QueryResultItemExtensionInterface.php ) .
38+ By default in API Platform, we made the choice to force eager loading for all relations, with or without the Doctrine
39+ ` fetch ` attribute. Thanks to the eager loading [ extension] ( extensions.md ) .
4640
47- For example, let's decorate the existing
48- [ ` PaginationExtension ` ] ( https://github.com/api-platform/core/blob/master/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php )
49- by setting the query hint:
41+ #### Max joins
5042
51- ``` php
52- <?php
53- // src/AppBundle/Doctrine/Orm/Extension/QueryHintPaginationExtension.php
43+ There is a default restriction with this feature. We allow up to 30 joins per query. Beyond, an
44+ ` ApiPlatform\Core\Exception\RuntimeException ` exception will be thrown but this value can easily be increased with a
45+ little of configuration:
5446
55- namespace AppBundle\Doctrine\Orm\Extension;
47+ ``` yaml
48+ # app/config/config.yaml
5649
57- use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface;
58- use ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator;
59- use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker;
60- use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
61- use Doctrine\Common\Persistence\ManagerRegistry;
62- use Doctrine\ORM\QueryBuilder;
63- use Doctrine\ORM\Tools\Pagination\Paginator as DoctrineOrmPaginator;
64- use Doctrine\ORM\Query;
50+ api_platform :
51+ # ...
6552
66- final class QueryHintPaginationExtension implements QueryResultCollectionExtensionInterface
67- {
68- private $managerRegistry;
69- private $decorated;
53+ eager_loading :
54+ max_joins : 100
7055
71- public function __construct(ManagerRegistry $managerRegistry, QueryResultCollectionExtensionInterface $decorated) {
72- $this->managerRegistry = $managerRegistry;
73- $this->decorated = $decorated;
74- }
56+ # ...
57+ ```
7558
76- /**
77- * {@inheritdoc}
78- */
79- public function supportsResult(string $resourceClass, string $operationName = null) : bool
80- {
81- return $this->decorated->supportsResult($resourceClass, $operationName);
82- }
59+ Be careful when you exceed this limit, it's often caused by the result of a circular reference. [ Serializer groups] ( serialization-groups-and-relations.md )
60+ can be a good solution to fix this issue.
8361
84- /**
85- * {@inheritdoc}
86- */
87- public function getResult(QueryBuilder $queryBuilder)
88- {
89- $query = $queryBuilder->getQuery();
90- // This forces doctrine to not lazy load entities
91- $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true);
62+ #### Force eager
9263
93- $doctrineOrmPaginator = new DoctrineOrmPaginator($query, $this->useFetchJoinCollection($queryBuilder));
94- $doctrineOrmPaginator->setUseOutputWalkers($this->useOutputWalkers($queryBuilder));
64+ As mentioned above, by default we force eager loading for all relations. This behaviour can be modified with the
65+ configuration in order to apply it only on join relations having the ` EAGER ` fetch mode:
9566
96- return new Paginator($doctrineOrmPaginator);
97- }
67+ ``` yaml
68+ # app/config/config.yaml
9869
99- /**
100- * {@inheritdoc}
101- */
102- public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
103- {
104- return $this->decorated->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName);
105- }
70+ api_platform :
71+ # ...
10672
107- /**
108- * Determines whether the Paginator should fetch join collections, if the root entity uses composite identifiers it should not.
109- *
110- * @see https://github.com/doctrine/doctrine2/issues/2910
111- *
112- * @param QueryBuilder $queryBuilder
113- *
114- * @return bool
115- */
116- private function useFetchJoinCollection(QueryBuilder $queryBuilder): bool
117- {
118- return !QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry);
119- }
73+ eager_loading :
74+ force_eager : false
12075
121- /**
122- * Determines whether output walkers should be used.
123- *
124- * @param QueryBuilder $queryBuilder
125- *
126- * @return bool
127- */
128- private function useOutputWalkers(QueryBuilder $queryBuilder) : bool
129- {
130- /*
131- * "Cannot count query that uses a HAVING clause. Use the output walkers for pagination"
132- *
133- * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L50
134- */
135- if (QueryChecker::hasHavingClause($queryBuilder)) {
136- return true;
137- }
138- 139- /*
140- * "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator."
141- *
142- * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L87
143- */
144- if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder, $this->managerRegistry)) {
145- return true;
146- }
147- 148- /*
149- * "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers."
150- *
151- * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L149
152- */
153- if (
154- QueryChecker::hasMaxResults($queryBuilder) &&
155- QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $this->managerRegistry)
156- ) {
157- return true;
158- }
159- 160- /*
161- * When using composite identifiers pagination will need Output walkers
162- */
163- if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) {
164- return true;
165- }
166- 167- // Disable output walkers by default (performance)
168- return false;
169- }
170- }
76+ # ...
17177```
17278
173- The service definition:
79+ #### Override at resource and operation level
17480
175- ``` yaml
176- # app/config/services.yml
177- services :
178- app.doctrine.orm.query_extension.pagination_hint :
179- class : ' AppBundle\Doctrine\Orm\Extension\QueryHintPaginationExtension '
180- decorates : api_platform.doctrine.orm.query_extension.pagination
181- arguments : ['@doctrine', '@api_platform.doctrine.orm.query_extension.pagination_hint.inner']
182- ` ` `
81+ When eager loading is enabled, whatever the status of the ` force_eager ` parameter, you can easily override it directly
82+ from the configuration of each resource. You can do this at the resource level, at the operations level, or both:
83+ 84+ ``` php
85+ <?php
86+ // src/AppBundle/Entity/Address.php
87+ 88+ namespace AppBundle\Entity;
18389
184- To alter the ` Query` object on an item data provider, we can also create an `QueryHintExtension` which will alter the result:
90+ use ApiPlatform\Core\Annotation\ApiResource;
91+ use Doctrine\ORM\Mapping as ORM;
92+ 93+ /**
94+ * @ApiResource
95+ * @ORM\Entity
96+ */
97+ class Address
98+ {
99+ // ...
100+ }
101+ ```
185102
186103``` php
187104<?php
188- // src/AppBundle/Doctrine/Orm/Extension/QueryHintExtension .php
105+ // src/AppBundle/Entity/User .php
189106
190- namespace AppBundle\D octrine \O rm \E xtension ;
107+ namespace AppBundle\Entity ;
191108
192- use ApiPlatform\C ore\B ridge\D octrine\O rm\E xtension\Q ueryResultItemExtensionInterface;
193- use ApiPlatform\C ore\B ridge\D octrine\O rm\Util\Quer yNameGeneratorInterface;
194- use Doctrine\O RM\Q uery;
195- use Doctrine\O RM\Q ueryBuilder;
109+ use ApiPlatform\Core\Annotation\ApiResource;
110+ use Doctrine\ORM\Mapping as ORM;
196111
197- class QueryHintExtension implements QueryResultItemExtensionInterface
112+ /**
113+ * @ApiResource(attributes={"force_eager"=false})
114+ * @ORM\Entity
115+ */
116+ class User
198117{
199118 /**
200- * {@inheritdoc}
119+ * @var Address
120+ *
121+ * @ORM\ManyToOne(targetEntity="Address", fetch="EAGER")
201122 */
202- public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null);
203- {
204- }
123+ public $address;
205124
206125 /**
207- * {@inheritdoc}
126+ * @var Group[]
127+ *
128+ * @ORM\ManyToMany(targetEntity="Group", inversedBy="users")
129+ * @ORM\JoinTable(name="users_groups")
208130 */
209- public function supportsResult(string $resourceClass, string $operationName = null) : bool
210- {
211- return true;
212- }
131+ public $groups;
132+ }
133+ ```
213134
135+ ``` php
136+ <?php
137+ // src/AppBundle/Entity/Group.php
138+ 139+ namespace AppBundle\Entity;
140+ 141+ use ApiPlatform\Core\Annotation\ApiResource;
142+ use Doctrine\ORM\Mapping as ORM;
143+ 144+ /**
145+ * @ApiResource(
146+ * attributes={"force_eager"=false},
147+ * itemOperations={
148+ * "get"={"method"="GET", "force_eager"=true},
149+ * "post"={"method"="POST"}
150+ * },
151+ * collectionOperations={
152+ * "get"={"method"="GET", "force_eager"=true},
153+ * "post"={"method"="POST"}
154+ * }
155+ * )
156+ * @ORM\Entity
157+ */
158+ class Group
159+ {
214160 /**
215- * {@inheritdoc}
161+ * @var User[]
162+ *
163+ * @ManyToMany(targetEntity="User", mappedBy="groups")
216164 */
217- public function getResult(QueryBuilder $queryBuilder)
218- {
219- $query = $queryBuilder->getQuery();
220- $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true);
221-
222- return $query->getResult();
223- }
165+ public $users;
224166}
225167```
226168
227- The service definition :
169+ Be careful, the operation level is higher priority than the resource level but both are higher priority than the global
170+ configuration.
171+ 172+ #### Disable eager loading
173+ 174+ If for any reason you don't want the eager loading feature, you can turn off it in the configuration:
228175
229176``` yaml
230- # app/config/services.yml
177+ # app/config/config.yaml
178+ 179+ api_platform :
180+ # ...
231181
232- services:
233- api_platform.doctrine.orm.query_extension.hint:
234- class: 'AppBundle\D octrine\O rm\E xtension\Q ueryHintExtension'
235- tags:
236- - {name: api_platform.doctrine.orm.query_extension.item}
182+ eager_loading :
183+ enabled : false
184+ 185+ # ...
237186```
238187
188+ The whole configuration seen before will no longer work and Doctrine will recover its default behavior.
189+ 239190Previous chapter: [ Security] ( security.md )
240191
241192Next chapter: [ Operation Path Naming] ( operation-path-naming.md )
0 commit comments