LoginSignup
2
4

More than 3 years have passed since last update.

grahpql-java-tools (+ kotlin) を使ってGraphQLサーバーを実装してみる

Posted at

この記事は何?

Kotlin + graphql-java-tools を使って、GraphQLサーバーを実装してレスポンスを返すまでの工程をまとめます。

この記事で含むこと

- 簡単なGraphQLサーバーを構築

この記事で含まないこと

- N+1問題の対策
- ページングの実装

graphql-java-toolsについて

GraphQLサーバーとしての機能を提供してくれます。
graphql-spring-boot というツールも内部では、 graphql-java-toolsを使用しています。
GraphQLサーバー自体には、Webアプリケーションサーバーとしての機能は備わっていません。
任意のフレームワークないし、ライブラリを使う必要があります。
この記事では、Ktorを使っていきます。

実装の流れ

  1. graphqlファイルをにスキーマを定義する
  2. RootResolverを実装する
  3. Resolverを実装する
  4. GraphQLのリクエストを処理するためのHandlerを実装する

RootResolverとResolverの違いについて

graphql-java-toolsには2種類の Resolver の概念があります。(ResolverとはMVCでいうところのControllerのようなものです)

以下のようなGraphQLスキーマを実装することを考えます。
親の Parent が 子供の Child を持っているデータです。

example.graphql
type Query {
  parent: Parent!
}

type Parent {
  id: ID!
  name: String!
  child: Child!
}

type Child {
  id: ID!
  name: String!
}

このような Has A 関係を持ったデータを graphql-java-tools で返す場合には、 RootResolverResolver の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 そのオブジェクトを受け取ることができます。
したがって ParentResolverParent を受け取ることで 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つの RootResolverGraphQLQueryResolverGraphQLMutaionResolver の双方を継承することも可能です。

  1. 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つです。

  1. GraphQLResolver を継承する。
    Resolverが受け取りたいオブジェクトをジェネリクスに指定して、 GraphQLResolver を継承しましょう。(今回の例でいうと GraphqlResolver<Sample> を継承してください)

  2. 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のリクエストを受け付けられるようになりました :smile:

終わりに

今回はシンプルなデータ構造のGraphQLサーバーを実装しました。
実際のコードは需要があれば載せます。

少しでもあなたの参考になれば幸いです。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4