Skip to main content
Version: 7.x.x

Data Loaders

Data Loaders are a popular caching pattern from the JavaScript GraphQL implementation. graphql-java provides support for this pattern using the DataLoader and DataLoaderRegistry.

Since graphql-kotlin allows you to abstract the schema generation and data fetching code, you may not even need data loaders if instead you have some persistant cache on your server.

class User(val id: ID) {
// The friendService and userService, which have nothing to do with GraphQL,
// should be concerned with caching and batch calls instead of your schema classes
fun getFriends(): List<User> {
val friends: List<ID> = friendService.getFriends(id)
return userService.getUsers(friends)
}

}

If you still want to use data loaders though, they are supported through the common interfaces.

graphql-kotlin-dataloader module provides convenient abstractions over the java-dataloader.

KotlinDataLoader

To help in the registration of DataLoaders, we have created an interface KotlinDataLoader:

interface KotlinDataLoader<K, V> {
val dataLoaderName: String
fun getDataLoader(): DataLoader<K, V>
}

This allows for library users to still have full control over the creation of the DataLoader and its various configuration options but also allows common server code to handle the registration, generation and execution of the request.

KotlinDataLoaderRegistryFactory

The GraphQLRequestHandler accepts an optional KotlinDataLoaderRegistryFactory. which generates a new KotlinDataLoaderRegistry on every request. The registry is a map of a unique data loader names to a DataLoader object that handles the cache for an output type in your graph. A DataLoader caches the types by some unique value, usually by the type id, and can handle different types of batch requests.

class UserDataLoader : KotlinDataLoader<ID, User> {
override val dataLoaderName = "UserDataLoader"
override fun getDataLoader() = DataLoaderFactory.newDataLoader<ID, User> { ids ->
CompletableFuture.supplyAsync {
ids.map { id -> userService.getUser(id) }
}
}
}

class FriendsDataLoader : KotlinDataLoader<ID, List<User>> {
override val dataLoaderName = "FriendsDataLoader"
override fun getDataLoader() = DataLoaderFactory.newDataLoader<ID, User>(
{ ids ->
CompletableFuture.supplyAsync {
ids.map { id ->
val friends: List<ID> = friendService.getFriends(id)
userService.getUsers(friends)
}
}
},
DataLoaderOptions.newOptions().setCachingEnabled(false)
)
}

val dataLoaderRegistryFactory = KotlinDataLoaderRegistryFactory(
UserDataLoader(), FriendsDataLoader()
)

val dataLoaderRegistry = dataLoaderRegistryFactory.generate()

KotlinDataLoaderRegistry

KotlinDataLoaderRegistry is a decorator of the original graphql-java DataLoaderRegistry that keeps track of all underlying DataLoaders futures. By keeping track of to cache map containing returned futures, we get more granular control when to dispatch data loader calls.

getValueFromDataLoader

graphql-kotlin-server includes a helpful extension function on the DataFetchingEnvironment so that you can easily retrieve values from the data loaders in your schema code.

class User(val id: ID) {
@GraphQLDescription("Get the users friends using data loader")
fun getFriends(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<User>> {
return dataFetchingEnvironment.getValueFromDataLoader("FriendsDataLoader", id)
}
}
info

Given that graphql-java relies on CompletableFutures for scheduling and asynchronous execution of DataLoader calls, currently we don't provide any native support for DataLoader pattern using coroutines. Instead, return the CompletableFuture directly from your DataLoaders. See issue #986.