|
|
|
|
@@ -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`
|
|
|
|
|
|
|
|
|
|
|