Async Models
By default, graphql-kotlin-schema-generator
will resolve all functions synchronously, i.e. it will block the
underlying thread while executing the target function. While you could configure your GraphQL server with execution
strategies that execute each query in parallel on some thread pools, instead we highly recommend to utilize asynchronous
programming models.
Coroutines
graphql-kotlin-schema-generator
has built-in support for Kotlin coroutines. Provided default
FunctionDataFetcher
will automatically asynchronously execute suspendable functions and convert the result to CompletableFuture
expected
by graphql-java
.
Example
data class User(val id: String, val name: String)
class Query {
suspend fun getUser(id: String): User {
// Your coroutine logic to get user data
}
}
will produce the following schema
type Query {
getUser(id: String!): User
}
type User {
id: String!
name: String!
}
Structured Concurrency
graphql-java
relies on CompletableFuture
for asynchronous execution of the incoming requests. CompletableFuture
is
unaware of any contextual information which means we have to rely on additional mechanism to propagate the coroutine context.
graphql-java
v17 introduced GraphQLContext
map as the default mechanism to propagate the contextual information about
the request. In order to preserve coroutine context, we need to populate GraphQLContext
map with a CoroutineScope
that
should be used to execute any suspendable functions.
val graphQLExecutionScope = CoroutineScope(coroutineContext + SupervisorJob())
val contextMap = mapOf(
CoroutineScope::class to graphQLExecutionScope
)
val executionInput = ExecutionInput.newExecutionInput()
.graphQLContext(contextMap)
.query(queryString)
.build()
graphql.executeAsync(executionInput)
graphql-kotlin-server
automatically populates GraphQLContext
map with appropriate coroutine scope. Users can customize
the coroutine context by providing CoroutineContext::class
entry in custom context using GraphQLContextFactory
.
CompletableFuture
graphql-java
relies on Java CompletableFuture
for asynchronously processing the requests. In order to simplify the
interop with graphql-java
, graphql-kotlin-schema-generator
has a built-in hook which will automatically unwrap a
CompletableFuture
and use the inner class as the return type in the schema.
data class User(val id: String, val name: String)
class Query {
fun getUser(id: String): CompletableFuture<User> {
// Your logic to get data asynchronously
}
}
will result in the exactly the same schema as in the coroutine example above.
RxJava/Reactor
If you want to use a different monad type, like Single
from RxJava or Mono
from
Project Reactor, you have to:
- Create custom
SchemaGeneratorHook
that implementswillResolveMonad
to provide the necessary logic to correctly unwrap the monad and return the inner class to generate valid schema
class MonadHooks : SchemaGeneratorHooks {
override fun willResolveMonad(type: KType): KType = when (type.classifier) {
Mono::class -> type.arguments.firstOrNull()?.type
else -> type
} ?: type
}
- Provide custom data fetcher that will properly process those monad types.
class CustomFunctionDataFetcher(target: Any?, fn: KFunction<*>, objectMapper: ObjectMapper) : FunctionDataFetcher(target, fn, objectMapper) {
override fun get(environment: DataFetchingEnvironment): Any? = when (val result = super.get(environment)) {
is Mono<*> -> result.toFuture()
else -> result
}
}
class CustomDataFetcherFactoryProvider(
private val objectMapper: ObjectMapper
) : SimpleKotlinDataFetcherFactoryProvider(objectMapper) {
override fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>): DataFetcherFactory<Any> = DataFetcherFactory<Any> {
CustomFunctionDataFetcher(
target = target,
fn = kFunction,
objectMapper = objectMapper)
}
}
With the above you can then create your schema as follows:
class ReactorQuery {
fun asynchronouslyDo(): Mono<Int> = Mono.just(1)
}
val configWithReactorMonoMonad = SchemaGeneratorConfig(
supportedPackages = listOf("myPackage"),
hooks = MonadHooks(),
dataFetcherFactoryProvider = CustomDataFetcherFactoryProvider())
toSchema(queries = listOf(TopLevelObject(ReactorQuery())), config = configWithReactorMonoMonad)
This will produce
type Query {
asynchronouslyDo: Int
}
You can find additional example on how to configure the hooks in our unit tests and example app.