Using Kotlin + graphql-java-tools, we will summarize the process of implementing GraphQL server and returning a response.
Including in this article --Build a simple GraphQL server
Not included in this article --Countermeasures for N + 1 problem --Paging implementation
It provides the function as a GraphQL server. A tool called graphql-spring-boot also uses graphql-java-tools internally. The GraphQL server itself does not function as a web application server. You need to use any framework or library. In this article, we will use Ktor.
graphql-java-tools has two types of Resolver
concepts. (Resolver is like Controller in MVC)
Consider implementing the following GraphQL schema.
The data in which the parent Parent
has the child Child
.
example.graphql
type Query {
parent: Parent!
}
type Parent {
id: ID!
name: String!
child: Child!
}
type Child {
id: ID!
name: String!
}
If you want to return data with Has A relation like this with graphql-java-tools, you need to implement two types, RootResolver
and Resolver
.
The roles of each are as follows.
RootResolver: Implement the processing when the Parent is referenced. Called with a request to the endpoint as a trigger.
Resolver: Implement the processing when Parent.Child is referenced. It is called when the registered parent class is called as a trigger.
It's hard to understand, so I'll explain it with an example.
When the type defined in ʻexmple.graphql` is expressed in Kotlin class, it becomes as follows.
data class Parent(
val id: Int,
val name: String,
val childId: Int
)
data class Child(
val id: Int,
val name: String
)
Parent.child
does not exist in the actual Class.
To return Parent.child
as a response, you need to use Parent.childId
to associate Child
.
To do that, you need Resolver
.
It can receive the object Resolver
when the specified class is returned as a response.
Therefore, you can refer to childId
by receiving Parent
with ParentResolver
, and you can get the associated Child
.
Summary··· Processing when the endpoint defined in Query is referenced-> RootResolver What to do when a property of type returned by the endpoint is referenced-> Resolver
Let's see the flow of actually implementing the server.
First, let's define the GraphQL schema. This article does not cover GraphQL syntax, so please refer to another article for syntax.
Here, we will implement a simple Query process. Create a file called sample.graphql in any directory. The server created this time will read the definition of this file.
I defined the endpoint called samples to return Sample type data as a list.
graphql/sample.graphql
type Query {
samples: [Sample!]!
}
type Sample {
id: ID
name: String
user: User
}
type User {
id: ID
sampleId: ID
}
The corresponding data class is also defined in Kotlin.
data class Sample(
val id: Int,
val name: String,
val userId: Int
)
data class User(
val id: Int,
val email: String
)
We will implement RootResolver. Implements the processing when the endpoint samples are called. Originally I think that UseCase etc. will be called, but this time I will make it so that the same list is returned.
RootSampleResolver.kt
class RootSampleResolver: GraphQLQueryResolver {
fun samples(): List<Sample> {
return listOf(
Sample(id = 1, name = "sample1", userId = 1),
Sample(id = 2, name = "sample2", userId = 2),
Sample(id = 3, name = "sample3", userId = 3)
)
}
}
There are two points.
Inherit GraphQLQueryResolver
.
This time we will implement the operation of Query
, so let's inheritGraphQLQueryResolver
.
Inherit GraphQLMutaionResolver
when implementing the operation of Mutation
.
When implementing both processes, one RootResolver
can inherit both GraphQLQueryResolver
and GraphQLMutaionResolver
.
Prepare a method with the same name as Query defined in GraphQL.
Since graphql / sample.graphql
defines a query endpoint called samples
, RootReposolver also needs to define a samples
method.
At this time, if the argument is specified on GraphQL side, it is necessary to receive the argument of the matching type by the method of RootReposolver.
We will implement SampleResolver. Implement the process when the property of Sample.user is called.
This SampleResolver is triggered when the Sample
class is returned as a response.
As usual, UseCase is not called and returns an object with a matching ʻid` from a fixed List.
SampleResolver.kt
class SampleResolver: GraphQLResolver<Sample> {
private val users = listOf(
User(id = 1, email = "[email protected]"),
User(id = 2, email = "[email protected]"),
User(id = 3, email = "[email protected]")
)
fun user(input: Sample): User? {
return users.find { it.id == input.userId }
}
}
There are two points.
Inherit GraphQLResolver
.
Let's inherit GraphQLResolver
by specifying the object that Resolver wants to receive in the generics. (In this example, inherit GraphqlResolver <Sample>
)
Prepare a method with the same name as the property defined in graphql
Since it implements the processing when Sample.user
is referenced as defined in GraphQL, implement the ʻuser` method.
Register the sample.graphql file and Resolver defined earlier in the Handler. This allows the GraphQL server to process your request.
GraphqlHander.kt
class GraphQLHander {
/**
*Build the GraphQL schema.
*Register the schema and Resolver defined in filePath.
**/
fun init(): GraphQL {
val filePath = "graphql/sample.graphql"
val schema = SchemaParser.newParser()
//Read GraphQL definition
.file(file)
//Load Resolver and associate it with GraphQL endpoint
.resolvers(listOf(RootSampleResolver(), SampleResolver()))
.build()
.makeExecutableSchema()
}
/**
*Handles GraphQL requests.
**/
fun execute(query: String, operationName: String, variables: Map<String, Any>, context: Any): ExecutionResult {
val graphql = init()
return graphql.execute(
ExecutionInput.newExecutionInput()
.query(query)
.operationName(operationName)
.variables(variables)
.context(context)
.dataLoaderRegistry(dataLoaderProvider.provideDataLoaderRegistry())
)
}
}
GraphQL processing can be performed by passing the GraphQL Request parameter to the ʻexecute` method of the defined Handler.
Let's implement using Ktor to perform GraphQL processing on the endpoint / graphql
.
Routes.kt
//specification of path
@Location("/graphql")
//Setting parameters received in request
data class GraphQLRequest(val query: String = "", val operationName: String = "", val variables: Map<String, Any> = mapOf())
fun Application.routes() {
val handler = GraphQLHandler()
post<GraphQLRequest> {
val request = call.receive<GraphQLRequest>()
val query = request.query
val operationName = request.operationName
val variables = request.variables
val context = ConcurrentHashMap<String, Any>()
call.respond(
//Execute GraphQL processing
handler.execute(query, operationName, variables, context).toSpecification()
)
}
}
Now when you start the server, the / graphql
endpoint can accept GraphQL requests: smile:
This time I implemented a GraphQL server with a simple data structure. The actual code will be posted if there is demand.
I hope it will be of some help to you.
Recommended Posts