GraphQL Kotlin + Ktorの実装調査
調査の背景
GraphQL KotlinをKtorで使用できることは公式のexampleに実装があるので知っていたが、実際にどういう実装が必要なのかを自分で実装しながら確かめてみる
Ktor Project Generator
プロジェクトの雛形を作成
exampleを見ている感じ、デフォルトで大丈夫そう👀
ライブラリの追加
Ktorの既存ライブラリに合わせて、GraphQL Kotlinを追加
graphql_kotlin_version=7.0.0-alpha.0
※バージョンを6.3.2
で試したが、GraphQLContextFactory周りが上手く動かなかったので変更
val graphql_kotlin_version: String by project
dependencies {
implementation("com.expediagroup:graphql-kotlin-spring-server:$graphql_kotlin_version")
}
Ktor用のGraphQLServerの実装
GprahQL KotlinをKtorに実装するには、GraphQLServer<ApplicationRequest>
を継承したclassを作成する必要があり、これにはrequestParser
、contextFactory
、requestHandler
が必要になる
class KtorGraphQLServer(
requestParser: GraphQLRequestParser<ApplicationRequest>,
contextFactory: GraphQLContextFactory<ApplicationRequest>,
requestHandler: GraphQLRequestHandler
) : GraphQLServer<ApplicationRequest>(requestParser, contextFactory, requestHandler)
requestParser
はGraphQLRequestParser
がinterfaceなので実装したclassが必要
class KtorGraphQLRequestParser(
private val mapper: ObjectMapper
): GraphQLRequestParser<ApplicationRequest> {
override suspend fun parseRequest(request: ApplicationRequest): GraphQLServerRequest? {
try {
val rawRequest = request.call.receiveText()
return mapper.readValue(rawRequest, GraphQLServerRequest::class.java)
} catch (e: IOException) {
throw IOException("Unable to parse GraphQL payload.")
}
}
}
contextFactory
はGraphQLContextFactory
がinterfaceなので実装したclassが必要
class KtorGraphQLContextFactory : GraphQLContextFactory<ApplicationRequest>
requestHandler
は単純にGraphQLRequestHandler
をimportするだけで済む
スキーマを定義するためのKtorGraphQLSchema
を定義する
queriesやmutationsに本来はTopLevelObject
を渡さないといけないが、今回はいったん空で進める
object KtorGraphQLSchema {
private val config = SchemaGeneratorConfig(
supportedPackages = listOf("com.yamachoo.graphql")
)
private val queries = listOf<TopLevelObject>()
private val mutations = listOf<TopLevelObject>()
private val graphQLSchema = toSchema(
config = config,
queries = queries,
mutations = mutations
)
fun getGraphQLObject(): GraphQL = GraphQL.newGraphQL(graphQLSchema)
.valueUnboxer(IDValueUnboxer())
.build()
}
また公式のサンプルでは直接ファイルに定義されていたが個人的に違和感があったので、objectで包んだ
GraphQLServer<ApplicationRequest>
を継承したclassは実際には要らなさそうなことが分かり、代わりにKtorGraphQLServer
にはルーティングで使用するhandle
のみを定義すれば良さそうなので以下のような実装をする
object KtorGraphQLServer {
private val mapper = jacksonObjectMapper()
private val ktorGraphQLServer = getGraphQLServer()
suspend fun handle(applicationCall: ApplicationCall) {
val result = ktorGraphQLServer.execute(applicationCall.request)
if (result != null) {
val json = mapper.writeValueAsString(result)
applicationCall.response.call.respond(json)
} else {
applicationCall.response.call.respond(HttpStatusCode.BadRequest, "Invalid request")
}
}
private fun getGraphQLServer(): GraphQLServer<ApplicationRequest> {
val requestParser = KtorGraphQLRequestParser(mapper)
val contextFactory = KtorGraphQLContextFactory()
val graphQL = KtorGraphQLSchema.getGraphQLObject()
val requestHandler = GraphQLRequestHandler(graphQL)
return GraphQLServer(requestParser, contextFactory, requestHandler)
}
}
fun Application.configureRouting() {
routing {
post("/graphql") {
KtorGraphQLServer.handle(this.call)
}
}
}
Queryの作成と登録
object HelloQuery : Query {
fun hello() = "Hello World!"
}
object KtorGraphQLSchema {
// 省略
private val queries = listOf(
TopLevelObject(HelloQuery)
)
}
サーバーの起動
./gradlew run
あと地味にPostmanがGraphQLに対応していることを知り利用してみたが割と良かった
SDLを外部に公開するAPIを実装する場合
外部向けのAPIなら実装するかな?
fun Application.configureRouting() {
routing {
// 省略
get("sdl") {
call.respondText(graphQLSchema.print())
}
}
}
Playgroundの実装
Playgroundのルーティングを設定する
fun Application.configureRouting() {
routing {
// 省略
get("/playground") {
this.call.respondText(buildPlaygroundHtml("graphql", "subscriptions"), ContentType.Text.Html)
}
}
}
private fun buildPlaygroundHtml(graphQLEndpoint: String, subscriptionsEndpoint: String) =
Application::class.java.classLoader.getResource("graphql-playground.html")?.readText()
?.replace("\${graphQLEndpoint}", graphQLEndpoint)
?.replace("\${subscriptionsEndpoint}", subscriptionsEndpoint)
?: throw IllegalStateException("graphql-playground.html cannot be found in the classpath")
resources
配下に以下のHTMLを追加
その他、分かったこと
Data Loadersは以下のように、dataLoaderRegistryFactoryを実装すれば簡単にいけそう
逆にSubscriptionsは公式に実装例がないので、graphql-java
で実装する必要がありそう…
気軽にやるのには少しハードルが高そう…
まだまだ良くなりそうなところ
- queriesやmutationsの登録が自動でできないあたりがSpring版を試していると余計に残念に感じ
- Ktorのプラグインにできると良さそう
1.は多分、リフレクションを使うか、ディレクトリを決め打ちにして全ファイルを読み込むような形にすれば自動化自体はできそう
2.はKtor自体のCustom pluginsを作れば良さそう
別件で思ったこと
意外とKtor系のコードはわざとclassを作っていないなぁと感じた
なんというか、JS/TSっぽさを感じるコードだった
個人的にはobject使うほうが安心感あるのだが、どっちが良いのか悪いのかが今のところ自分の中で結論、というか違いがあまり出ていないことが分かったので今後暇なときに考えてみたい