Spring BootでGraphQLを試した

# はじめに
遅ればせながらGraphQLなるものを知ったので、自分が使い慣れているJava+Spring Bootでの実装を試した。
GraphQL自体の説明は[こちら](https://qiita.com/bananaumai/items/3eb77a67102f53e8a1ad)などに任せるとして以下では実装について記述する。

# プロジェクト準備
### 開発環境
* Windows 10
* Java 11 （AdoptOpenJDK jdk11.0.4+11 OpenJ9 0.15.1）
* Maven 3.5.4
* STS 4.4.0

### ベースとなるSpring Bootプロジェクトを作成する
* Spring Boot 2.1.9

|依存関係||備考|
|---|---|---|
|Spring Web|必須|サーブレットとして起動するため|
|Lombok|推奨|エンティティクラスを作成する上で便利なので|
|JDBC API|選択|DBアクセスの方法はお好みで|
|H2 Database|〃|〃|
|MyBatis Framework|〃|〃|

### GraphQL Javaの依存関係を追加する
graphql-java-kickstart/graphql-spring-bootを利用する。実行するための最小限の依存関係は以下のみでOK。

```xml:pom.xml
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>5.10.0</version>
</dependency>
```

プロパティとしてkotlinのバージョン指定も必要（記述がないと実行時に例外が発生する）。

```xml:pom.xml
<properties>
 	<java.version>11</java.version>
	<kotlin.version>1.3.10</kotlin.version>
</properties>
```

# APIその1:簡単な取得クエリに対応
以下のようなモデルに対してIDにより1つのエンティティを取得することを考える。

> メンバー（**メンバーID**, 氏名, 年齢）

### GraphQL定義作成
以下のように型定義とクエリ定義を記述したファイルを`src/main/resources/graphql`配下に作成する。

```graphql:schema.graphqls
type Member {
	memberId: ID!
	name: String!
	age: Int
}

type Query {
	getMember(memberId:ID!): Member
}
```

### Java側の実装
##### エンティティクラス
まず型定義に合ったエンティティクラスを作成する。

```java:Member
@Data
public class Member {
	private String memberId;
	private String name;
	private Integer age;
}
```

##### クエリ
次にクエリに対応するメソッドを作成する。`GraphQLQueryResolver`実装のクラスを作成し、クエリ名と同じ名前のメソッドを作成する。引数および戻り値も対応するクラスを定義し、実際に戻すインスタンスを実装する。戻り値の作成方法は任意（MyBatisのMapperを利用（割愛））。**戻り値のフィールドにはサーブレットとして返す可能性のある値をすべて詰めておく**。

```java:MemberQueryResolver
@Component
public class MemberQueryResolver implements GraphQLQueryResolver {

	@Autowired
	MemberMapper mapper;

	public Member getMember(String memberId) {
		return mapper.getById(memberId);
	}
}
```

### 実行
##### 起動構成
サーブレットの起動構成をapplication.ymlに記述する。とりあえず以下の設定があれば起動する。[詳細はこちらを参照](https://github.com/graphql-java-kickstart/graphql-spring-boot#enable-graphql-servlet)。

```yml:application.yml
graphql:
  servlet:
    enabled: true
    mapping: /graphql
    corsEnabled: true
```

##### 起動
記述したら作成したプロジェクトをSpring Boot Appとして実行する。

##### クエリ発行
`http://localhost:8080/graphql`に対してURLパラメータ`query`の値としてGraphQLのクエリを設定してアクセスすると結果が得られる。

たとえば以下のクエリを発行する場合、

```graphql
query {
  getMember(memberId:"aaaaaaaa") {
    name
  }
}
```

不要な空白と改行を除いて`encodeURI`すると`query%7BgetMember(memberId:%22aaaaaaaa%22)%7Bname%7D%7D`なのでcurlで確認する。

```bash
$ curl -s "http://localhost:8080/graphql?query=query%7BgetMember(memberId:%22aaaaaaaa%22)%7Bname%7D%7D"
{"data":{"getMember":{"name":"齊藤京子"}}}
```

memberIdを変更して、取得キーにageを加えた場合は以下のような結果が得られる。

```bash
$ curl -s "http://localhost:8080/graphql?query=query%7BgetMember(memberId:%22bbbbbbbb%22)%7Bname,age%7D%7D"
{"data":{"getMember":{"name":"佐々木久美","age":23}}}
```

# Graph<em>i</em>QLを導入する
クエリを逐一`encodeURI`してcurlやブラウザでアクセスするのは面倒すぎるので、クエリを試すツールを導入する。今回はGraph<em>i</em>QLを利用する。

### 依存関係の追加
graph「i」ql-spring-boot-starterを依存関係に追加する。

```xml:pom.xml
<dependency>
	<groupId>com.graphql-java-kickstart</groupId>
	<artifactId>graphiql-spring-boot-starter</artifactId>
	<version>5.10.0</version>
</dependency>
```

### 実行
##### 起動構成
サーブレットの起動構成をapplication.ymlに記述する。とりあえず以下の設定があれば起動する。[詳細はこちらを参照](https://github.com/graphql-java-kickstart/graphql-spring-boot#enable-graphiql)。上で記述したgraphqlの設定は不要。

```yml:application.yml
graphiql:
  enabled: true
  mapping: /graphiql
  endpoint:
    graphql: /graphql
```

##### 起動
記述したら作成したプロジェクトをSpring Boot Appとして実行する。
サーバが起動したらブラウザから`http://localhost:8080/graphiql`にアクセスする。

![GraphiQL.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266076/2d28f34c-5ea2-e3a9-4518-a15c4e7597c1.png)

左側のクエリ作成画面ではフィールド名の入力補助があり、実行結果のjsonも整形済みで表示される。

# APIその2:型を入れ子にしてみる
その1でのモデルに加えて以下のような多対多のモデルをリレーションによってつないでいるモデルに対してIDにより1つのエンティティを取得することを考える。

> メンバー（**メンバーID**, 氏名, 年齢）
> 楽曲（**楽曲ID**, 曲名）
> 参加楽曲リレーション（***楽曲ID***, ***メンバーID***）

### GraphQL定義修正
定義ファイルに追記する。多対多なので`Member`に対する`Music`も`Music`に対する`Member`も配列になる。

```graphql:schema.graphqls
type Member {
	memberId: ID!
	name: String!
	age: Int
	joining: [Music]
}

type Music {
	musicId: ID!
	title: String!
	members: [Member]
}

type Query {
	getMember(memberId:ID!): Member
	getMusic(musicId:ID!):Music
}
```

### Java側の実装
##### エンティティクラス
新しい型定義に対応するエンティティクラスを作成する。配列として定義されているプロパティは`List`にしておく。

```java:Music
@Data
public class Music {
	private String musicId;
	private String title;
	private List<Member> members;
}
```

`Member`もプロパティが追加されたのでフィールドを追加する。

```java:Member
@Data
public class Member {
	private String memberId;
	private String name;
	private int age;
	private List<Music> joining;
}
```

##### クエリ
`MemberQueryResolver`は**変更無し**。`Member.joining`に値を挿入するロジックは別クラスに作成する。
DataClass（`<>`の中）には挿入先のエンティティクラスを指定した`GraphQLResolver`実装のクラスを作成し、挿入先フィールドと同名のメソッドを作成する。引数には挿入先のエンティティクラスを指定し、挿入するインスタンスをreturnする。
（MusicMapper.getMusicsOfMemberはmemberIdにリレーション経由で紐づくするMusicをすべて取得するメソッド（実装内容は割愛）。）

```java:MemberResolver
@Component
public class MemberResolver implements GraphQLResolver<Member> {

	@Autowired
	MusicMapper mapper;

	public List<Music> joining(Member member) {
		return mapper.getMusicsOfMember(member.getMemberId());
	}
}
```

### 実行
以下のクエリを発行すると

```graphql
query {
  getMember(memberId:"cccccccc") {
    name, age, joining {
      title
    }
  }
}
```

以下のようなデータが返却される。

```json
{
  "data": {
    "getMember": {
      "name": "富田鈴花",
      "age": 18,
      "joining": [
        {
          "title": "こんなに好きになっちゃっていいの?"
        },
        {
          "title": "ホントの時間"
        },
        {
          "title": "まさか　偶然…"
        },
        {
          "title": "川は流れる"
        }
      ]
    }
  }
}
```

STSのデバッグモードで確認すると`MemberResolver.joining`の引数には`joining`以外のフィールドに検索結果が挿入された状態のインスタンスが渡されることがわかる。
また、クエリに`joining`を指定していない場合および存在しないmemberIdを指定して結果がnullの場合はこのメソッドは実行されない。

# APIその3:登録機能を作ってみる
ここまではQuery（CRUDのRに相当）のみを扱ってきた。次はMutation（CRUDのC・U・Dに相当）を実装してみる。

### GraphQL定義追加
定義ファイルに追記する。

```graphql:schema.graphqls
type Mutation {
	registerMusic(title: String!): Music
}
```

### Java側の実装
##### ミューテーション
エンティティは既存のものを使用するので、ここではミューテーションに対応するメソッドのみを作成する。`GraphQLMutationResolver`実装のクラスを作成し、クエリと同様にGraphQLに対応する引数・戻り値を持つメソッドを作成する。
（generateId()はIDとなる文字列を生成するメソッド、MusicMapper.insertは引数の情報をすべてinsertするメソッド。（実装内容は割愛））

```java:MusicMutationResolver
@Component
public class MusicMutationResolver implements GraphQLMutationResolver {

	@Autowired
	MusicMapper mapper;

	public Music registerMusic(String title) {
		Music music = new Music();
		music.setMusicId(generateId());
		music.setTitle(title);
		mapper.insert(music);
		return music;
	}
}
```

### クエリ発行
mutationの場合は`POST`でAPIを呼び出す。クエリはJSON形式のパラメータとして添付する。
※クエリ内の`"`をエスケープすること

```bash
$ curl -s "http://localhost:8080/graphql" -X POST -H "Content-Type: application/json" -d '{"query":"mutation{registerMusic(title:\"NO WAR in the future\"){musicId,title}}"}'
{"data":{"registerMusic":{"musicId":"hogehoge","title":"NO WAR in the future"}}}
```

#### Graph<em>i</em>QL
Graph<em>i</em>QLで試す場合は左側に`mutation{...}`を記述し、その内側にqueryと同様にAPI名、引数、出力内容を記述する。

![GraphiQL_mutation.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266076/12f531ba-6781-7fc8-d684-9ee757d145ea.png)

# 参考
* [GraphQL入門 - 使いたくなるGraphQL - Qiita](https://qiita.com/bananaumai/items/3eb77a67102f53e8a1ad)
* [Spring Boot + GraphQLでAPIを作成してみよう！ - Yahoo! JAPAN Tech Blog](https://techblog.yahoo.co.jp/advent-calendar-2018/create-springbook-graphql-api/)
* [GraphQL Java](https://www.graphql-java.com/)
* [GitHub - graphql-java-kickstart/graphql-spring-boot](https://github.com/graphql-java-kickstart/graphql-spring-boot)