はじめに
- OSSなどでWebAPIを提供する際に、GraphQLが採用されるケースがしばしば見受けられるようになってきた。
- REST後継とも目されるGraphQLを調査するとともにJava言語から利用する方法を調査する。
- 本ドキュメントの想定購読者は以下とする。
- Java言語で GraphQL Clientを構築したい。
- スターウォーズよりもポケモン(第1世代:カントー地方)が好き。
GraphQLとは
- Web APIのデファクトはREST(Restful API)であるが、その後継(の1つ)と見做されている GraphQL を紹介する。
- Facebook(現Meta)が開発したクエリ言語で、現在はGraphQL財団に移譲されている。
- リクエストに「必要なデータの構造」を定義でき、レスポンスは「定義されたデータ構造」が応答される。必要なデータのみ取得することで効率良く通信を実施できる。
- 特徴
- 型システム、クエリ言語、実行セマンティクス、静的な検証、型チェックから構成される。
- データの問い合わせ (query)、書き換え (mutation)、購読 (subscription) をサポートする。
- JSONに似たDSLで記述する(JSONとの完全な互換はなし)
- 参考
GraphQL リクエスト例 (ポケモンの図鑑ナンバーのみを取得)
query { pokemon(name: "Mew") { number } }
GraphQL レスポンス例 (ミュウのの図鑑ナンバーのみを応答)
{ "data": { "pokemon": { "number": "151" }}}
GraphiQL / GraphQL Playground
- GraphQLのAPIを簡単に実行・挙動確認するためのツールがオープンソースで開発されている。有名なものは以下となっている。
- いずれもブラウザ上で利用できる統合開発環境で、シンタックスハイライト、入力補完などの機能が提供されている。
GraphQL Pokémonを利用したクエリ例
- GraphQLでよく利用されているサンプルとして、以下を確認した。
- 本ドキュメントでは、一番楽しそうな「ポケモン(第1世代)」をGraphQLで検索できる
GraphQL Pokémon
を利用して学習を行う(graphql-pokemon
の方が色々と属性情報が多そうだが、習熟が目的なのでより簡易的なものを利用する方針とした)。 -
GraphQL Pokémon
では、上記のGraphiQLも提供されており、簡易にクエリを実行・動作確認ができる。
クエリ定義
- 実行可能なクエリとして以下が確認できる(スキーマ定義)。
-
pokemons
: 指定した「数」の「ポケモン一覧」を図鑑順で取得するクエリ。 -
pokemon
: 「ポケモンID」および「名前」を指定して特定の「ポケモン」を取得するクエリ。
-
pokemons(first: Int!): [Pokemon]
pokemon(id: String, name: String): Pokemon
型定義
- クエリ応答結果となる
Pokemon
型が下記構成であることが確認できる(スキーマ定義)。
実行例 pokemons
クエリ
- 引数
first
に4
および属性情報としてname
を指定し、pokemons
クエリを実行した。
query {
pokemons(first: 4) {
name
}
}
実行結果
- 図鑑順にポケモン(
Pokemons
型)一覧が応答されていることが確認できる。 - ポケモンの属性情報として、クエリで指定した
name
のみが返されていることも確認できる。- 図鑑No.001: フシギダネ(Bulbasaur)
- 図鑑No.002: フシギソウ(Ivysaur)
- 図鑑No.003: フシギバナ(Venusaur)
- 図鑑No.004: ヒトカゲ(Charmander)
{ "data": {
"pokemons": [
{ "name": "Bulbasaur" },
{ "name": "Ivysaur" },
{ "name": "Venusaur" },
{ "name": "Charmander"}
]
}}
実行例 pokemon
クエリ
- 引数
name
にPikachu
、属性情報にnumber
,name
,attacks
を指定し、pokemon
クエリを実行した。
query {
pokemon(name: "Pikachu") {
number
name
attacks {
special {
name
}
}
}
}
実行結果
- ポケモンの属性情報として、クエリで指定した
number
,name
,attacks
のみが返されていることが確認できる。
{ "data": {
"pokemon": {
"number": "025",
"name": "Pikachu",
"attacks": {
"special": [
{ "name": "Discharge" },
{ "name": "Thunder" },
{ "name": "Thunderbolt" }
]
}}}}
JavaからGraphQLを利用する方法
- GraphQLはhttpプロトコルで実現されているため、究極的にはHTTPクライアントさえあればGraphQLは利用はできる。
- ただし、利用するシステムに対応した静的型付けされたクラスがないと汎用性・可読性が低くなり利用がしづらい。
- そのため、スキーマ定義からJavaクラスを生成・利用する方法を調査する。
GraphQLにおけるスキーマ定義
- GraphQLではスキーマを定義する方法として以下の2種類が存在している(内容はほぼ同一だが、構造に差分あり)
-
SDL
- QraphQLのスキーマ定義ファイル。
- OSSなどであれば公開されていることが多い。
- 拡張子は
.graphql
-
Introspection Result
- GraphQLではサポートしているオペレーションを確認する仕様(Introspection)が規定されているため、必ず公開されている。
-
Introspection queryの応答としてスキーマ定義情報(Introspection result)を取得できる。
(参考: martinheld氏が公開しているGraphQL introspection query via curlが完璧) - 拡張子は
.json
-
SDL
スキーマ定義からJavaクラスの生成
- 調査した結果、スキーマ定義からJavaクラスを生成できるツールが色々存在していることが確認できた。
本資料では、graphql-java-codegen
を利用する方針とする。- SDLからJavaクラスを生成できるツール
- Introspection resultからJavaクラスを生成できるツール
graphql-java-codegenを利用したJavaファイル生成
-
graphqlSchemas
で SDLファイルを指定する(ここでは、GraphQL PokémonのSDLを指定) -
graphql-java-codegen
のオプションはこちらを参照。
pom.xml
<build><plugins><plugin>
<groupId>io.github.kobylynskyi</groupId>
<artifactId>graphql-codegen-maven-plugin</artifactId>
<version>${graphql-java-codegen.version}</version>
<executions>
<execution>
<id>generate-sources-product-client</id>
<goals><goal>generate</goal></goals>
<configuration>
<graphqlSchemas>
<includePattern>schema.graphqls</includePattern>
</graphqlSchemas>
<outputDir>${project.build.directory}/generated-sources/client</outputDir>
<modelPackageName>com.github.nomunomu5678.pokemon.model</modelPackageName>
<generateClient>true</generateClient>
</configuration>
</execution>
</executions>
</plugin></plugins></build>
- SDLファイルはリソースディレクトリに配置する。
.
├── src
│ └── main
│ └── resources
│ └── schema.graphqls
└── pom.xml
-
mvn compile
でJavaファイルが生成される。-
*Request.java
: GraphQLのリクエスト(パラメータ) -
*ResponseProjection.java
: GraphQLのリクエスト(取得対象の項目定義) -
*Response.java
: GraphQLのレスポンス - その他: GraphQLのデータモデル
-
$ mvn compile
$ tree ./target/generated-sources/client
./target/generated-sources/client
├── Attack.java
├── AttackResponseProjection.java
├── Pokemon.java
├── PokemonAttack.java
├── PokemonAttackResponseProjection.java
├── PokemonDimension.java
├── PokemonDimensionResponseProjection.java
├── PokemonEvolutionRequirement.java
├── PokemonEvolutionRequirementResponseProjection.java
├── PokemonQueryRequest.java
├── PokemonQueryResponse.java
├── PokemonResponseProjection.java
├── PokemonsQueryRequest.java
├── PokemonsQueryResponse.java
├── QueryQueryRequest.java
└── QueryQueryResponse.java
GraphQLリクエストの生成/シリアライズ
-
GraphQLRequest
のコンストラクタに、*Request
と*ResponseProjection
を指定する。-
*Request.java
: GraphQLのリクエスト(パラメータ) -
*ResponseProjection.java
: GraphQLのリクエスト(取得対象の項目定義)
-
public static void main(String[] args) {
PokemonQueryRequest req = PokemonQueryRequest.builder()
.setName("Pikachu")
.build();
PokemonResponseProjection resp = new PokemonResponseProjection()
.number()
.name();
GraphQLRequest graphqlReq = new GraphQLRequest(req, resp);
System.out.println(graphqlReq.toHttpJsonBody());
}
応答結果
{"query":"query pokemon { pokemon: pokemon(name: \"Pikachu\"){ number name } }"}
GraphQLレスポンス/デシリアライズ
- JSONをオブジェクトマッパーを利用してデシリアライズする。
public static void main(String[] args) throws Exception {
String graphqlRes = "{\"data\": {\"pokemon\": {\"id\": \"UG9rZW1vbjowMjU=\"}}}";
ObjectMapper objectMapper = new ObjectMapper();
PokemonQueryResponse res = objectMapper.readValue(graphqlRes, PokemonQueryResponse.class);
System.out.println(res.getData());
}
応答結果
{pokemon={ id: "UG9rZW1vbjowMjU=" }}
生成したコードサンプル
まとめ
- GraphQLをJavaから利用する方法の調査を行った。
- 実現方式として、GraphQLのスキーマ定義から生成したJavaクラスを利用する方式を調査した。