Add ability to add raw batch loader instances to BatchLoaderRegistry alongside registration methods
There are times when the registration methods for batch loaders are too limited or when using them creates code that is not in a preferred style for a project. Also, it is not possible to register a batch loader that returns a typed collection which itself is accurately typed when placed in the registry because we can't specify a value type with generics.
I've started implementing something that hopefully will be a good starting point for discussion around what I've proposed. I've tested it all locally and everything is working in this branch: https://github.com/spring-projects/spring-graphql/compare/main...bsara:spring-graphql:batch-loader-registry-updates. So, anyone should be able to pull it down and try it out. I don't have automated tests in place yet. I was hoping to get feedback and find out if these updates would even be considered before I went that far 😄.
Here's what I've done:
-
BatchLoaderRegistrycan now register loader classes directly. Something like this, for example:
...would be registered in@Component @RequiredArgsConstructor public class PersonBatchLoader implements MappedBatchLoader<String, Person> { private final MockData mockData; @Override public CompletionStage<Map<String, Person>> load(Set<String> personIds) { return supplyAsync(() -> { var people = mockData.getPeople(); return personIds.stream().collect(toMap(identity(), people::get)) }); } }BatchLoaderRegistryusing a Spring Boot config like this:@Bean public BatchLoaderRegistry batchLoaderRegistry( @Autowired(required = false) DataLoaderOptions defaultDataLoaderOptions, Collection<BatchLoader<?, ?>> batchLoaders, Collection<BatchLoaderWithContext<?, ?>> batchLoadersWithContext, Collection<MappedBatchLoader<?, ?>> mappedBatchLoaders, Collection<MappedBatchLoaderWithContext<?, ?>> mappedBatchLoadersWithContext, Collection<SpringBatchLoaderWithOptions> batchLoadersWithOptions ) { var registry = ( defaultDataLoaderOptions == null ? new DefaultBatchLoaderRegistry() : new DefaultBatchLoaderRegistry(() -> defaultDataLoaderOptions) ); batchLoaders.forEach(registry::register); batchLoadersWithContext.forEach(registry::register); mappedBatchLoaders.forEach(registry::register); mappedBatchLoadersWithContext.forEach(registry::register); batchLoadersWithOptions.forEach(registry::register); return registry; } - By default, the names of the generated
DataLoaderfor a batch loader class is the camelcase version of the batch loader class name. If the batch loader class name ends inBatchLoader, then it is replaced withDataLoaderin the data loader name.- Example: The
DataLoaderforPersonBatchLoaderwould, by default, be namedpersonDataLoader.
- Example: The
-
*WithOptionsinterfaces were added to allow one to create a component and provide the desiredDataLoaderOptionsthat would be connected with the correlating requestDataLoader. These also allow for specifying an explicit name for theDataLoaderthat is created if the default naming convention is not wanted.- All of the
*WithOptionsinterfaces inherit a singleSpringBatchLoaderWithOptionsinterface for ease of referencing them all. Inheritance ofSpringBatchLoaderWithOptionsis sealed to only the interfaces that were created. - Example:
@Component @RequiredArgsConstructor public class PersonBatchLoader implements MappedBatchLoaderWithOptions<String, Person> { private final MockData mockData; @Override public CompletionStage<Map<String, Person>> load(Set<String> personIds) { return supplyAsync(() -> { var people = mockData.getPeople(); return personIds.stream().collect(toMap(identity(), people::get)); }); } /** NOT required to override this method */ @Override public String getName() { return "peopleDataLoader"; } @Override public DataLoaderOptions getDataLoaderOptions() { return DataLoaderOptions.newOptions().setMaxBatchSize(42); } }
- All of the
-
BatchLoaderRegistrycan now register mapped and unmapped Reactive batch loaders which return collections without issue.- I noticed that there are issues with typing and resolution of data loaders because of the fact that we can't provide a collection with a generic when calling
forTypePairand similar typing issues occur when usingforNameto register a batch loader. -
registermethods were added that allow passing a name,DataLoaderOptions(optionally), and aBiFunctioninstance. This allows typing issues to be resolved without the need to provide explicit classes when registering a batch loader. - Example:
There is aregistry.registerMapped("parentsLoader", (Set<String> childrenIds, BatchLoaderEnvironment env) -> { Mono<Map<String, Collection<Person>>> childrenMap = personService.getParents(childrenIds); return childrenMap; });registermethod as well used for theFluxreturn values
- I noticed that there are issues with typing and resolution of data loaders because of the fact that we can't provide a collection with a generic when calling
- Added
FunctionBatchLoaderSpecto allow the option to create batch loaders as beans that can then be registered inBatchLoaderRegistry.- Example:
@Bean public FunctionBatchLoaderSpec<String, Person> personBatchLoaderSpec(MockData mockData) { return FunctionBatchLoaderSpec.createMapped("personLoader", (Set<String> personIds, BatchLoaderEnvironment env) -> { var people = getPeople(); return Mono.just(personIds.stream().collect(toMap(identity(), people::get))); }); } @Bean public BatchLoaderRegistry batchLoaderRegistry( @Autowired(required = false) DataLoaderOptions defaultDataLoaderOptions, Collection<FunctionBatchLoaderSpec> functionBatchLoaderSpecs, Collection<BatchLoader<?, ?>> batchLoaders, Collection<BatchLoaderWithContext<?, ?>> batchLoadersWithContext, Collection<MappedBatchLoader<?, ?>> mappedBatchLoaders, Collection<MappedBatchLoaderWithContext<?, ?>> mappedBatchLoadersWithContext, Collection<SpringBatchLoaderWithOptions> batchLoadersWithOptions ) { var registry = ( defaultDataLoaderOptions == null ? new DefaultBatchLoaderRegistry() : new DefaultBatchLoaderRegistry(() -> defaultDataLoaderOptions) ); functionBatchLoaderSpecs.forEach(registry::register); batchLoaders.forEach(registry::register); batchLoadersWithContext.forEach(registry::register); mappedBatchLoaders.forEach(registry::register); mappedBatchLoadersWithContext.forEach(registry::register); batchLoadersWithOptions.forEach(registry::register); return registry; }
- Example:
@bsara thanks for raising this discussion and happy to provide feedback, but could you please take a step back and better explain the issues you are facing in a new comment below? I see you have a detailed draft in a branch, but we can't look at a solution without understanding the problems it is trying to solve.
...could you please take a step back and better explain the issues you are facing in a new comment below?
There really are two issues here:
- The only ways in which one can add batch loaders to
BatchLoaderRegistryare via the@BatchMappingannotation, or manually adding a batch loader by creating aBiFunctionand usingBatchLoaderRegistry#forTypePairorBatchLoaderRegistry#forName. While the option of a functional approach is much appreciated, there is no way to add batch loader objects directly toBatchLoaderRegistry(which would be instances of any of the available interfaces provided byjava-dataloaderlikeBatchLoaderorMappedBatchLoaderWithContext). It would be desirable for reasons of easy reuse, lack of boilerplate, and the preferred coding style of some to allow batch loader component classes to be auto-registered withBatchLoaderRegistry(using Spring Boot auto-configuration). - With the current API, one cannot create a batch loader function where the value would be a collection and have the generic of the value collection come through when creating the loader. (Yes, one could just inline the
BiFunction, but there should be a way to use non-inlined instances as well that are accurately typed with generics). For example:
The first point is the main problem I'm attempting to solve, the second is something that I think goes along with providing alternate means to register batch loaders in general.
Bump