GraphQLをJavaから利用してみる


## はじめに
- OSSなどでWebAPIを提供する際に、GraphQLが採用されるケースがしばしば見受けられるようになってきた。
- REST後継とも目されるGraphQLを調査するとともにJava言語から利用する方法を調査する。
- 本ドキュメントの想定購読者は以下とする。
  - Java言語で GraphQL Clientを構築したい。
  - スターウォーズよりもポケモン（第1世代：カントー地方）が好き。

## GraphQLとは
- Web APIのデファクトはREST(Restful API)であるが、その後継（の１つ）と見做されている GraphQL を紹介する。
  - Facebook(現Meta)が開発したクエリ言語で、現在はGraphQL財団に移譲されている。
  - リクエストに「必要なデータの構造」を定義でき、レスポンスは「定義されたデータ構造」が応答される。必要なデータのみ取得することで効率良く通信を実施できる。
  - 特徴
    - 型システム、クエリ言語、実行セマンティクス、静的な検証、型チェックから構成される。
    - データの問い合わせ (query)、書き換え (mutation)、購読 (subscription) をサポートする。
    - JSONに似たDSLで記述する（JSONとの完全な互換はなし）
- 参考
  - https://www.howtographql.com/basics/1-graphql-is-the-better-rest/
  - https://techtarget.itmedia.co.jp/tt/news/2002/10/news04.html

```graphql: GraphQL リクエスト例 (ポケモンの図鑑ナンバーのみを取得)
query { pokemon(name: "Mew") { number } }
```

```graphql: GraphQL レスポンス例 (ミュウのの図鑑ナンバーのみを応答)
{ "data": { "pokemon": { "number": "151" }}}
```

### GraphiQL / GraphQL Playground
- GraphQLのAPIを簡単に実行・挙動確認するためのツールがオープンソースで開発されている。有名なものは以下となっている。
  - [GraphiQL](https://github.com/graphql/graphiql) 
  - [GraphQL Playground](https://github.com/graphql/graphql-playground)
- いずれもブラウザ上で利用できる統合開発環境で、シンタックスハイライト、入力補完などの機能が提供されている。

## GraphQL Pokémonを利用したクエリ例
- GraphQLでよく利用されているサンプルとして、以下を確認した。
  - [The Star Wars API](https://github.com/graphql/swapi-graphql)
  - [GitHub GraphQL API](https://docs.github.com/ja/graphql)
  - [GraphQL Pokémon](https://github.com/lucasbento/graphql-pokemon)
  - [graphql-pokemon](https://graphql-pokemon.js.org/)
- 本ドキュメントでは、一番楽しそうな「ポケモン（第1世代）」をGraphQLで検索できる `GraphQL Pokémon` を利用して学習を行う(`graphql-pokemon`の方が色々と属性情報が多そうだが、習熟が目的なのでより簡易的なものを利用する方針とした)。
- `GraphQL Pokémon`では、上記の[GraphiQL](https://graphql-pokemon2.vercel.app/)も提供されており、簡易にクエリを実行・動作確認ができる。

### クエリ定義
- 実行可能なクエリとして以下が確認できる([スキーマ定義](https://github.com/lucasbento/graphql-pokemon/blob/master/schemas/schema.graphql#L87-L92))。
  - `pokemons`: 指定した「数」の「ポケモン一覧」を図鑑順で取得するクエリ。
  - `pokemon`: 「ポケモンID」および「名前」を指定して特定の「ポケモン」を取得するクエリ。
```
  pokemons(first: Int!): [Pokemon]
  pokemon(id: String, name: String): Pokemon
```

### 型定義
- クエリ応答結果となる`Pokemon`型が下記構成であることが確認できる([スキーマ定義](https://github.com/lucasbento/graphql-pokemon/blob/master/schemas/schema.graphql#L1-L85))。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306047/26143857-9bc7-53f7-435c-150b0e2eb3af.png)

### 実行例 pokemons
クエリ
- 引数`first`に `4` および属性情報として`name`を指定し、`pokemons`クエリを実行した。
```graphql
query {
  pokemons(first: 4) {
    name
  }
}
```
実行結果
- 図鑑順にポケモン(`Pokemons`型)一覧が応答されていることが確認できる。
- ポケモンの属性情報として、クエリで指定した`name`のみが返されていることも確認できる。
  - 図鑑No.001： フシギダネ(Bulbasaur)
  - 図鑑No.002： フシギソウ(Ivysaur)
  - 図鑑No.003： フシギバナ(Venusaur)
  - 図鑑No.004： ヒトカゲ(Charmander)
```json
{ "data": {
    "pokemons": [
      { "name": "Bulbasaur" },
      { "name": "Ivysaur" },
      { "name": "Venusaur" },
      { "name": "Charmander"}
    ]
}}
```

### 実行例 pokemon
クエリ
- 引数`name`に `Pikachu`、属性情報に`number`,`name`,`attacks`を指定し、`pokemon`クエリを実行した。
```graphql
query {
  pokemon(name: "Pikachu") {
    number
    name
    attacks {
      special {
        name
      }
    }
  }
}
```
実行結果
- ポケモンの属性情報として、クエリで指定した`number`,`name`,`attacks`のみが返されていることが確認できる。
  - Discharge ([ほうでん](https://wiki.xn--rckteqa2e.com/wiki/%E3%81%BB%E3%81%86%E3%81%A7%E3%82%93))
  - Thunder ([かみなり](https://wiki.xn--rckteqa2e.com/wiki/%E3%81%8B%E3%81%BF%E3%81%AA%E3%82%8A))
  - Thunderbolt ([10まんボルト](https://wiki.xn--rckteqa2e.com/wiki/10%E3%81%BE%E3%82%93%E3%83%9C%E3%83%AB%E3%83%88))
```json
{ "data": {
    "pokemon": {
      "number": "025",
      "name": "Pikachu",
      "attacks": {
        "special": [
          { "name": "Discharge" },
          { "name": "Thunder" },
          { "name": "Thunderbolt" }
        ]
}}}}
```


## JavaからGraphQLを利用する方法
- GraphQLはhttpプロトコルで実現されているため、究極的にはHTTPクライアントさえあればGraphQLは利用はできる。
- ただし、利用するシステムに対応した静的型付けされたクラスがないと汎用性・可読性が低くなり利用がしづらい。
- そのため、スキーマ定義からJavaクラスを生成・利用する方法を調査する。

### GraphQLにおけるスキーマ定義
- GraphQLではスキーマを定義する方法として以下の２種類が存在している（内容はほぼ同一だが、構造に差分あり）
  - [SDL](https://graphql.org/learn/schema/)
      - QraphQLのスキーマ定義ファイル。
      - OSSなどであれば公開されていることが多い。
      - 拡張子は `.graphql`
  - [Introspection Result](https://graphql.org/learn/introspection/)
      - GraphQLではサポートしているオペレーションを確認する仕様(Introspection)が規定されているため、必ず公開されている。
      - [Introspection query](http://spec.graphql.org/draft/#sec-Schema-Introspection)の応答としてスキーマ定義情報(Introspection result)を取得できる。
(参考：　martinheld氏が公開している[GraphQL introspection query via curl](https://gist.github.com/martinheld/9fe32b7e2c8fd932599d36e921a2a825)が完璧)
      - 拡張子は `.json`

### スキーマ定義からJavaクラスの生成
- 調査した結果、スキーマ定義からJavaクラスを生成できるツールが色々存在していることが確認できた。
本資料では、 `graphql-java-codegen` を利用する方針とする。
    - SDLからJavaクラスを生成できるツール
        - [graphql-java-codegen](https://github.com/kobylynskyi/graphql-java-codegen)
        - [GraphQL Java](https://github.com/graphql-java/graphql-java)
    - Introspection resultからJavaクラスを生成できるツール
        - [graphql_java_gen](https://github.com/Shopify/graphql_java_gen)
        - [graphql-java-generator](https://github.com/adobe/graphql-java-generator)


### graphql-java-codegenを利用したJavaファイル生成

- `graphqlSchemas`で SDLファイルを指定する(ここでは、[GraphQL Pokémon](https://github.com/lucasbento/graphql-pokemon/blob/master/schemas/schema.graphql)のSDLを指定)
- `graphql-java-codegen`のオプションは[こちら](https://github.com/kobylynskyi/graphql-java-codegen/blob/main/docs/codegen-options.md#option-graphqlschemas)を参照。

```xml: 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のデータモデル
```shell
$ 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のリクエスト(取得対象の項目定義）

```java
  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());
  }
```

```shell:応答結果
{"query":"query pokemon { pokemon: pokemon(name: \"Pikachu\"){ number name } }"}
```

#### GraphQLレスポンス／デシリアライズ
- JSONをオブジェクトマッパーを利用してデシリアライズする。
```java
  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());
  }
```

```shell:応答結果
{pokemon={ id: "UG9rZW1vbjowMjU=" }}
```

#### 生成したコードサンプル
- https://github.com/nomunomu5678/graphql_pokemon_java/tree/main

## まとめ
- GraphQLをJavaから利用する方法の調査を行った。
- 実現方式として、GraphQLのスキーマ定義から生成したJavaクラスを利用する方式を調査した。