この記事は何?
Kotlin + graphql-java-tools を使って、GraphQLサーバーを実装してレスポンスを返すまでの工程をまとめます。
この記事で含むこと
- 簡単なGraphQLサーバーを構築
この記事で含まないこと
- N+1問題の対策
- ページングの実装
graphql-java-toolsについて
GraphQLサーバーとしての機能を提供してくれます。
graphql-spring-boot というツールも内部では、 graphql-java-toolsを使用しています。
GraphQLサーバー自体には、Webアプリケーションサーバーとしての機能は備わっていません。
任意のフレームワークないし、ライブラリを使う必要があります。
この記事では、Ktorを使っていきます。
実装の流れ
- graphqlファイルをにスキーマを定義する
- RootResolverを実装する
- Resolverを実装する
- GraphQLのリクエストを処理するためのHandlerを実装する
RootResolverとResolverの違いについて
graphql-java-toolsには2種類の Resolver
の概念があります。(ResolverとはMVCでいうところのControllerのようなものです)
以下のようなGraphQLスキーマを実装することを考えます。
親の Parent
が 子供の Child
を持っているデータです。
type Query {
parent: Parent!
}
type Parent {
id: ID!
name: String!
child: Child!
}
type Child {
id: ID!
name: String!
}
このような Has A 関係を持ったデータを graphql-java-tools で返す場合には、 RootResolver
と Resolver
の2種類を実装する必要があります。
それぞれの役割は次のようになります。
RootResolver:
Parent が参照された時の処理を実装します。
エンドポイントへのリクエストがトリガーとなって呼び出されます。
Resolver:
Parent.Child が参照された時の処理を実装します。
登録した親クラスが呼び出されたタイミングがトリガーとなって呼び出されます。
わかりにくいので例を見て説明します。
exmple.graphql
で定義した型をKotlinのクラスで表現した時に、以下のようになったとします。
data class Parent(
val id: Int,
val name: String,
val childId: Int
)
data class Child(
val id: Int,
val name: String
)
実際のClassには Parent.child
は存在しません。
これをレスポンスとして Parent.child
を返すには、 Parent.childId
を使って、 Child
を紐づける必要があります。
そのために、必要なのが Resolver
です。
これは、指定したクラスがレスポンスとして返される時に、 Resolver
そのオブジェクトを受け取ることができます。
したがって ParentResolver
で Parent
を受け取ることで childId
を参照することができるので、 紐づいている Child
を取得することができます。
まとめると・・・
Queryで定義したエンドポイントが参照された時の処理 -> RootResolver
エンドポイントが返す type のプロパティが参照された時の処理 -> Resolver
1. スキーマを定義
実際にサーバーを実装する流れをみてみましょう。
まずは、GraphQLのスキーマを定義していきます。
この記事ではGraphQLの構文については解説していませんので、構文については別の記事を参照してください。
ここでは、簡単なQueryの処理を実装していきます。
任意のディレクトリに sample.graphql というファイルを作成してください。
今回作成するサーバーではこのファイルの定義を読み込ませます。
samplesというエンドポイントでSample型のデータをリストで返すように定義してみました。
graphql/sample.graphql
type Query {
samples: [Sample!]!
}
type Sample {
id: ID
name: String
user: User
}
type User {
id: ID
sampleId: ID
}
これに対応するデータクラスをKotlinでも定義します。
data class Sample(
val id: Int,
val name: String,
val userId: Int
)
data class User(
val id: Int,
val email: String
)
2. RootResolverを実装
RootResolverを実装していきます。
エンドポイント samples が呼び出された時の処理を実装します。
本来はUseCaseなどを呼び出しますと思いますが、今回は同じリストを返すように作ります。
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)
)
}
}
ポイントは2つです。
1. GraphQLQueryResolver
を継承する。
今回は Query
の操作を実装するので、 GraphQLQueryResolver
を継承しましょう。
Mutation
の操作を実装する場合には GraphQLMutaionResolver
を継承してください。
両方の処理を実装する場合には、1つの RootResolver
で GraphQLQueryResolver
と GraphQLMutaionResolver
の双方を継承することも可能です。
- GraphQLで定義したQueryと同じ名前のメソッドを用意する。
graphql/sample.graphql
で、samples
という Queryのエンドポイントを定義しているので、RootReposolverでもsamples
メソッドを定義する必要があります。 このとき、GraphQL側で引数を指定している場合には、RootReposolverのメソッドで一致する型の引数を受ける必要があります。
3. Resolverを実装
SampleResolverを実装していきます。
Sample.user のプロパティが呼び出された時の処理を実装します。
このSampleResolverは Sample
クラスがレスポンスとして返却されるタイミングがトリガーになります。
例によってUseCaseは呼び出さず、固定のListから id
が一致するオブジェクトを返します。
SampleResolver.kt
class SampleResolver: GraphQLResolver<Sample> {
private val users = listOf(
User(id = 1, email = "user1@hoge.com"),
User(id = 2, email = "user2@hoge.com"),
User(id = 3, email = "user3@hoge.com")
)
fun user(input: Sample): User? {
return users.find { it.id == input.userId }
}
}
ポイントは2つです。
GraphQLResolver
を継承する。
Resolverが受け取りたいオブジェクトをジェネリクスに指定して、GraphQLResolver
を継承しましょう。(今回の例でいうとGraphqlResolver<Sample>
を継承してください)graphqlで定義したプロパティと同じ名前のメソッドを用意する
GraphQLで定義した、Sample.user
参照された時の処理を実装するので、user
メソッドを実装します。
4. Handlerを実装
先ほど定義した sample.graphql ファイル と Resolver を Handler に登録します。
これによって、GraphQLサーバーでリクエストを処理することができます。
GraphqlHander.kt
class GraphQLHander {
/**
* GraphQLのスキーマをビルドします。
* filePathに定義されているスキーマとResolverを登録します。
**/
fun init(): GraphQL {
val filePath = "graphql/sample.graphql"
val schema = SchemaParser.newParser()
// GraphQLの定義を読み込む
.file(file)
// Resolverを読み込ませて、GraphQLのエンドポイントと紐づける
.resolvers(listOf(RootSampleResolver(), SampleResolver()))
.build()
.makeExecutableSchema()
}
/**
* GraphQLのリクエストを処理します。
**/
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())
)
}
}
定義したHandlerの execute
メソッドに、GraphQLのRequestパラメータを渡すことでGraphQLの処理を行うことができます。
Ktorを使って、 /graphql
というエンドポイントでGraphQLの処理を行うように実装してみます。
Routes.kt
// pathの指定
@Location("/graphql")
// リクエストで受け取るパラメータの設定
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(
// GraphQLの処理を実行
handler.execute(query, operationName, variables, context).toSpecification()
)
}
}
これで、サーバーを起動すると、 /graphql
のエンドポイントでGraphQLのリクエストを受け付けられるようになりました
終わりに
今回はシンプルなデータ構造のGraphQLサーバーを実装しました。
実際のコードは需要があれば載せます。
少しでもあなたの参考になれば幸いです。