@@ -288,33 +288,36 @@ default it is marked as `INTERNAL_ERROR`.
288
288
[[execution-batching]]
289
289
=== Batching
290
290
291
- Given a `Book` and its `Author`, we can create one `DataFetcher` to load books and another
292
- to load the author for each book. This enables queries to select only the data they need,
293
- but when loading multiple books we end up loading the author for each book individually,
294
- which is known as the N+1 select problem.
291
+ Given a `Book` and its `Author`, we can create one `DataFetcher` for books and another
292
+ for the author of a book. This means books and authors aren't automatically loaded
293
+ together, which enables queries to select the subset of data they need. However, when
294
+ loading multiple books, the author for each book is loaded individually, and this is
295
+ an issue known as the N+1 select problem.
295
296
296
297
To address the issue, GraphQL Java provides a
297
298
https://www.graphql-java.com/documentation/v16/batching/[batching feature] that allows
298
- related entities, in this case the authors for all books, to be loaded together instead
299
- of individually. Here is an outline of that mechanism:
300
-
301
- - At request time, an application can register a batch loading function in the
302
- `DataLoaderRegistry` for each request, that can load instances of a given entity such as
303
- `Author` from a set of unique keys.
304
- - A `DataFetcher` can access the `DataLoader` for the entity and use it to load the
305
- entity by its unique key.
306
- - The `DataLoader` does not load the entity immediately but rather returns a promise, and
307
- defers until it can use the batch loading function to load all related entities together.
308
- - The `DataLoader` also maintains a cache of previously loaded entities.
309
-
310
- Spring GraphQL provides the `BatchLoaderRegistry` to store registrations of batch
311
- loading functions. This is given to the `ExecutionGraphQlService` that in turn uses
312
- it to make registrations in the `DataLoaderRegistry` for each request.
313
-
314
- A `DataFetcher` can then look up a registered `DataLoader` and use it to load entity
315
- instances, and likewise a controller method can declare a
316
- <<controllers-data-loader,DataLoader argument>> to access the registered loader for the
317
- entity.
299
+ related entities, such as the authors for all books, to be loaded together. This is how
300
+ the underlying mechanism works in GraphQL Java:
301
+
302
+ - For each request, an application can register a batch loading function as a
303
+ `DataLoader` in the `DataLoaderRegistry` to assist with loading instances of a given
304
+ entity, such as `Author` from a set of unique keys.
305
+ - A `DataFetcher` can access the `DataLoader` for the entity and use it to load entity
306
+ instances; for example the author `DataFetcher` obtains the authorId from the `Book`
307
+ parent object, and uses it to load the `Author`.
308
+ - `DataLoader` does not load the entity immediately but rather returns a future, and
309
+ defers until it is ready to batch load all related entities as one.
310
+ - `DataLoader` additionally maintains a cache of previously loaded entities that can
311
+ further improve efficiency when the same entity is in multiple places of the response.
312
+
313
+ Spring GraphQL exposes a `BatchLoaderRegistry` that accepts and stores registrations of
314
+ batch loading functions. The `ExecutionGraphQlService` accepts the registry as input and
315
+ uses it to make per request `DataLoader` registrations. A `DataFetcher` then looks up the
316
+ `DataLoader` for an entity and uses it to load instances, or in an annotated controller,
317
+ simply declare a <<controllers-data-loader,DataLoader argument>> to access the
318
+ registered loader. Annotated controllers also support a
319
+ <<controllers-batch-mapping,@BatchMapping>> that avoids the need to use `DataLoader`
320
+ directly.
318
321
319
322
The Spring Boot starter declares a
320
323
<<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
522
525
----
523
526
524
527
528
+ [[controllers-batch-mapping]]
529
+ === Batch Mapping
530
+
531
+ <<execution-batching>> addresses the N+1 select problem through the use of an
532
+ `org.dataloader.DataLoader` to defer the loading of individual entity instances, so they
533
+ can be loaded together. For example:
534
+
535
+ [source,java,indent=0,subs="verbatim,quotes"]
536
+ ----
537
+ @Controller
538
+ public class BookController {
539
+
540
+ public BookController(BatchLoaderRegistry registry) {
541
+ registry.forTypePair(Long.class, Author.class).registerBatchLoader((authorIds, environment) -> {
542
+ // how to load authors for the given author id's...
543
+ });
544
+ }
545
+
546
+ @SchemaMapping
547
+ public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
548
+ return loader.load(book.getAuthorId());
549
+ }
550
+
551
+ }
552
+ ----
553
+
554
+ For the straight-forward case of loading an associated entity, shown above, the
555
+ `@SchemaMapping` method does nothing more than delegate to the `DataLoader`. This is
556
+ boilerplate that can be avoided with a `@BatchMapping` method. For example:
557
+
558
+ [source,java,indent=0,subs="verbatim,quotes"]
559
+ ----
560
+ @Controller
561
+ public class BookController {
562
+
563
+ @BatchMapping
564
+ public Flux<Author> author(List<Book> books) {
565
+ // ...
566
+ }
567
+ }
568
+ ----
569
+
570
+ The above becomes a batch loading function in the `BatchLoaderRegistry`
571
+ where keys are `Book` instances and the loaded values their authors. In addition, a
572
+ `DataFetcher` is also transparently bound to the `author` field of the type `Book`, which
573
+ simply delegates to the `DataLoader` for authors, given its source/parent `Book` instance.
574
+
575
+ [TIP]
576
+ ====
577
+ To be used as a unique key, `Book` must implement `hashcode` and `equals`.
578
+ ====
579
+
580
+ By default, the field name defaults to the method name, while the type name defaults to
581
+ the simple class name of the input `List` element type. Both can be customized through
582
+ annotation attributes. The type name can also be inherited from a class level
583
+ `@SchemaMapping`.
584
+
585
+ A `@BatchMapping` method can be a
586
+ {javadoc}/org/springframework/graphql/execution/BatchLoaderRegistry.RegistrationSpec.html#registerMappedBatchLoader-java.util.function.BiFunction-[mapped batch loading] function:
587
+
588
+ [source,java,indent=0,subs="verbatim,quotes"]
589
+ ----
590
+ @Controller
591
+ public class BookController {
592
+
593
+ @BatchMapping
594
+ public Mono<Map<Book, Author>> author(List<Book> books) {
595
+ // ...
596
+ }
597
+ }
598
+ ----
599
+
600
+ It is possible to use imperative method signatures too, i.e. returning `List<V>` or
601
+ `Map<K, V>`, which can be useful when there are no remote calls to make.
602
+
603
+ `BatchMapping` methods support two types of arguments:
604
+
605
+ [cols="1,2"]
606
+ |===
607
+ | Method Argument | Description
608
+
609
+ | `List<K>`
610
+ | The source/parent objects.
611
+
612
+ | `BatchLoaderEnvironment`
613
+ | The environment that is available in GraphQL Java to a
614
+ `org.dataloader.BatchLoaderWithContext`.
615
+
616
+ |===
617
+
618
+
619
+
620
+
525
621
[[controllers-methods]]
526
622
=== Handler Methods
527
623
@@ -664,6 +760,12 @@ to locate it in the `DataLoaderRegistry`. As a fallback, the `DataLoader` method
664
760
resolver will also try the method argument name as the key but typically that should not
665
761
be necessary.
666
762
763
+ [TIP]
764
+ ====
765
+ For straight-forward cases where the `@SchemaMapping` simply delegates to a `DataLoader`,
766
+ you can reduce boilerplate by using a <<controllers-batch-mapping,@BatchMapping>> method
767
+ instead.
768
+ ====
667
769
668
770
669
771
[[controllers-graphql-context]]
0 commit comments