Skip to content

True cursor pagination for Django #180

@mjtamlyn

Description

@mjtamlyn

Problem

The default pagination approach used for Django querysets executes a .count() on the query. For tables with hundreds of thousands or millions of rows, this is very slow on Postgres. It also assumes that the set is stable, which somewhat defeats the point of using a cursor based pagination system in the first place.

Possible solution

I released today django-cursor-pagination which provides a paginator class which fulfils the relay cursor connection spec. It is available on PyPI.

Below is a pretty simple approach to using this in Graphene, with inspiration taken from graphene-gae - thanks to @ekampf. It would want some checks around full_args, and possibly a means to explicitly set the ordering. It would also need testing and documenting.

Should we use this in graphene? There are some caveats of using this approach. Most importantly, the ordering fields in my current implementation must uniquely determine the items in the queryset. (Discussion of this restriction). Perhaps we should provide both options?

import graphene
from cursor_pagination import CursorPaginator

def connection_from_cursor_paginated(queryset, args, connection_type, edge_type, pageinfo_type, **kwargs):
    paginator = CursorPaginator(queryset, queryset.query.order_by)
    full_args = dict(args, **kwargs)
    page = paginator.page(**full_args)

    edges = []
    for item in page:
        edge = edge_type(node=item, cursor=paginator.cursor(item))
        edges.append(edge)

    return connection_type(
        edges=edges,
        page_info=pageinfo_type(
            start_cursor=paginator.cursor(page[0]),
            end_cursor=paginator.cursor(page[-1]),
            has_previous_page=page.has_previous,
            has_next_page=page.has_next,
        )
    )


class CursorPaginatedConnection(graphene.relay.types.Connection):
    @classmethod
    def from_list(cls, queryset, args, context, info):
        connection = connection_from_cursor_paginated(queryset, args, connection_type=cls, edge_type=cls.edge_type, pageinfo_type=graphene.relay.PageInfo)
        connection.set_connection_data(queryset)
        return connection


class CursorPaginatedConnectionField(graphene.relay.ConnectionField):
    def __init__(self, *args, **kwargs):
        kwargs['connection_type'] = kwargs.pop('connection_type', CursorPaginatedConnection)
        super(CursorPaginatedConnectionField, self).__init__(*args, **kwargs)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions