Closed13

GraphQL Kotlin + Ktorの実装調査

ヤマチューヤマチュー

Ktor Project Generator

プロジェクトの雛形を作成

https://start.ktor.io/

exampleを見ている感じ、デフォルトで大丈夫そう👀

ヤマチューヤマチュー

ライブラリの追加

Ktorの既存ライブラリに合わせて、GraphQL Kotlinを追加

gradle.properties
graphql_kotlin_version=7.0.0-alpha.0

※バージョンを6.3.2で試したが、GraphQLContextFactory周りが上手く動かなかったので変更

build.gradle.kts
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を作成する必要があり、これにはrequestParsercontextFactoryrequestHandlerが必要になる

KtorGraphQLServer
class KtorGraphQLServer(
    requestParser: GraphQLRequestParser<ApplicationRequest>,
    contextFactory: GraphQLContextFactory<ApplicationRequest>,
    requestHandler: GraphQLRequestHandler
) : GraphQLServer<ApplicationRequest>(requestParser, contextFactory, requestHandler)

requestParserGraphQLRequestParserがinterfaceなので実装したclassが必要

KtorGraphQLRequestParser
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.")
        }
    }
}

contextFactoryGraphQLContextFactoryがinterfaceなので実装したclassが必要

KtorGraphQLContextFactory
class KtorGraphQLContextFactory : GraphQLContextFactory<ApplicationRequest>

requestHandlerは単純にGraphQLRequestHandlerをimportするだけで済む

ヤマチューヤマチュー

スキーマを定義するためのKtorGraphQLSchemaを定義する
queriesやmutationsに本来はTopLevelObjectを渡さないといけないが、今回はいったん空で進める

KtorGraphQLSchema
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のみを定義すれば良さそうなので以下のような実装をする

KtorGraphQLServer
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)
    }
}
Routing.kt
fun Application.configureRouting() {

    routing {
        post("/graphql") {
            KtorGraphQLServer.handle(this.call)
        }
    }
}
ヤマチューヤマチュー

Queryの作成と登録

HelloQuery
object HelloQuery : Query {
    fun hello() = "Hello World!"
}
KtorGraphQLSchema
object KtorGraphQLSchema {
    // 省略
    private val queries = listOf(
        TopLevelObject(HelloQuery)
    )
}
ヤマチューヤマチュー

サーバーの起動

./gradlew run

あと地味にPostmanがGraphQLに対応していることを知り利用してみたが割と良かった

ヤマチューヤマチュー

SDLを外部に公開するAPIを実装する場合

外部向けのAPIなら実装するかな?

Routing.kt
fun Application.configureRouting() {

    routing {
        // 省略
        get("sdl") {
            call.respondText(graphQLSchema.print())
        }
    }
}
ヤマチューヤマチュー

Playgroundの実装

Playgroundのルーティングを設定する

Routing.kt
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を追加

https://github.com/ExpediaGroup/graphql-kotlin/blob/master/examples/server/ktor-server/src/main/resources/graphql-playground.html

ヤマチューヤマチュー

その他、分かったこと

Data Loadersは以下のように、dataLoaderRegistryFactoryを実装すれば簡単にいけそう

https://github.com/ExpediaGroup/graphql-kotlin/blob/186e0e2a83dc59556cd7a5e302f378bfdc6bdf32/examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/KtorGraphQLServer.kt#L38-L48

逆にSubscriptionsは公式に実装例がないので、graphql-javaで実装する必要がありそう…
気軽にやるのには少しハードルが高そう…

https://opensource.expediagroup.com/graphql-kotlin/docs/server/server-subscriptions

ヤマチューヤマチュー

まだまだ良くなりそうなところ

  1. queriesやmutationsの登録が自動でできないあたりがSpring版を試していると余計に残念に感じ
  2. Ktorのプラグインにできると良さそう

1.は多分、リフレクションを使うか、ディレクトリを決め打ちにして全ファイルを読み込むような形にすれば自動化自体はできそう
2.はKtor自体のCustom pluginsを作れば良さそう

https://ktor.io/docs/custom-plugins.html

ヤマチューヤマチュー

別件で思ったこと

意外とKtor系のコードはわざとclassを作っていないなぁと感じた
なんというか、JS/TSっぽさを感じるコードだった
個人的にはobject使うほうが安心感あるのだが、どっちが良いのか悪いのかが今のところ自分の中で結論、というか違いがあまり出ていないことが分かったので今後暇なときに考えてみたい

このスクラップは2022/12/11にクローズされました