Skip to content

Supports keeping the cursors for pagination #1915

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

Closed
wants to merge 4 commits into from

Conversation

sroze
Copy link
Contributor

@sroze sroze commented May 1, 2018

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets ø
License MIT
Doc PR ø

The background of this pull-request is explained in the following (public but non-published) blog post:
https://medium.com/@sroze/cursor-based-pagination-with-apiplatform-74fd1d324723

This is the first step, allowing to manually use the filters to achieve a cursor-based pagination. A follow-up PR will come with an obfuscation mechanism if this one goes through :)

@sroze sroze force-pushed the cursor-pagination branch 2 times, most recently from d3627b1 to c4731df Compare May 1, 2018 17:50
@sroze
Copy link
Contributor Author

sroze commented May 2, 2018

(CircleCI failure in unrelated by the way)

$lastObject = end($objects);

if (false !== $lastObject) {
$data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters']);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This... doesn't require the $lastObject to be not null

@soyuka
Copy link
Member

soyuka commented May 2, 2018

(CircleCI failure in unrelated by the way)

Issue with the merging stuff. Just rebase should be fine on master since #1917 .

private $pageParameterName;
private $enabledParameterName;

public function __construct(NormalizerInterface $collectionNormalizer, string $pageParameterName = 'page', string $enabledParameterName = 'pagination')
public function __construct(NormalizerInterface $collectionNormalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, string $pageParameterName = 'page', string $enabledParameterName = 'pagination')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new arguments should have a default value according to SF BC promise IIRC

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, if we apply the BC policy here I shouldn't change the argument order either 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, we can't do that :)

@@ -71,12 +75,40 @@ public function normalize($object, $format = null, array $context = [])
return $data;
}

$metadata = isset($context['resource_class']) ? $this->resourceMetadataFactory->create($context['resource_class']) : null;
$isPaginatedWithCursor = $paginated && null !== $metadata && null !== $cursorPaginationAttribute = $metadata->getAttribute('pagination_via_cursor');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about using getCollectionOperationAttribute() instead of getAttribute() ?
isn't collection_operation_name key set in the context ?

* @ORM\Entity
* @ApiResource(attributes={
* "pagination_partial"=true,
* "pagination_via_cursor"={
Copy link
Contributor

@bendavies bendavies May 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about supporting multiple fields? this is often how it is done in postgres...
SELECT * FROM table WHERE (date, id) < (prev_date, prev_id) ORDER BY date DESC, id DESC LIMIT 10;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about adding more later but I think you're right, we can start with multiple.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple fields added 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than just supporting fields, it might be needed. Unlike the sorting for a regular pagination, the cursor must be unique. Although not efficient performance-wise, the safest way to achieve this is to always append (when necessary as it requires what fields are already present) a sort by ID as a last order by.

I also think you need to account for the NULL values as they might mess up the order

* }
* })
*
* @ApiFilter(RangeFilter::class, properties={"id"})
Copy link
Contributor

@bendavies bendavies May 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so these filters must match the pagination_via_cursor. should there be a check for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, shouldn't we put these filters automatically based on the pagination_via_cursor configuration?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Think so!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've looked at it, will do that in another PR while refactoring the pagination actually.

$forwardRangeOperator = 'desc' === strtolower($cursorPaginationAttribute['direction']) ? 'lt' : 'gt';
$backwardRangeOperator = 'gt' === $forwardRangeOperator ? 'lt' : 'gt';

$accessor = PropertyAccess::createPropertyAccessor();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should reuse the existing service if it exists (and create it only if null is passed in the constructor). This way we'll benefit from the component's cache.

@sroze sroze force-pushed the cursor-pagination branch from 86e3e03 to 8f31842 Compare May 12, 2018 14:48
@sroze
Copy link
Contributor Author

sroze commented May 12, 2018

Status: Needs review

@sroze sroze force-pushed the cursor-pagination branch from 8f31842 to f2d247a Compare May 12, 2018 14:57
@@ -71,12 +78,29 @@ public function normalize($object, $format = null, array $context = [])
return $data;
}

$metadata = isset($context['resource_class']) && null !== $this->resourceMetadataFactory ? $this->resourceMetadataFactory->create($context['resource_class']) : null;
$isPaginatedWithCursor = $paginated && null !== $metadata && null !== $cursorPaginationAttribute = $metadata->getCollectionOperationAttribute($context['collection_operation_name'], 'pagination_via_cursor', null, true);
Copy link
Member

@soyuka soyuka May 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find these lines a bit long but apart from that 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I split them, it will be longer (i.e. repeating these metadata !== null) 🙊

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be extracted into a private method?

@meyerbaptiste
Copy link
Member

What about HAL (ApiPlatform\Core\Hal\Serializer\CollectionNormalizer)?

if ($paginated) {
if ($isPaginatedWithCursor) {
$objects = iterator_to_array($object);
$firstObject = current($objects);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will fail if there is no object

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will return false if there is no object and this is tested below, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, what @meyerbaptiste said

@sroze
Copy link
Contributor Author

sroze commented May 14, 2018

What about HAL (ApiPlatform\Core\Hal\Serializer\CollectionNormalizer)?

Whoever needs it will add it? 🤷‍♂️

@bendavies
Copy link
Contributor

it would be nice if all output formats support a major feature like this. otherwise it sets a precedent that contributors only add features to the formats they use, which will result in not great framework caveats of "Awesome Feature XXXX (only supported for JSON API)"

@sroze
Copy link
Contributor Author

sroze commented May 14, 2018

@bendavies I do agree with you. Do you want to add HAL support in this PR? 😉

@bendavies
Copy link
Contributor

i could but it would not be for a few weeks. currently on vacation

@teohhanhui
Copy link
Contributor

About the format support, on the other hand, we would not want to discourage contribution because of the need to support all formats? And sometimes it's just better for people who are actually familiar with the format / who actually use it to add the feature.

@teohhanhui
Copy link
Contributor

"Awesome Feature XXXX (only supported for JSON API)"

Only supported for JSON-LD / Hydra is acceptable to me, as it's the default / recommended(?) format. But not for the other formats. 😛

@trsteel88
Copy link
Contributor

This PR doesn't support duplicate values for something you are sorting by. e.g if you are sorting by "createdAt" and there are multiple entities it needs to check for a backup field like id.

Query would be something like:

entity.createdAt > [DATE from first/last collection] or (entity.createdAt = [DATE from first/last collection] and entity.id > [FIRST OR LAST ID OF COLLECTION])

direction of the > is going to change depending on which way you are going in the pagination.

@bendavies
Copy link
Contributor

it supports multiple fields. it's up to the developer to use the fields they need to do the sorting.

@sroze
Copy link
Contributor Author

sroze commented Aug 28, 2018

What do you expect as a change in here except fixing the merge conflicts? Can we go ahead without all the formats?

@dunglas
Copy link
Member

dunglas commented Aug 28, 2018

Let’s go ahead

@sroze sroze force-pushed the cursor-pagination branch from f2d247a to a60b771 Compare August 28, 2018 17:47
@sroze
Copy link
Contributor Author

sroze commented Aug 28, 2018

Rebased on master 👍

{
$this->collectionNormalizer = $collectionNormalizer;
$this->pageParameterName = $pageParameterName;
$this->enabledParameterName = $enabledParameterName;
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??

@soyuka
Copy link
Member

soyuka commented Oct 4, 2018

ping @sroze we'd like to merge this one

@bendavies
Copy link
Contributor

Let's get this going again. It's an awesome feature.
I'd like to add hal support too

@soyuka
Copy link
Member

soyuka commented Feb 20, 2019

Thanks for the feature @sroze, I merged this onto master in #2532!

@soyuka soyuka closed this Feb 20, 2019
@sroze
Copy link
Contributor Author

sroze commented Feb 20, 2019 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants