Skip to content

Commit 4f873a3

Browse files
committed
Add documentation for pagination
Closes gh-620
1 parent 627d4f4 commit 4f873a3

File tree

4 files changed

+252
-2
lines changed

4 files changed

+252
-2
lines changed

spring-graphql-docs/src/docs/asciidoc/index.adoc

+246
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,156 @@ to send to the client.
568568

569569

570570

571+
[[execution.pagination]]
572+
=== Pagination
573+
574+
The https://relay.dev/graphql/connections.htm[GraphQL Cursor Connection specification]
575+
defines a mechanism for efficient navigation of large result sets by returning a limited
576+
subset of items at a time. Each item is assigned a unique cursor that a client can use to
577+
request the next items after or the previous items before the cursor reference, as a way of
578+
navigating forward or backward.
579+
580+
The spec calls this pattern "Connections", and each schema type whose name ends on
581+
"Connection" is considered a _Connection Type_ and represents a paginated result set.
582+
Each `Connection` contains "edges" where an `EdgeType` is a wrapper around the actual item
583+
and its cursor. There is also a `PageInfo` object with flags for whether you can navigate
584+
further forward and backward and the cursors of the start and end items in the set.
585+
586+
587+
[[execution.pagination.type.definitions]]
588+
==== Connection Type Definitions
589+
590+
`Connection` type definitions must be repeated for every type that needs pagination, adding
591+
boilerplate and noise to the schema. To address this, Spring for GraphQL provides the
592+
`ConnectionTypeDefinitionConfigurer` that adds these types on startup, if not already
593+
present in the parsed schema files.
594+
595+
That means you can declare `Connection` fields, but leave out their declaration:
596+
597+
[source,graphql,indent=0,subs="verbatim,quotes"]
598+
----
599+
Query {
600+
books: BookConnection
601+
}
602+
603+
type Book {
604+
id: ID!
605+
title: String!
606+
}
607+
----
608+
609+
Then configure the `ConnectionTypeDefinitionConfigurer`:
610+
611+
[source,java,indent=0,subs="verbatim,quotes"]
612+
----
613+
GraphQlSource.schemaResourceBuilder()
614+
.schemaResources(..)
615+
.typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer)
616+
----
617+
618+
The following type definitions are added on startup to the schema:
619+
620+
[source,graphql,indent=0,subs="verbatim,quotes"]
621+
----
622+
type BookConnection {
623+
edges: [BookEdge]!
624+
pageInfo: PageInfo!
625+
}
626+
627+
type BookEdge {
628+
node: Book!
629+
cursor: String!
630+
}
631+
632+
type PageInfo {
633+
hasPreviousPage: Boolean!
634+
hasNextPage: Boolean!
635+
startCursor: String
636+
endCursor: String
637+
}
638+
----
639+
640+
641+
[[execution.pagination.adapters]]
642+
==== Connection Adapters
643+
644+
Once <<execution.pagination.type.definitions>> are available in the schema, you also need
645+
equivalent Java types. GraphQL Java provides those, including generic `Connection` and
646+
`Edge` types, as well as `PageInfo`.
647+
648+
One option is to populate and return `Connection` directly from your controller method or
649+
`DataFetcher`. However, this is boilerplate work, to wrap each item, create cursors, and
650+
so on. Moreover, you may already have an underlying pagination mechanism such as when
651+
using Spring Data repositories.
652+
653+
To make this transparent, Spring for GraphQL has a `ConnectionAdapter` contract to adapt
654+
any container of items to `Connection`. This is applied through a
655+
`ConnectionFieldTypeVisitor` that looks for any `Connection` field, decorates the
656+
registered `DataFetcher`, and adapts its return values.
657+
658+
For example:
659+
660+
[source,java,indent=0,subs="verbatim,quotes"]
661+
----
662+
ConnectionAdapter adapter = ... ;
663+
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(adapter)) // <1>
664+
665+
GraphQlSource.schemaResourceBuilder()
666+
.schemaResources(..)
667+
.typeDefinitionConfigurer(..)
668+
.typeVisitors(List.of(visitor)) // <2>
669+
----
670+
671+
<1> Create type visitor with one or more `Connection` adapters.
672+
<2> Resister the type visitor.
673+
674+
There are <<data.scroll.sort,built-in>> ``ConnectionAdapter``s for the Spring Data
675+
pagination types `Window` and `Slice`. You can also create your own custom adapter.
676+
677+
`ConnectionAdapter` implementations rely on a <<execution.pagination.cursor.strategy>> to create a cursor for
678+
each returned item. , and the same strategy is also used subsequently to decode the cursor
679+
to support the <<controllers.schema-mapping.subrange>> controller method argument .
680+
681+
682+
[[execution.pagination.cursor.strategy]]
683+
==== Cursor Strategy
684+
685+
`CursorStrategy` is a contract to create a String cursor for an item to reflect its
686+
position within a large result set, e.g. based on an offset or key set.
687+
<<execution.pagination.adapters>> use this to create a cursor for returned items.
688+
689+
The strategy also helps to decode a cursor back to an item position. For this to work,
690+
you need to declare a `CursorStrategy` bean, and ensure that annotated controllers are
691+
<<controllers-declaration, enabled>>.
692+
693+
`CursorEncoder` is a related, supporting strategy to encode and decode cursors to make
694+
them opaque to clients. `EncodingCursorStrategy` combines `CursorStrategy` with a
695+
`CursorEncoder`. There is a built-in `Base64CursorEncoder`.
696+
697+
There is a <<data.scroll.sort,built-in>> `CursorStrategy` for the Spring Data `ScrollPosition`.
698+
699+
700+
[[execution.pagination.arguments]]
701+
==== Arguments
702+
703+
Controller methods can declare a <<controllers.schema-mapping.subrange>>, or a
704+
`ScrollSubange` method argument, to handle requests for forward or backward pagination.
705+
The method argument resolver is configured for use when a
706+
<<execution.pagination.cursor.strategy>> bean is declared in Spring configuration.
707+
708+
709+
[[execution.pagination.sort.strategy]]
710+
==== Sort
711+
712+
Pagination depends on a stable sort order. There is no standard for how to declare sort
713+
related GraphQL input arguments. You can keep it as an internal detail with a default
714+
sort, or if it you need to expose it, then you'll need to extract the sort details from
715+
GraphQL arguments.
716+
717+
There is partial, <<data.scroll.sort,built-in>> support for to create a Spring Data
718+
`Sort`, with the help of a `SortStrategy`, and inject that into a controller method.
719+
720+
571721
[[execution.batching]]
572722
=== Batch Loading
573723

@@ -1048,6 +1198,47 @@ required fields (or columns) are part of the database query result.
10481198

10491199

10501200

1201+
[[data.scroll.sort]]
1202+
=== Scroll and Sort
1203+
1204+
As explained in <<execution.pagination>>, the GraphQL Cursor Connection spec defines a
1205+
mechanism for pagination with the `Connection`, `Edge`, and `PageInfo` schema type, while
1206+
GraphQL Java provides the equivalent Java type representations.
1207+
1208+
Spring for GraphQL has built-in ``ConnectionAdapter``s to adapt the Spring Data pagination
1209+
types `Window` and `Slice` transparently. You can configure that as follows:
1210+
1211+
[source,java,indent=0,subs="verbatim,quotes"]
1212+
----
1213+
CursorStrategy<ScrollPosition> strategy = CursorStrategy.withEncoder(
1214+
new ScrollPositionCursorStrategy(),
1215+
CursorEncoder.base64()); // <1>
1216+
1217+
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(
1218+
new WindowConnectionAdapter(strategy),
1219+
new SliceConnectionAdapter(strategy))); // <2>
1220+
1221+
GraphQlSource.schemaResourceBuilder()
1222+
.schemaResources(..)
1223+
.typeDefinitionConfigurer(..)
1224+
.typeVisitors(List.of(visitor)); // <3>
1225+
----
1226+
1227+
<1> Create strategy to convert `ScrollPosition` to a Base64 encoded cursor.
1228+
<2> Create type visitor to adapt `Window` and `Slice` returned from ``DataFetcher``s.
1229+
<3> Register the type visitor.
1230+
1231+
On the request side, a controller method can declare a
1232+
<<controllers.schema-mapping.subrange,ScrollSubrange>> method argument to paginate forward
1233+
or backward. For this to work, you must declare a <<execution.pagination.cursor.strategy>>
1234+
supports `ScrollPosition` as a bean.
1235+
1236+
Spring for GraphQL defines a `SortStrategy` to create `Sort` from GraphQL arguments.
1237+
`AbstractSortStrategy` implements the contract with abstract methods to extract the sort
1238+
direction and properties. To enable support for `Sort` as a controller method argument,
1239+
you need to declare a `SortStrategy` bean.
1240+
1241+
10511242

10521243
[[controllers]]
10531244
== Annotated Controllers
@@ -1218,6 +1409,16 @@ See <<controllers.schema-mapping.projectedpayload.argument>>.
12181409

12191410
See <<controllers.schema-mapping.source>>.
12201411

1412+
| `Subrange` and `ScrollSubrange`
1413+
| For access to pagination arguments.
1414+
1415+
See <<execution.pagination>>, <<data.scroll.sort>>, <<controllers.schema-mapping.subrange>>.
1416+
1417+
| `Sort`
1418+
| For access to sort details.
1419+
1420+
See <<execution.pagination>>, <<data.scroll.sort>>, <<controllers.schema-mapping.sort>>.
1421+
12211422
| `DataLoader`
12221423
| For access to a `DataLoader` in the `DataLoaderRegistry`.
12231424

@@ -1455,6 +1656,51 @@ given a list of source/parent books objects.
14551656
====
14561657

14571658

1659+
[[controllers.schema-mapping.subrange]]
1660+
==== `Subrange`
1661+
1662+
When there is a <<execution.pagination.cursor.strategy>> bean in Spring configuration,
1663+
controller methods support a `Subrange<P>` argument where `<P>` is a relative position
1664+
converted from a cursor. For Spring Data, `ScrollSubrange` exposes `ScrollPosition`.
1665+
For example:
1666+
1667+
[source,java,indent=0,subs="verbatim,quotes"]
1668+
----
1669+
@Controller
1670+
public class BookController {
1671+
1672+
@QueryMapping
1673+
public Window<Book> books(ScrollSubrange subrange) {
1674+
ScrollPosition position = subrange.position().orElse(OffsetScrollPosition.initial())
1675+
int count = subrange.count().orElse(20);
1676+
// ...
1677+
}
1678+
1679+
}
1680+
----
1681+
1682+
1683+
[[controllers.schema-mapping.sort]]
1684+
==== `Sort`
1685+
1686+
When there is a <<data.scroll.sort,SortStrategy>> bean in Spring configuration, controller
1687+
methods support `Sort` as a method argument. For example:
1688+
1689+
[source,java,indent=0,subs="verbatim,quotes"]
1690+
----
1691+
@Controller
1692+
public class BookController {
1693+
1694+
@QueryMapping
1695+
public Window<Book> books(Optional<Sort> optionalSort) {
1696+
Sort sort = optionalSort.orElse(Sort.by(..));
1697+
}
1698+
1699+
}
1700+
----
1701+
1702+
1703+
14581704
[[controllers.schema-mapping.data-loader]]
14591705
==== `DataLoader`
14601706

spring-graphql/src/main/java/org/springframework/graphql/data/pagination/Base64CursorEncoder.java

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
/**
2626
* {@link CursorEncoder} that applies Base 64 encoding and decoding.
2727
*
28+
* <p>To create an instance, use {@link CursorEncoder#base64()}.
29+
*
2830
* @author Rossen Stoyanchev
2931
* @since 1.2
3032
*/

spring-graphql/src/main/java/org/springframework/graphql/data/pagination/EncodingCursorStrategy.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
* Decorator for a {@link CursorStrategy} that applies a {@link CursorEncoder}
2323
* to the cursor String to make it opaque for external use.
2424
*
25-
* <p>Use {@link CursorStrategy#withEncoder(CursorStrategy, CursorEncoder)} to
26-
* decorate a {@code CursorStrategy}.
25+
* <p>To create an instance, use
26+
* {@link CursorStrategy#withEncoder(CursorStrategy, CursorEncoder)}.
2727
*
2828
* @author Rossen Stoyanchev
2929
* @since 1.2

spring-graphql/src/main/java/org/springframework/graphql/data/pagination/NoOpCursorEncoder.java

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
/**
2020
* {@link CursorEncoder} that leaves the cursor value unchanged.
2121
*
22+
* <p>To create an instance, use {@link CursorEncoder#noOpEncoder()}.
23+
*
2224
* @author Rossen Stoyanchev
2325
* @since 1.2
2426
*/

0 commit comments

Comments
 (0)