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 a list
.This relationship can be best understood as:
{ "person": { "friends": [ { "name": "Sam" }, { "name": "Dan" }, { "name": "Cam" } ] } }
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
PaginationClassic pagination approach.
Offset
pagination is most appropriate for page-based pagination feature.
page 1
has 1-10 records, page 2
has 11-20 records.sentity_name(first:2 offset:2)
offset
is a start index.
after
, returning records after the offset.first
is a count of items to be retrieved (limit).
first
records.first:4 first:3 (after offset) v v v v v v v [][][][][offset:4][][][][][][][] ^[zero-based-index]
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).
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)
connection
?Why name a list of edges
a connection
?
Why not just call it list
?
connection
is a way to get all of the nodes
that are connected to another node
in a specific way.Connection
worksThe connection
evaluates:
first
and after
arguments to filter the edges
in offset based pagination.first
, last
, before
, and after
arguments to filter the edges
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
.
connection
might be between a user
node to all of the posts
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
.
10
friends.Pagination is done with the after
argument to friends by passing a cursor
or an offset
value.
after
the cursor
or new offset value.Availability of neighboring edges are sufaced under pageInfo
which has fields like hasNextPage
.
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 fields node
.pageInfo
pageInfo
must have non-null boolean
fields of:
hasPreviousPage
boolean
).hasNextPage
boolean
).Should also have fields like:
Offset value
(current page)This example queries for next 3
records after the provided entityId
.
entityId
corresponds to the id of the last 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.
Result
{ "data": { "hero": { "name": "R2-D2", "friendsConnection": { "totalCount": 3, "nodes": [ { "name": "Han Solo" }, { "name": "Leia Organa" } ], "pageInfo": { "hasNextPage": false } } } } }
Cursor
-basedCursor
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 the last 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 an opaque string
, and is precisely what we would pass to the after
arg to paginate starting after the specified edge
.opaque
means the inner structure exists, but is unknown.
base64
translation of a string of Id.We modify the connection field friends
to add edges
field.
entity
should give a list of edges that has both cursor
and the underlying node
(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.
first
which is a non-negative integer.
first
edge.edges(records) <= first
returned.after
which is a cursor type
.
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.
query { users(first:5, after:$cursor) { edges { ... } pageInfo { ... } } }
n-records
before cursor
):last
which is a non-negative integer
last
edges.edges(records) <= last
returned.before
which is a cursor type
edges
before the before
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 like time_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 } } } }
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.