はじめに
遅ればせながらGraphQLなるものを知ったので、自分が使い慣れているJava+Spring Bootでの実装を試した。
GraphQL自体の説明はこちらなどに任せるとして以下では実装について記述する。
プロジェクト準備
開発環境
- 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。
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>5.10.0</version>
</dependency>
プロパティとしてkotlinのバージョン指定も必要(記述がないと実行時に例外が発生する)。
<properties>
<java.version>11</java.version>
<kotlin.version>1.3.10</kotlin.version>
</properties>
APIその1:簡単な取得クエリに対応
以下のようなモデルに対してIDにより1つのエンティティを取得することを考える。
メンバー(メンバーID, 氏名, 年齢)
GraphQL定義作成
以下のように型定義とクエリ定義を記述したファイルをsrc/main/resources/graphql
配下に作成する。
type Member {
memberId: ID!
name: String!
age: Int
}
type Query {
getMember(memberId:ID!): Member
}
Java側の実装
エンティティクラス
まず型定義に合ったエンティティクラスを作成する。
@Data
public class Member {
private String memberId;
private String name;
private Integer age;
}
クエリ
次にクエリに対応するメソッドを作成する。GraphQLQueryResolver
実装のクラスを作成し、クエリ名と同じ名前のメソッドを作成する。引数および戻り値も対応するクラスを定義し、実際に戻すインスタンスを実装する。戻り値の作成方法は任意(MyBatisのMapperを利用(割愛))。戻り値のフィールドにはサーブレットとして返す可能性のある値をすべて詰めておく。
@Component
public class MemberQueryResolver implements GraphQLQueryResolver {
@Autowired
MemberMapper mapper;
public Member getMember(String memberId) {
return mapper.getById(memberId);
}
}
実行
起動構成
サーブレットの起動構成をapplication.ymlに記述する。とりあえず以下の設定があれば起動する。詳細はこちらを参照。
graphql:
servlet:
enabled: true
mapping: /graphql
corsEnabled: true
起動
記述したら作成したプロジェクトをSpring Boot Appとして実行する。
クエリ発行
http://localhost:8080/graphql
に対してURLパラメータquery
の値としてGraphQLのクエリを設定してアクセスすると結果が得られる。
たとえば以下のクエリを発行する場合、
query {
getMember(memberId:"aaaaaaaa") {
name
}
}
不要な空白と改行を除いてencodeURI
するとquery%7BgetMember(memberId:%22aaaaaaaa%22)%7Bname%7D%7D
なのでcurlで確認する。
$ curl -s "http://localhost:8080/graphql?query=query%7BgetMember(memberId:%22aaaaaaaa%22)%7Bname%7D%7D"
{"data":{"getMember":{"name":"齊藤京子"}}}
memberIdを変更して、取得キーにageを加えた場合は以下のような結果が得られる。
$ curl -s "http://localhost:8080/graphql?query=query%7BgetMember(memberId:%22bbbbbbbb%22)%7Bname,age%7D%7D"
{"data":{"getMember":{"name":"佐々木久美","age":23}}}
GraphiQLを導入する
クエリを逐一encodeURI
してcurlやブラウザでアクセスするのは面倒すぎるので、クエリを試すツールを導入する。今回はGraphiQLを利用する。
依存関係の追加
graph「i」ql-spring-boot-starterを依存関係に追加する。
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>5.10.0</version>
</dependency>
実行
起動構成
サーブレットの起動構成をapplication.ymlに記述する。とりあえず以下の設定があれば起動する。詳細はこちらを参照。上で記述したgraphqlの設定は不要。
graphiql:
enabled: true
mapping: /graphiql
endpoint:
graphql: /graphql
起動
記述したら作成したプロジェクトをSpring Boot Appとして実行する。
サーバが起動したらブラウザからhttp://localhost:8080/graphiql
にアクセスする。
左側のクエリ作成画面ではフィールド名の入力補助があり、実行結果のjsonも整形済みで表示される。
APIその2:型を入れ子にしてみる
その1でのモデルに加えて以下のような多対多のモデルをリレーションによってつないでいるモデルに対してIDにより1つのエンティティを取得することを考える。
メンバー(メンバーID, 氏名, 年齢)
楽曲(楽曲ID, 曲名)
参加楽曲リレーション(楽曲ID, メンバーID)
GraphQL定義修正
定義ファイルに追記する。多対多なのでMember
に対するMusic
もMusic
に対するMember
も配列になる。
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
にしておく。
@Data
public class Music {
private String musicId;
private String title;
private List<Member> members;
}
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をすべて取得するメソッド(実装内容は割愛)。)
@Component
public class MemberResolver implements GraphQLResolver<Member> {
@Autowired
MusicMapper mapper;
public List<Music> joining(Member member) {
return mapper.getMusicsOfMember(member.getMemberId());
}
}
実行
以下のクエリを発行すると
query {
getMember(memberId:"cccccccc") {
name, age, joining {
title
}
}
}
以下のようなデータが返却される。
{
"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定義追加
定義ファイルに追記する。
type Mutation {
registerMusic(title: String!): Music
}
Java側の実装
ミューテーション
エンティティは既存のものを使用するので、ここではミューテーションに対応するメソッドのみを作成する。GraphQLMutationResolver
実装のクラスを作成し、クエリと同様にGraphQLに対応する引数・戻り値を持つメソッドを作成する。
(generateId()はIDとなる文字列を生成するメソッド、MusicMapper.insertは引数の情報をすべてinsertするメソッド。(実装内容は割愛))
@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形式のパラメータとして添付する。
※クエリ内の"
をエスケープすること
$ 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"}}}
GraphiQL
GraphiQLで試す場合は左側にmutation{...}
を記述し、その内側にqueryと同様にAPI名、引数、出力内容を記述する。