Pagination
What is Pagination
Pagination is about receiving data in chunk rather than as a whole.
This can help to improve performance, especially when the list of items is large or when the data is being retrieved over a slow connection.
In Graphql, pagination concerns with connection
between objects.
connection
between objects are represented as alist
.
This relationship can be best understood as:
- "Molly" has friends who are "Sam", "Dan", and "Cam".
{ "person": { "friends": [ { "name": "Sam" }, { "name": "Dan" }, { "name": "Cam" } ] } }
Slicing
Client may specify how many records to be fetched.
Client may ask for first two records to be fetched, but may request for the next two records to be fetched after in a separate request.
query { hero { name friends(first: 2) { name } } }
Offset
Pagination
Classic pagination approach.
Offset
pagination is most appropriate for page-based pagination feature.
- e.g.
page 1
has 1-10 records,page 2
has 11-20 records.s
entity_name(first:2 offset:2)
offset
is a start index.
- similar to
after
, returning records after the offset.
first
is a count of items to be retrieved (limit).
- returning at most
first
records.
first:4 first:3 (after offset) v v v v v v v [][][][][offset:4][][][][][][][] ^[zero-based-index]
Downsides
Performance and security downside when dealing with high volume/frequency data.
If new records are added after user had made a request, then offset calculation for subsequent pages may become ambiguous.
You typically query until an empty list comes back (to know that the pagination has reached the end).
- This is an additional call to verify the next page.
Requesting a specific chunk of the list by providing the number of items to be retrieved.
Connection-based
(e.g. raw id value or offset)
Characterized by use of nodes
(optionally, edges
).
entity_name(first:2 after:$entityId)
Why connection
?
Why name a list of edges
a connection
?
Why not just call it list
?
- A
connection
is a way to get all of thenodes
that are connected to anothernode
in a specific way.
How Connection
works
The connection
evaluates:
first
andafter
arguments to filter theedges
in offset based pagination.first
,last
,before
, andafter
arguments to filter theedges
in cursor based pagination.
So - connection
is about efficiently fetching the nodes (records).
In this case we want to get all of the nodes
connected to our users
that are friends
.
- Another
connection
might be between auser
node to all of theposts
that they liked which would be its own query.
# Offset Based Connection Pagination { user { id name friends(first: 10, after: 10) { # shortcut to get the nodes (records) nodes { id name } } } }
In above case, friends
is a connection
type.
Slicing is done with the first
argument to friends
.
- This asks for the connection to return
10
friends.
Pagination is done with the after
argument to friends by passing a cursor
or an offset
value.
- server to return friends
after
thecursor
or new offset value.
Availability of neighboring edges are sufaced under pageInfo
which has fields like hasNextPage
.
- that will tell us if there are more edges available, or if we’ve reached the end of this connection.
Field Requirements
Connection types
node must have fields named edges
and/or nodes
and pageInfo
.
Edges
edges
field must return a list
type that wraps an edge
type.
edge
type must be an object with fieldsnode
.
pageInfo
pageInfo
must have non-null boolean
fields of:
hasPreviousPage
- Indicates whether more edges exist prior to the set defined by client arguments (
boolean
).
- Indicates whether more edges exist prior to the set defined by client arguments (
hasNextPage
- Indicates whether more edges exist following the set defined by client arguments (
boolean
).
- Indicates whether more edges exist following the set defined by client arguments (
Should also have fields like:
Offset value
(current page)
This example queries for next 3
records after the provided entityId
.
- Provided
entityId
corresponds to the id of thelast record
in the previous result fetched.
For offset, this would be a number of records to skip.
first:4 first:3 (after entityId) v v v v v v v [][][][entityId][][][][][][][][]
Using PageInfo
pagination allows the client to retrieve the data in chunks in both directions.
- This makes cursor-based approach appropriate for infinite-scrolling.
Result
{ "data": { "hero": { "name": "R2-D2", "friendsConnection": { "totalCount": 3, "nodes": [ { "name": "Han Solo" }, { "name": "Leia Organa" } ], "pageInfo": { "hasNextPage": false } } } } }
Cursor
-based
Cursor
is a modified version of Connected
pagination concept.
Cursor
is an opaque identifier
corresponding to a record.
base64
encoding is recommended for making the record opaque.
Every record
in the list
is associated with a cursor
.
cursor
by default represents thelast item
that was fetched in the previous query.
Using cursor
gives flexibility if the pagination model changes in the future in any arbitrary way.
Clients paginating through the list then provide the cursor
of the starting record as well as a count of items to be retrieved.
type User { id: ID! name: String friends( # friends connection field first: Int, after: String, last: Int, before: String ): UserFriendsConnection } type UserFriendsConnection { edges: [UserEdge!]! pageInfo: PageInfo! } type UserEdge { node: User # actual item in the list cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }
edges
(connection)
For each edge in the connection
, we retrieve a cursor
.
cursor
is anopaque string
, and is precisely what we would pass to theafter
arg to paginate starting after the specifiededge
.
opaque
means the inner structure exists, but is unknown.
- e.g.
base64
translation of a string of Id.
We modify the connection field friends
to add edges
field.
- now, the
entity
should give a list of edges that has bothcursor
and the underlyingnode
(record).
Because cursor
is a connection property, not the Entity
object itself (unrelated to the entity
object itself), we need a new layer of indirection.
cursor
should not be a field on the entity directly.
We introduce the edges
field to the connection.
type UserFriendsConnection { pageInfo: PageInfo! edges: [UserFriendsEdge] # optional, can directly reference nodes } type UserFriendsEdge { node: User }
edge
can have additionalproperties of its own which act effectively as metadata.
type UserFriendsEdge { cursor: String! node: User # metadata fields friendedAt: DateTime anotherMetadataField: String }
{ user { id name friends(first: 10, after: "opaqueCursor") { edges { # always a list as a connection cursor node { # actual record id name } } pageInfo { hasNextPage } } } }
pageInfo
Has all the fields of connection
based pagination:
hasPreviousPage
hasNextPage
startCursor
endCursor
Both startCursor
and endCursor
can be null
when if there are no results.
Arguments
and Pagination Direction
Applies to both connection
and cursor
based pagination.
Forward Pagination (next n-records):
first
which is a non-negative integer.
- server should return at most
first
edge. edges(records) <= first
returned.
after
which is a cursor type
.
- server should return edges after the
after
cursor.
If you have a query that only uses first
and after
, you have a forward-only pagination.
Offset based pagination does not require backwards pagination.
- Offset is a single directional lookahead.
query { users(first:5, after:$cursor) { edges { ... } pageInfo { ... } } }
Backwards Pagination (n-records
before cursor
):
last
which is a non-negative integer
- server should return at most
last
edges. edges(records) <= last
returned.
before
which is a cursor type
- server should return
edges
before thebefore
cursor.
node
Each node
returned within edges
list corresponds to the records.
edge
Each edge
represents a connection
.
connection
represents a relationship between two objects (nodes
).
edges
is not a boilerplate field, it should contain additional metadata fields that accurately represents a relationship.
edges
can be used for exposing other things liketime_entity_spent_with_another_entity
(edge relationship metric).
query { hero { name friends(first: 2) { # this line is the entity edges { # always a list as a connection node { # actual record name } cursor # edges hold the cursor metadata_with_node # natural place for this field } } } }
pageInfo
type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }
If you need to get other cursor
positions from the object, you can reduce the need for querying in either direction by adding endCursor
or startCursor
to the pageInfo
field.
query { hero { name friends(first: 2) { # this line is the entity pageInfo { # added to not query for edges endCursor hasPreviousPage hasNextPage } totalCount # added to get total count of records edges { # always a list as a connection node { # actual record name } cursor # edges have the cursor time_with_another_entity # natural place for this field } } } }
Pagination Algorithm
EdgesToReturn(allEdges, before, after, first, last) - Let edges be the result of calling ApplyCursorsToEdges(allEdges, before, after). - If first is set: - If first is less than 0: - Throw an error. - If edges has length greater than than first: - Slice edges to be of length first by removing edges from the end of edges. - If last is set: - If last is less than 0: - Throw an error. - If edges has length greater than than last: - Slice edges to be of length last by removing edges from the start of edges. - Return edges. ApplyCursorsToEdges(allEdges, before, after) - Initialize edges to be allEdges. - If after is set: - Let afterEdge be the edge in edges whose cursor is equal to the after argument. - If afterEdge exists: - Remove all elements of edges before and including afterEdge. - If before is set: - Let beforeEdge be the edge in edges whose cursor is equal to the before argument. - If beforeEdge exists: - Remove all elements of edges after and including beforeEdge. - Return edges.