Update documentation for @BatchMapping

Closes gh-130
This commit is contained in:
Rossen Stoyanchev
2021-09-28 15:09:39 +01:00
parent 0bb8f58008
commit 5109a1195f

View File

@@ -288,33 +288,36 @@ default it is marked as `INTERNAL_ERROR`.
[[execution-batching]]
=== Batching
Given a `Book` and its `Author`, we can create one `DataFetcher` to load books and another
to load the author for each book. This enables queries to select only the data they need,
but when loading multiple books we end up loading the author for each book individually,
which is known as the N+1 select problem.
Given a `Book` and its `Author`, we can create one `DataFetcher` for books and another
for the author of a book. This means books and authors aren't automatically loaded
together, which enables queries to select the subset of data they need. However, when
loading multiple books, the author for each book is loaded individually, and this is
an issue known as the N+1 select problem.
To address the issue, GraphQL Java provides a
https://www.graphql-java.com/documentation/v16/batching/[batching feature] that allows
related entities, in this case the authors for all books, to be loaded together instead
of individually. Here is an outline of that mechanism:
related entities, such as the authors for all books, to be loaded together. This is how
the underlying mechanism works in GraphQL Java:
- At request time, an application can register a batch loading function in the
`DataLoaderRegistry` for each request, that can load instances of a given entity such as
`Author` from a set of unique keys.
- A `DataFetcher` can access the `DataLoader` for the entity and use it to load the
entity by its unique key.
- The `DataLoader` does not load the entity immediately but rather returns a promise, and
defers until it can use the batch loading function to load all related entities together.
- The `DataLoader` also maintains a cache of previously loaded entities.
- For each request, an application can register a batch loading function as a
`DataLoader` in the `DataLoaderRegistry` to assist with loading instances of a given
entity, such as `Author` from a set of unique keys.
- A `DataFetcher` can access the `DataLoader` for the entity and use it to load entity
instances; for example the author `DataFetcher` obtains the authorId from the `Book`
parent object, and uses it to load the `Author`.
- `DataLoader` does not load the entity immediately but rather returns a future, and
defers until it is ready to batch load all related entities as one.
- `DataLoader` additionally maintains a cache of previously loaded entities that can
further improve efficiency when the same entity is in multiple places of the response.
Spring GraphQL provides the `BatchLoaderRegistry` to store registrations of batch
loading functions. This is given to the `ExecutionGraphQlService` that in turn uses
it to make registrations in the `DataLoaderRegistry` for each request.
A `DataFetcher` can then look up a registered `DataLoader` and use it to load entity
instances, and likewise a controller method can declare a
<<controllers-data-loader,DataLoader argument>> to access the registered loader for the
entity.
Spring GraphQL exposes a `BatchLoaderRegistry` that accepts and stores registrations of
batch loading functions. The `ExecutionGraphQlService` accepts the registry as input and
uses it to make per request `DataLoader` registrations. A `DataFetcher` then looks up the
`DataLoader` for an entity and uses it to load instances, or in an annotated controller,
simply declare a <<controllers-data-loader,DataLoader argument>> to access the
registered loader. Annotated controllers also support a
<<controllers-batch-mapping,@BatchMapping>> that avoids the need to use `DataLoader`
directly.
The Spring Boot starter declares a
<<boot-graphql-batch-loader-registry,BatchLoaderRegistry bean>>, so that applications can
@@ -522,6 +525,99 @@ for fields under the Query, Mutation, and Subscription types respectively. For e
----
[[controllers-batch-mapping]]
=== Batch Mapping
<<execution-batching>> addresses the N+1 select problem through the use of an
`org.dataloader.DataLoader` to defer the loading of individual entity instances, so they
can be loaded together. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerBatchLoader((authorIds, environment) -> {
// how to load authors for the given author id's...
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
----
For the straight-forward case of loading an associated entity, shown above, the
`@SchemaMapping` method does nothing more than delegate to the `DataLoader`. This is
boilerplate that can be avoided with a `@BatchMapping` method. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@BatchMapping
public Flux<Author> author(List<Book> books) {
// ...
}
}
----
The above becomes a batch loading function in the `BatchLoaderRegistry`
where keys are `Book` instances and the loaded values their authors. In addition, a
`DataFetcher` is also transparently bound to the `author` field of the type `Book`, which
simply delegates to the `DataLoader` for authors, given its source/parent `Book` instance.
[TIP]
====
To be used as a unique key, `Book` must implement `hashcode` and `equals`.
====
By default, the field name defaults to the method name, while the type name defaults to
the simple class name of the input `List` element type. Both can be customized through
annotation attributes. The type name can also be inherited from a class level
`@SchemaMapping`.
A `@BatchMapping` method can be a
{javadoc}/org/springframework/graphql/execution/BatchLoaderRegistry.RegistrationSpec.html#registerMappedBatchLoader-java.util.function.BiFunction-[mapped batch loading] function:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@BatchMapping
public Mono<Map<Book, Author>> author(List<Book> books) {
// ...
}
}
----
It is possible to use imperative method signatures too, i.e. returning `List<V>` or
`Map<K, V>`, which can be useful when there are no remote calls to make.
`BatchMapping` methods support two types of arguments:
[cols="1,2"]
|===
| Method Argument | Description
| `List<K>`
| The source/parent objects.
| `BatchLoaderEnvironment`
| The environment that is available in GraphQL Java to a
`org.dataloader.BatchLoaderWithContext`.
|===
[[controllers-methods]]
=== Handler Methods
@@ -664,6 +760,12 @@ to locate it in the `DataLoaderRegistry`. As a fallback, the `DataLoader` method
resolver will also try the method argument name as the key but typically that should not
be necessary.
[TIP]
====
For straight-forward cases where the `@SchemaMapping` simply delegates to a `DataLoader`,
you can reduce boilerplate by using a <<controllers-batch-mapping,@BatchMapping>> method
instead.
====
[[controllers-graphql-context]]