Add documentation for pagination

Closes gh-620
This commit is contained in:
rstoyanchev
2023-03-21 12:13:21 +00:00
parent 627d4f43fa
commit 4f873a31eb
4 changed files with 252 additions and 2 deletions

View File

@@ -568,6 +568,156 @@ to send to the client.
[[execution.pagination]]
=== Pagination
The https://relay.dev/graphql/connections.htm[GraphQL Cursor Connection specification]
defines a mechanism for efficient navigation of large result sets by returning a limited
subset of items at a time. Each item is assigned a unique cursor that a client can use to
request the next items after or the previous items before the cursor reference, as a way of
navigating forward or backward.
The spec calls this pattern "Connections", and each schema type whose name ends on
"Connection" is considered a _Connection Type_ and represents a paginated result set.
Each `Connection` contains "edges" where an `EdgeType` is a wrapper around the actual item
and its cursor. There is also a `PageInfo` object with flags for whether you can navigate
further forward and backward and the cursors of the start and end items in the set.
[[execution.pagination.type.definitions]]
==== Connection Type Definitions
`Connection` type definitions must be repeated for every type that needs pagination, adding
boilerplate and noise to the schema. To address this, Spring for GraphQL provides the
`ConnectionTypeDefinitionConfigurer` that adds these types on startup, if not already
present in the parsed schema files.
That means you can declare `Connection` fields, but leave out their declaration:
[source,graphql,indent=0,subs="verbatim,quotes"]
----
Query {
books: BookConnection
}
type Book {
id: ID!
title: String!
}
----
Then configure the `ConnectionTypeDefinitionConfigurer`:
[source,java,indent=0,subs="verbatim,quotes"]
----
GraphQlSource.schemaResourceBuilder()
.schemaResources(..)
.typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer)
----
The following type definitions are added on startup to the schema:
[source,graphql,indent=0,subs="verbatim,quotes"]
----
type BookConnection {
edges: [BookEdge]!
pageInfo: PageInfo!
}
type BookEdge {
node: Book!
cursor: String!
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
----
[[execution.pagination.adapters]]
==== Connection Adapters
Once <<execution.pagination.type.definitions>> are available in the schema, you also need
equivalent Java types. GraphQL Java provides those, including generic `Connection` and
`Edge` types, as well as `PageInfo`.
One option is to populate and return `Connection` directly from your controller method or
`DataFetcher`. However, this is boilerplate work, to wrap each item, create cursors, and
so on. Moreover, you may already have an underlying pagination mechanism such as when
using Spring Data repositories.
To make this transparent, Spring for GraphQL has a `ConnectionAdapter` contract to adapt
any container of items to `Connection`. This is applied through a
`ConnectionFieldTypeVisitor` that looks for any `Connection` field, decorates the
registered `DataFetcher`, and adapts its return values.
For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
ConnectionAdapter adapter = ... ;
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(adapter)) // <1>
GraphQlSource.schemaResourceBuilder()
.schemaResources(..)
.typeDefinitionConfigurer(..)
.typeVisitors(List.of(visitor)) // <2>
----
<1> Create type visitor with one or more `Connection` adapters.
<2> Resister the type visitor.
There are <<data.scroll.sort,built-in>> ``ConnectionAdapter``s for the Spring Data
pagination types `Window` and `Slice`. You can also create your own custom adapter.
`ConnectionAdapter` implementations rely on a <<execution.pagination.cursor.strategy>> to create a cursor for
each returned item. , and the same strategy is also used subsequently to decode the cursor
to support the <<controllers.schema-mapping.subrange>> controller method argument .
[[execution.pagination.cursor.strategy]]
==== Cursor Strategy
`CursorStrategy` is a contract to create a String cursor for an item to reflect its
position within a large result set, e.g. based on an offset or key set.
<<execution.pagination.adapters>> use this to create a cursor for returned items.
The strategy also helps to decode a cursor back to an item position. For this to work,
you need to declare a `CursorStrategy` bean, and ensure that annotated controllers are
<<controllers-declaration, enabled>>.
`CursorEncoder` is a related, supporting strategy to encode and decode cursors to make
them opaque to clients. `EncodingCursorStrategy` combines `CursorStrategy` with a
`CursorEncoder`. There is a built-in `Base64CursorEncoder`.
There is a <<data.scroll.sort,built-in>> `CursorStrategy` for the Spring Data `ScrollPosition`.
[[execution.pagination.arguments]]
==== Arguments
Controller methods can declare a <<controllers.schema-mapping.subrange>>, or a
`ScrollSubange` method argument, to handle requests for forward or backward pagination.
The method argument resolver is configured for use when a
<<execution.pagination.cursor.strategy>> bean is declared in Spring configuration.
[[execution.pagination.sort.strategy]]
==== Sort
Pagination depends on a stable sort order. There is no standard for how to declare sort
related GraphQL input arguments. You can keep it as an internal detail with a default
sort, or if it you need to expose it, then you'll need to extract the sort details from
GraphQL arguments.
There is partial, <<data.scroll.sort,built-in>> support for to create a Spring Data
`Sort`, with the help of a `SortStrategy`, and inject that into a controller method.
[[execution.batching]]
=== Batch Loading
@@ -1048,6 +1198,47 @@ required fields (or columns) are part of the database query result.
[[data.scroll.sort]]
=== Scroll and Sort
As explained in <<execution.pagination>>, the GraphQL Cursor Connection spec defines a
mechanism for pagination with the `Connection`, `Edge`, and `PageInfo` schema type, while
GraphQL Java provides the equivalent Java type representations.
Spring for GraphQL has built-in ``ConnectionAdapter``s to adapt the Spring Data pagination
types `Window` and `Slice` transparently. You can configure that as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
CursorStrategy<ScrollPosition> strategy = CursorStrategy.withEncoder(
new ScrollPositionCursorStrategy(),
CursorEncoder.base64()); // <1>
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(
new WindowConnectionAdapter(strategy),
new SliceConnectionAdapter(strategy))); // <2>
GraphQlSource.schemaResourceBuilder()
.schemaResources(..)
.typeDefinitionConfigurer(..)
.typeVisitors(List.of(visitor)); // <3>
----
<1> Create strategy to convert `ScrollPosition` to a Base64 encoded cursor.
<2> Create type visitor to adapt `Window` and `Slice` returned from ``DataFetcher``s.
<3> Register the type visitor.
On the request side, a controller method can declare a
<<controllers.schema-mapping.subrange,ScrollSubrange>> method argument to paginate forward
or backward. For this to work, you must declare a <<execution.pagination.cursor.strategy>>
supports `ScrollPosition` as a bean.
Spring for GraphQL defines a `SortStrategy` to create `Sort` from GraphQL arguments.
`AbstractSortStrategy` implements the contract with abstract methods to extract the sort
direction and properties. To enable support for `Sort` as a controller method argument,
you need to declare a `SortStrategy` bean.
[[controllers]]
== Annotated Controllers
@@ -1218,6 +1409,16 @@ See <<controllers.schema-mapping.projectedpayload.argument>>.
See <<controllers.schema-mapping.source>>.
| `Subrange` and `ScrollSubrange`
| For access to pagination arguments.
See <<execution.pagination>>, <<data.scroll.sort>>, <<controllers.schema-mapping.subrange>>.
| `Sort`
| For access to sort details.
See <<execution.pagination>>, <<data.scroll.sort>>, <<controllers.schema-mapping.sort>>.
| `DataLoader`
| For access to a `DataLoader` in the `DataLoaderRegistry`.
@@ -1455,6 +1656,51 @@ given a list of source/parent books objects.
====
[[controllers.schema-mapping.subrange]]
==== `Subrange`
When there is a <<execution.pagination.cursor.strategy>> bean in Spring configuration,
controller methods support a `Subrange<P>` argument where `<P>` is a relative position
converted from a cursor. For Spring Data, `ScrollSubrange` exposes `ScrollPosition`.
For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@QueryMapping
public Window<Book> books(ScrollSubrange subrange) {
ScrollPosition position = subrange.position().orElse(OffsetScrollPosition.initial())
int count = subrange.count().orElse(20);
// ...
}
}
----
[[controllers.schema-mapping.sort]]
==== `Sort`
When there is a <<data.scroll.sort,SortStrategy>> bean in Spring configuration, controller
methods support `Sort` as a method argument. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@QueryMapping
public Window<Book> books(Optional<Sort> optionalSort) {
Sort sort = optionalSort.orElse(Sort.by(..));
}
}
----
[[controllers.schema-mapping.data-loader]]
==== `DataLoader`