-
-
Notifications
You must be signed in to change notification settings - Fork 921
Add a partial paginator to prevent SQL COUNT queries #1292
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a partial paginator to prevent SQL COUNT queries #1292
Conversation
use ApiPlatform\Core\DataProvider\PartialPaginatorInterface; | ||
use Doctrine\ORM\Tools\Pagination\Paginator; | ||
|
||
abstract class AbstractPaginator implements \IteratorAggregate, PartialPaginatorInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI,
abstract class AbstractPaginator implements PartialPaginatorInterface, \IteratorAggregate
=> fatal error
abstract class AbstractPaginator implements \IteratorAggregate, PartialPaginatorInterface
=> it works
🤔 🤣
* | ||
* @return mixed | ||
*/ | ||
public function getResult(QueryBuilder $queryBuilder); | ||
public function getResult(QueryBuilder $queryBuilder/*, string $resourceClass, string $operationName = null*/); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To keep consistency with QueryResultCollectionExtensionInterface
but yes, maybe it's useless...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep it, at least we have them if we need it later :P.
Shouldn't you change the |
I also noticed this performance hit. |
1219528
to
d8af927
Compare
👍 @bendavies thought about this too but the doctrine paginator is a dark place to change things like this (it works on the same basis QueryBuilder for all the queries). If you managed to do it I'd be interested in the how. |
@soyuka i can't see much apart from writing our own Paginator or decorating it and overriding |
I meant to remove |
If I remember correctly, on the project we profiled there was already a decorator to remove in some cases the |
c7bc360
to
3a74628
Compare
@@ -96,13 +96,16 @@ public function getConfigTreeBuilder() | |||
->addDefaultsIfNotSet() | |||
->children() | |||
->booleanNode('enabled')->defaultTrue()->info('To enable or disable pagination for all resource collections by default.')->end() | |||
->booleanNode('partial')->defaultFalse()->info('')->end() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add an info
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops!
|
||
$currentPage = $lastPage = $itemsPerPage = null; | ||
if ($isPaginator) { | ||
$currentPage = $lastPage = $itemsPerPage = $pageTotalItems = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whew, lots of variables initiated to nil
, is this required?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of this?
------ ---------------------------------------------
Line src/Hal/Serializer/CollectionNormalizer.php
------ ---------------------------------------------
87 Undefined variable: $currentPage
92 Undefined variable: $lastPage
97 Undefined variable: $currentPage
98 Undefined variable: $currentPage
101 Undefined variable: $currentPage
101 Undefined variable: $pageTotalItems
101 Undefined variable: $itemsPerPage
102 Undefined variable: $currentPage
123 Undefined variable: $itemsPerPage
------ ---------------------------------------------
------ ----------------------------------------------------------
Line src/Hydra/Serializer/PartialCollectionViewNormalizer.php
------ ----------------------------------------------------------
75 Undefined variable: $currentPage
80 Undefined variable: $lastPage
85 Undefined variable: $currentPage
86 Undefined variable: $currentPage
89 Undefined variable: $currentPage
89 Undefined variable: $pageTotalItems
89 Undefined variable: $itemsPerPage
90 Undefined variable: $currentPage
------ ----------------------------------------------------------
if (1. === $currentPage && 1. === $lastPage) { | ||
// Consider the collection not paginated if there is only one page | ||
$paginated = false; | ||
$currentPage = $lastPage = $itemsPerPage = $pageTotalItems = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same I wonder why is this needed
3a74628
to
6a2f4ad
Compare
|
||
if (null === ($firstResult = $query->getFirstResult()) || null === $maxResults = $query->getMaxResults()) { | ||
throw new InvalidArgumentException(sprintf('"%1$s::setFirstResult()" or/and "%1$s::setMaxResults()" was/were not applied to the query.', Query::class)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw the test below but I don't get why we need such message (as we didn't needed it before)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because these two methods can potentially return null
. See the Doctrine PHPDoc:
Returns NULL if {link setFirstResult} was not applied to this query.
Returns NULL if {link setMaxResults} was not applied to this query.
We always call Query::setFirstResult()
and Query::setMaxResults()
before instantiating our Paginator
class, it's why we never had the bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We always call Query::setFirstResult() and Query::setMaxResults() before instantiating our Paginator class, it's why we never had the bug.
Yes that's the reason why we don't need the test no? I mean we're still always using setFirstResult
and setMaxResults
or I miss something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes but this is a library and this class is not internal so users are free to use it as they want. IMO we have to prevent a fatal error by preventing this specific use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay wasn't sure this was the reason but I thought about it! Thanks for clarifying!
6a2f4ad
to
577535f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
phpstan issues but then 👍
577535f
to
c888e89
Compare
890521a
to
a15af93
Compare
a15af93
to
347ad0e
Compare
@meyerbaptiste could you please rebase ? this should be ok to merge, @dunglas could you please look at this ? |
347ad0e
to
b0c86a8
Compare
@meyerbaptiste could you rebase again, so we can merge it ? |
724dcf2
to
ec5d87a
Compare
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getIterator() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can add the \Traversable
return type even if not in the parent class.
phpstan.neon
Outdated
@@ -14,3 +14,8 @@ parameters: | |||
- '#Parameter \#2 \$dqlPart of method Doctrine\\ORM\\QueryBuilder::add\(\) expects Doctrine\\ORM\\Query\\Expr\\Base, Doctrine\\ORM\\Query\\Expr\\Join\[\] given#' # Fixed in Doctrine's master | |||
- '#Call to an undefined method Doctrine\\Common\\Persistence\\ObjectManager::getConnection\(\)#' | |||
- '#Parameter \#1 \$callable of static method Doctrine\\Common\\Annotations\\AnnotationRegistry::registerLoader\(\) expects callable, mixed\[\] given#' | |||
- '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\QueryResult(Item|Collection)ExtensionInterface::getResult\(\) invoked with 3 parameters, 1 required#' | |||
- '#Undefined variable: \$currentPage#' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These Undefined variable
look like code smell.
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function count() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here (: int
)
if (func_num_args() >= 2) { | ||
$resourceClass = func_get_arg(1); | ||
} else { | ||
@trigger_error(sprintf('Method %s() will have a 2nd `string $resourceClass` argument in version 3.0. Not defining it is deprecated since 2.1.', __METHOD__), E_USER_DEPRECATED); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2.2 (same everywhere)
$doctrineOrmPaginator = new DoctrineOrmPaginator($queryBuilder, $this->useFetchJoinCollection($queryBuilder)); | ||
$doctrineOrmPaginator->setUseOutputWalkers($this->useOutputWalkers($queryBuilder)); | ||
|
||
$resourceMetadata = null !== $resourceClass ? $this->resourceMetadataFactory->create($resourceClass) : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null === $resourceClass ? null : $this->resourceMetadataFactory->create($resourceClass)
(negation not needed).
$enabled = $this->partial; | ||
$clientEnabled = $this->clientPartial; | ||
|
||
if ($resourceMetadata) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null !==
(same for all tests below)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I do it after yesterday's conversation? 😛
|
||
/** | ||
* Decorates the Doctrine ORM paginator. | ||
* | ||
* @author Kévin Dunglas <[email protected]> | ||
*/ | ||
final class Paginator implements \IteratorAggregate, PaginatorInterface | ||
final class Paginator extends AbstractPaginator implements PaginatorInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
implements PaginatorInterface
can be removed (already implemented by the abstract
class).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nop, AbstractPaginator
implements PartialPaginatorInterface
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, keep it as is :) Sorry.
->booleanNode('client_enabled')->defaultFalse()->info('To allow the client to enable or disable the pagination.')->end() | ||
->booleanNode('client_items_per_page')->defaultFalse()->info('To allow the client to set the number of items per page.')->end() | ||
->booleanNode('client_partial')->defaultFalse()->info('')->end() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
info('')
must be filled (BTW, I'm not sure that it's really necessary to be able to set this option client-side, but as it's implemented, keep it)
|
||
$currentPage = $lastPage = $itemsPerPage = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think those changes introduce the PHPStan errors. It should probably be reverted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, see #1292 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels weird to me to declare empty variables but maybe it's better... Your call @dunglas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes the execution path more predictible for static analysis tools (and even for humans IMO).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Humans > Robots
658a499
to
cefd25e
Compare
Done @dunglas. |
if ($object instanceof PaginatorInterface) { | ||
$paginated = 1. !== $lastPage = $object->getLastPage(); | ||
} else { | ||
$lastPage = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this one needed if we declared it above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
if ($object instanceof PaginatorInterface) { | ||
$paginated = 1. !== $lastPage = $object->getLastPage(); | ||
} else { | ||
$lastPage = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same now that it's declared above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
cefd25e
to
9480dc7
Compare
Thanks @meyerbaptiste :D |
Thanks a lot, great job 👍 |
…formances Add a partial paginator to prevent SQL COUNT queries
Recently we had to profile an API Platform application. This one had quite long response times because of the Doctrine paginator and more specifically the SQL
COUNT()
query it executes. Based on theses observations, I created a partial paginator which doesn't execute this query and therefore returns only the previous and the next page.Next step: next and previous page with a cursor (Facebook, Twitter, etc. like).
Cc @thomasglachant