Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit f56f4e1

Browse files
Merge pull request #147 from meyerbaptiste/add_eager_loading_doc
Add documentation for eager loading
2 parents 80b3e9c + c10f7c3 commit f56f4e1

File tree

1 file changed

+117
-166
lines changed

1 file changed

+117
-166
lines changed

‎core/performance.md‎

Lines changed: 117 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -27,215 +27,166 @@ with a huge collection. [Here are some examples to index LIKE
2727
filters](http://use-the-index-luke.com/sql/where-clause/searching-for-ranges/like-performance-tuning) depending on your
2828
database 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\Doctrine\Orm\Extension;
107+
namespace AppBundle\Entity;
191108

192-
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultItemExtensionInterface;
193-
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
194-
use Doctrine\ORM\Query;
195-
use Doctrine\ORM\QueryBuilder;
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\Doctrine\Orm\Extension\QueryHintExtension'
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+
239190
Previous chapter: [Security](security.md)
240191

241192
Next chapter: [Operation Path Naming](operation-path-naming.md)

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /