はじめに
これは Ubie Advent Calendar 2019 - Qiita の7日目の記事です。
6日目は かみな/Kaito Minatoya@Ubie (@kamina_zzz) | Twitter による 地球上の Kubernetes ユーザーは絶対使うべきツールたちを紹介するよ - Qiita でした。
想定読者
- Spring + Kotlin + GraphQLを使っていて、Ktorに移行したくなっちゃった人 (需要🤔)
本題
UbieでAdvent CalendarをKtor縛りで書くぞ!となりました。じゃあ自分は何を書こうかなーって考えてたらGraphQLに決まってるじゃん?と振られたので、UbieではKtor+GraphQLは使ってないのですが、もしSpringのアプリケーションである https://github.com/ubie-inc/kotlin-graphql-sample をKtorに移し替えるとしたら何をするのかを調査しつつ、記事にすることにしました。
この記事では一旦動くところまでを目指します。
生かすコード、諦めるコード
フレームワークを載せ替えるとしてもそこまでガッツリ書き直す訳ではなく、生かすことが出来るコードが多々あります。今回は出来うる限りコードを生かす方向にしました。そのため、graphql-java-toolsを同様に採用します。
これを採用するため、domainとquery resolver, resolver はSpringのアノテーションを消すだけで再利用出来ます。
import com.coxautodev.graphql.tools.GraphQLResolver - import org.springframework.stereotype.Component - @Component class DiseaseResolver(...) : GraphQLResolver<Disease> {
jdbc も同様に修正するのですが、Springのjdbcを使っているため、思った以上にガッツリ修正が入ります。この辺りはKtorで利用するDBアクセスライブラリ次第になります。ここではあまり関係ないので、省略します。
スキーマファイルもSpringの時同様にresources直下に置きます。
再利用できるコードもありますが、完全に捨てるコードもあります。
例えば、JdbcConfigやKotlinGraphQLSampleApplication これらはSpringのための設定なので再利用は出来ません。ただこのようなコードだけなのでほとんどのコードが再利用することが可能となります。
新規実装
IntelliJのウィザードで作成されるここから、新規の実装をしていきます。
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { }
最初にRoutingの定義です。 GraphQLなので /graphql
にコードを書いていきます。GraphQLは GETとPOSTのサポートをしているので、一応、両方とものAPIを書きます。
fun Application.module(testing: Boolean = false) { routing { get("/graphql") { TODO() } post("/graphql") { TODO() } } }
最初にリクエストをパースします。リクエストはこんなJSONです。
{ "query":"{\n drug(id:\"id\") {\n name\n }\n}", "variables":null, "operationName":null }
これをこのままdata classにします。
data class GraphQLRequest( val query: String = "", val operationName: String? = "", val variables: Map<String, Any>? = mapOf() )
Ktorでデフォルトで使える、 ObjectMapper
を使ってパース処理を書くと。
fun Application.module(testing: Boolean = false) { routing { get("/graphql") { val stringRequest = call.receive<String>() val mapper = ObjectMapper() val request = mapper.readValue(stringRequest, GraphQLRequest::class.java) } post("/graphql") { val stringRequest = call.receive<String>() val mapper = ObjectMapper() val request = mapper.readValue(stringRequest, GraphQLRequest::class.java) } } }
何度も同じ処理を二箇所に書くのは辛いので、 GraphQLController
を作成します。
class GraphQLController { suspend fun handleGraphQL(call: ApplicationCall) { val stringRequest = call.receive<String>() val mapper = ObjectMapper() val request = mapper.readValue(stringRequest, GraphQLRequest::class.java) TODO() } }
これを使ってコード量減らします。
fun Application.module(testing: Boolean = false) { // Controller val graphQLController = GraphQLController() routing { get("/graphql") { graphQLController.handleGraphQL(call) } post("/graphql") { graphQLController.handleGraphQL(call) } } }
さて次は実際に graphql-java
を利用してコードです。
val filePath = "kotlingraphqlsample.graphqls" val graphQLSchema = SchemaParserBuilder() .file(filePath) .resolvers( listOf(diseaseResolver, DiseaseQueryResolver(diseaseService), // 他のResolverもここに記述する )) .build() .makeExecutableSchema() val graphQL = GraphQL.newGraphQL(graphQLSchema).build()
この graphQL
インスタンスをController内で使い、それぞれのResolverにアクセスしていきます。graphQL
インスタンスはDI使うなり、コンストラクタに差し込むなり、Controller内でインスタンス化するなり、好きなようにControllerに差し込みます。
suspend fun handleGraphQL(call: ApplicationCall) { val stringRequest = call.receive<String>() val mapper = ObjectMapper() val request = mapper.readValue(stringRequest, GraphQLRequest::class.java) val context = ConcurrentHashMap<String, Any>() // graphQLは上でインスタンス化したもの val result = graphQL.execute( ExecutionInput.newExecutionInput() .query(request.query) .operationName(request.operationName) .variables(request.variables ?: emptyMap()) .context(context) ) TODO() }
最後にresultをJSONにして、返します。
suspend fun handleGraphQL(call: ApplicationCall) { val stringRequest = call.receive<String>() val mapper = ObjectMapper() val request = mapper.readValue(stringRequest, GraphQLRequest::class.java) val context = ConcurrentHashMap<String, Any>() val result = graphQL.execute( ExecutionInput.newExecutionInput() .query(request.query) .operationName(request.operationName) .variables(request.variables ?: emptyMap()) .context(context) ) val json = mapper.writeValueAsString(result.toSpecification()) call.respondText { json } }
これでAPIは完成。APIアクセスを簡単にするために、GraphiQLも導入します。 graphiql/packages/examples/graphiql-cdn at master · graphql/graphiql · GitHub この辺りのサンプルコードをそのまま拝借し、index.htmlとしてプロジェクト直下に置きます。
fun Application.module(testing: Boolean = false) { // Controller val graphQLController = GraphQLController() routing { static("/graphiql") { // GraphiQL default("index.html") } get("/graphql") { graphQLController.handleGraphQL(call) } post("/graphql") { graphQLController.handleGraphQL(call) } } }
Runボタン押して、実際にGraphiQL http://localhost:8080/graphiql にアクセスしてみます。
見えるようになりました。yay!
終わりに
いかがでしたでしょうか?(一回やってみたかった)
やってみて思ったんですが、Springですでに動いているのであれば、今のところKtorに移す必要ないなーと言うのが感想です。
今回はこんなレポジトリのコードを参考にしました。
あまりアクティブな開発がされていないので、今後どうなるのかわからないですが、もうちょっとKtorのGraphQL周りのプラグインが出来たら良いなーと。
自分で作れってことか 🙃