この前職場でGraphQLの質問を受けたんですが、やったことがないので答えられませんでした。というわけで、これを機に理解しておこうかと思います。
GraphQLとは?
そもそもGraphQLとは一体全体どんなものなのでしょうか?公式サイトには以下の文言が書かれています。
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
要約するとGraphQLとはクエリ言語の一つで、実行時にデータを満たす特徴があるそうです。完全でわかりやすくAPIのデータを表明でき、クライアントの必要に応じたやりとり(どれが必要でどれがいらないとか)ができます。さらにAPIを進化させやすく、開発ツールのパワーを最大化できるとも書いてあります。
これだけ読んでも全くわからないので、とりあえずチュートリアルをやってみました。
チュートリアルをやってみる
JavaでできるチュートリアルがあったのでKotlinに置き換えてやってみました。
手順は以下のとおりです。
- Spring Boot アプリケーションの作成
- Schemaファイルの作成
- DataFetcherの作成
今回作成したものの全体はGitHubで公開しています。
Spring Boot アプリケーションの作成
GraphQLに関する依存関係を追加したSpringBootのプロジェクトを作成します。GraphQLに関する依存は以下の2つになります。
implementation("com.graphql-java:graphql-java:11.0") implementation("com.graphql-java:graphql-java-spring-boot-starter-webmvc:1.0")
全体の build.gradle.kts
はこんな感じ。
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.2.4.RELEASE" id("io.spring.dependency-management") version "1.0.9.RELEASE" kotlin("jvm") version "1.3.61" kotlin("plugin.spring") version "1.3.61" } group = "com.graphql-java.tutorial" version = "0.0.1-SNAPSHOT" java.sourceCompatibility = JavaVersion.VERSION_1_8 repositories { mavenCentral() } dependencies { implementation("com.graphql-java:graphql-java:11.0") implementation("com.graphql-java:graphql-java-spring-boot-starter-webmvc:1.0") implementation("com.google.guava:guava:26.0-jre") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } } tasks.withType<Test> { useJUnitPlatform() } tasks.withType<KotlinCompile> { kotlinOptions { freeCompilerArgs = listOf("-Xjsr305=strict") jvmTarget = "1.8" } }
Schemaファイルの作成
GraphQLではSchemaファイルと呼ばれるAPIスキーマを定義するファイルを用意するみたいです。GraphQLはクライアントやサーバーのやり取りをこのスキーマファイルに定義されている内容をもとに行うようです。なので柔軟なAPI呼び出しを行えるんですね。
今回は src/main/resources/schema.graphqls
に以下の内容を記述しました。
type Query { bookById(id: ID): Book } type Book { id: ID name: String pageCount: Int author: Author } type Author { id: ID firstName: String lastName: String }
DataFetcherの作成
最後にDataFetcherと呼ばれるものを作れば終わりですが、これが少し理解が難しいです。先に作成したものを載せます。
package com.graphqljava.tutorial.bookdetails import graphql.schema.DataFetcher import org.springframework.stereotype.Component @Component class GraphQLDataFetchers { private val books = listOf( mapOf("id" to "book-1", "name" to "Harry Potter and the Philosopher's Stone", "pageCount" to "223", "authorId" to "author-1"), mapOf("id" to "book-2", "name" to "Moby Dick", "pageCount" to "635", "authorId" to "author-3"), mapOf("id" to "book-3", "name" to "Interview with the vampire", "pageCount" to "371", "authorId" to "author-3") ) private val authors = listOf( mapOf("id" to "author-1", "firstName" to "Joanne", "lastName" to "Rowling"), mapOf("id" to "author-2", "firsName" to "Herman", "lastName" to "Melville"), mapOf("id" to "author-3", "firstName" to "Anne", "lastName" to "Rice") ) fun getBookByIdDataFetcher(): DataFetcher<Map<String, String>?> { return DataFetcher { dataFetchingEnvironment -> val bookId = dataFetchingEnvironment.getArgument<String>("id") books.stream().filter { it["id"] == bookId }.findFirst().orElse(null) } } fun getAuthorDataFetcher(): DataFetcher<Map<String, String>?> { return DataFetcher { dataFetchingEnvironment -> val book = dataFetchingEnvironment.getSource<Map<String, String>>() val authorId = book["authorId"] authors.stream().filter { it["id"] == authorId }.findFirst().orElse(null) } } }
DataFetcherとはその名の通り、データを取ってくる役割を持ったクラスです。上のコードだと、 getBookByIdDataFetcher
と getAuthorDataFetcher
がDataFetcherのインスタンスを返しています。GraphQLはこのDataFetcherを使ってリクエストに応じデータを取得します。
DataFetcherはスキーマファイルで定義した型、フィールドごと作れます。指定しない場合は PropertyDataFetcher
が使用されます。今回は Book と Author に対してDataFetcherを作りましたが、それらのフィールドに対しては作っていないので、フィールド値を取得するときには PropertyDataFetcher
が使用されます。
DataFetcherのインスタンスは DataFetcher
インターフェイスを実装する必要があります。インターフェイスは T get(DataFetchingEnvironment environment) throws Exception;
というメソッドが定義されていて、このメソッドの戻り値がクエリの応答(レスポンス)に該当します。以下のリンク先にそのインターフェイスが載っています。
DataFetcherが戻り値を返すには通常、クエリの内容を取得する必要が出てきます。これは引数の DataFetchingEnvironment
が情報を持っているので、問い合わせて使用します。上記の例だと以下の2パターンでクエリの内容を取得しています。
- getArgument ... クエリの引数を取得する。今回はBookIdを取得している。
- getSource ... 親フィールドの情報を取得する。今回はどのAuthorのデータを返すかを調べるために、親となるBookのフィールドを取得している。
あとはお作法に則った形で GraphQL
というインスタンスを作り、Spring の Bean に登録してあげればチュートリアルは完了です。
package com.graphqljava.tutorial.bookdetails import com.google.common.io.Resources import graphql.GraphQL import graphql.schema.GraphQLSchema import graphql.schema.idl.RuntimeWiring import graphql.schema.idl.SchemaGenerator import graphql.schema.idl.SchemaParser import graphql.schema.idl.TypeRuntimeWiring import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.stereotype.Component import javax.annotation.PostConstruct @Component class GraphQLProvider { private var graphQl: GraphQL? = null @Autowired private lateinit var graphQLDataFetchers: GraphQLDataFetchers @Bean fun graphQL(): GraphQL? { return graphQl } @PostConstruct @Suppress("UnstableApiUsage") fun init() { // schema.graphqls ファイルの読み込み val url = Resources.getResource("schema.graphqls") val sdl = Resources.toString(url, Charsets.UTF_8) // GraphQLSchema 及び GraphQL インスタンスの作成 val graphQLSchema = buildSchema(sdl) this.graphQl = GraphQL.newGraphQL(graphQLSchema).build() } /** * GraphQLSchema インスタンスの作成 */ fun buildSchema(sdl: String): GraphQLSchema { val typeRegistry = SchemaParser().parse(sdl) val runtimeWiring = buildWiring() return SchemaGenerator().makeExecutableSchema(typeRegistry, runtimeWiring) } private fun buildWiring(): RuntimeWiring { return RuntimeWiring.newRuntimeWiring() .type(TypeRuntimeWiring.newTypeWiring("Query") .dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher())) .type(TypeRuntimeWiring.newTypeWiring("Book") .dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher())) .build() } }
作成したDataFetcherは、上記のコードの buildWiring
メソッド内で使っています。
APIを叩いてみる
実際にAPIを叩いてみました。APIを叩く便利なツールを公式が用意しているのでそちらを使用します。
エンドポイントはデフォルトでは graphql
になっているので、 http://localhost:8080/graphql
をアクセス先に設定し、以下のリクエストを送信します。
{ bookById(id: "book-1") { id author { firstName lastName } } }
レスポンスは以下のようになりました。
{ "data": { "bookById": { "id": "book-1", "author": { "firstName": "Joanne", "lastName": "Rowling" } } } }
リクエストで指定したbookIdの内容が、指定したものだけ返却されているのがわかります。この例のように、GraphQLではクライアントがどんな情報がほしいのかを細かく指定することができるのが特徴です。
ここでふと思ったのですが、スキーマで定義されていないリクエストを送るとどうなるのか...?実際にやってみました。author
にage
を含め送信すると以下のようになりました。
{ bookById(id: "book-1") { id author { firstName lastName age } } }
{ "data": { "bookById": { "id": "book-1", "author": { "firstName": "Joanne", "lastName": "Rowling", "age": null } } } }
レスポンスを見ると分かる通りnull
になるみたいです。