Support per-request BatchLoaderContextProviders in data loaders and free up BatchLoaderEnvironment key contexts
alex-lange opened this issue · comments
Is your feature request related to a problem? Please describe.
Per the graphql-java data loader docs :
The data loader library supports two types of context being passed to the batch loader. The first is an overall context object per dataloader, and the second is a map of per loaded key context objects.
The current KotlinDataLoader
interface makes it difficult to provide per-request objects (e.g. the GraphQLContext
) for the former "overall context". We would usually define this BatchLoaderContextProvider
when creating the data loader, but the KotlinDataLoader#getDataLoader()
function (which instantiates the data loaders for each request) has no means of passing in per-request context to make the BatchLoaderContextProvider
helpful.
And while it is possible to define the latter "key context" objects when the data loader load
function is called, the graphql-kotlin documentation recommends overloading this behavior by passing in the GraphQLContext
and using the DataFetchingEnvironment#getGraphQLContext() extension function to access it.
By overloading per-item key contexts with the GraphQLContext
, we are unable to use both types of contexts in our data loaders.
Describe the solution you'd like
Ideally the API would have a way to specify the BatchLoaderContextProvider
(or something similar) that is automatically included in the KotlinDataLoader#getDataLoader
call. This definition would probably trickle down from the GraphQLRequestHandler#executeRequest
, which calls KotlinDataLoaderRegistryFactory#generate
, which then calls KotlinDataLoader#getDataLoader
.
If this were possible, we could use it to provide the GraphQLContext
in the "overall context", and free up the "key context" for what it is intended to be used for.
Describe alternatives you've considered
We were able to work around this by defining our own GraphQLRequestHandler
subclass that essentially changes this line of executeRequest to have a custom DataLoaderRegistryFactory
whose generate
function allows for the GraphQLContext
on input. This is a pretty rough approach because the functions that executeRequest
calls are all private
to GraphQLRequestHandler
, meaning if we want to change only that line in executeRequest
in our subclass, we cannot re-use the logic that calls the private functions.
So, a small change that would clean up our "quick and dirty" solution would be to change GraphQLRequestHandler
's private
methods to protected
.
Additional context
In case it is helpful, here is an example of my described "quick and dirty" work around:
class RequestHandlerWithDataloaderContext(
private val graphQLEngine: GraphQL,
private val dataloaderFactories: List<DataLoaderWithContextFactory<*, *>>,
private val batchLoaderContextProviderFactory: (GraphQLContext) -> BatchLoaderContextProvider
) : GraphQLRequestHandler(
graphQLEngine,
// We're not going to use the built-in registry factory
dataLoaderRegistryFactory = null
) {
// This is effectively what a new KotlinDataLoaderRegistryFactory would do if it supported BatchLoaderContextProvider in its generate() function
private fun generateDataLoaderRegistry(batchLoaderContextProvider: BatchLoaderContextProvider): KotlinDataLoaderRegistry {
val registry = DataLoaderRegistry()
dataloaderFactories.forEach { d ->
registry.register(
d::class.jvmName,
d.getDataLoader(batchLoaderContextProvider)
)
}
return KotlinDataLoaderRegistry(registry)
}
override suspend fun executeRequest(
graphQLRequest: GraphQLServerRequest,
graphQLContext: GraphQLContext
): GraphQLServerResponse {
val dataLoaderRegistry = generateDataLoaderRegistry(batchLoaderContextProviderFactory(graphQLContext))
return when (graphQLRequest) {
is GraphQLRequest -> {
val batchGraphQLContext = graphQLContext + getBatchContext2(1, dataLoaderRegistry)
execute2(graphQLRequest, batchGraphQLContext, dataLoaderRegistry)
}
else -> TODO()
}
}
...
// Rest of the class is copies of the private functions (e.g. `execute` is copied to `execute2`, `getBatchContext` is copied to `getBatchContext2`)
interface DataLoaderWithContextFactory<K, V> {
val loader: BatchLoaderWithContext<K, V>
fun getDataLoader(batchLoaderContextProvider: BatchLoaderContextProvider): DataLoader<K, V> =
DataLoaderFactory.newDataLoader(
loader,
DataLoaderOptions.newOptions().setBatchLoaderContextProvider(batchLoaderContextProvider)
)
}
Sorry for the late response,
And while it is possible to define the latter "key context" objects when the data loader load function is called, the graphql-kotlin documentation recommends overloading this behavior by passing in the GraphQLContext and using the DataFetchingEnvironment#getGraphQLContext() extension function to access it.
the main reason why we recommended this approach is to access to the standardized CoroutineScope to execute a coroutine in a dispatch fn of the DataLoader, there should be a way to free up the key context objects per load call by adding an abstraction over our KotlinDataLoaders, will look into it.
Apologies for the delay --
#1785