概要
SpringBootでGraphQLのサポートがされました!
ので、実際に触ってみたいと思います!
そもそもGraphQLって?
おそらくRestAPIに親しんだ人が多いと思うので、RestAPIと比較をしてみます。
例えばToDoListのタスクの一覧を取得するとき
RestAPIだと GET /todoList
みたいなエンドポイントにアクセスし情報を取得しますが、
GraphQLだとエンドポイントは1つしかなく、
{
todoList: {
id, title
}
}
このようにどんな情報が欲しいかクエリで指定して情報を取得します。
これだけだとRestAPIの方がクエリを書かなくて良い分便利に思えるのですが、
Webサービスを作ると「同じような情報を取得するけど、機能ごとに微妙に違う情報が必要」ということが出てきます。
このときRestAPIだと機能ごとにAPIを増やさなければなりませんが、GraphQLだとクエリさえ変えれば良いです!
GraphQLで作るToDoリストアプリケーション
それでは具体的なGraphQLの実装方法に移っていきますが、
ここではToDoリストの情報を提供するようなアプリケーションを作りながら
GraphQLの実装ポイントをまとめてみます。
Todoのリスト取得
まずはTODOをリストで取得するところまでやってみます。
スキーマ定義
RestAPIでいう、IFの定義の部分です
resources/graphql
配下にschema.graphqls
を作成します。
type Query {
todoList: [Todo]
}
type Todo {
id: ID
title: String
}
ポイント
-
type Query
ではtodoList
を要求するとTodo
のリストを返すよということを定義している -
type Todo
ではTodo
というスキーマが保持する情報を定義している
※QueryでエンドポイントとそのレスポンスBodyを定義して、その他でレスポンスボディの詳細を定義してあげるイメージ
Controller実装
RestAPIと同じように @Controller
を付与したクラスを用意します。
RestAPIとの違いは、GetMapping
等の代わりに@QueryMapping
を使用するところです。
@Controller
public class TodoController {
@QueryMapping
Flux<Todo> todoList() {
return Flux.just(
new Todo("1", "task1"),
new Todo("2", "task2")
);
}
}
@Value
public class Todo {
private String id;
private String title;
}
ポイント
- メソッド名は
type Query
で定義した名前と対応させる - データクラスは名前とフィールドで保持する情報をスキーマ定義と対応させる
動かしてみる
application.ymlで
spring:
graphql:
graphiql:
enabled: true
の設定を追加してからbootRun !
本番アプリケーションではtrueにしないでね😇
http://localhost:8080/graphiql/ にアクセスして、以下のようなクエリを実行してみます。
query {
todoList {
id,
title
}
}
情報取れましたね!!!
次はtitleだけ取得するようにしてみます。
{
todoList {
title
}
}
queryは省略可
クエリに合わせて取得できる情報がtitleだけになりました!
「サーバサイドの実装を変えずに必要な情報にしぼってレスポンスを変える」ことができました!!
パラメータを渡す
idを指定して、合致するTodoだけを返せるようにしてみます。
スキーマ定義
type Query {
todo(id: String): Todo
}
ポイント
- パラメータは
(フィールド名: 型)
で指定する
Controller実装
@Controller
public class TodoController {
private static List<Todo> TODO_LIST = Arrays.asList(
new Todo("1", "task1"),
new Todo("2", "task2")
);
@QueryMapping
Todo todo(@Argument String id) {
return TODO_LIST.stream()
.filter(todo -> todo.getId().equals(id))
.findFirst()
.orElse(null);
}
}
ポイント
- パラメータは
@Argument
を付与した引数で受け取る
動かしてみる
http://localhost:8080/graphiql/ にアクセスして、以下のようなクエリを実行してみます。
{
todo(id: "1") {
title
}
}
複数のスキーマを結合する
次は複数のプロジェクトで、それぞれのプロジェクトにたくさんのTODOが状況を想定し、
プロジェクトを検索したら紐づくTODOも取得できるようにしてみます。
スキーマ定義
type Query {
todoList: [Todo]
projects: [Project]
todo(id: String): Todo
}
type Todo {
id: ID
title: String
}
type Project {
id: ID
name: String
todoList: [Todo]
}
Controller実装
今回は簡略的に、TODOのタイトルの先頭にプロジェクト名があり、
前方一致でどのプロジェクトと紐づくのか判断できるようにしておきます。
スキーマ同士の紐付けを定義する場合は @SchemaMapping
を使用します。
今回の例だと、
プロジェクトを検索条件にTODOのリストが取得できれば良いので、
「引数:Project、戻り値:Todoのリスト」
というメソッドを用意します。
@Controller
public class TodoController {
private static List<Todo> TODO_LIST = Arrays.asList(
new Todo("1", "project1-task1"),
new Todo("2", "project1-task2"),
new Todo("3", "project1-task3"),
new Todo("4", "project2-task1"),
new Todo("5", "project2-task2"),
new Todo("6", "project2-task3")
);
private static List<Project> PROJECT_LIST = Arrays.asList(
new Project("1", "project1"),
new Project("2", "project2")
);
@QueryMapping
Flux<Project> projects() {
return Flux.fromStream(PROJECT_LIST.stream());
}
@SchemaMapping(typeName = "Project")
Flux<Todo> todoList(Project project) {
return Flux.fromStream(
TODO_LIST.stream()
// 前方一致で対応するTODOのみに絞り込み
.filter(todo -> todo.getTitle().startsWith(project.getName()))
);
}
}
ポイント
-
SchemaMapping
のtypeName
で引数で受け取るスキーマを設定する
もしTodoもProjectも同一のDBに入っているのであればSQLで結合させていっぺんに情報をとった方が絶対早い気がします。
自分が使うとしたらTodoとProjectが別のリソース(DBが別とか、片方がAPIからしか取得できないとか)の時の使用にとどめると思います。
データの更新
次は更新系としてTodoの追加をやってみようと思います。
RestAPIだと情報をPOSTで受け取って処理するやつですね。
スキーマ定義
更新系はMutation
というtypeで定義していきます。
書き方はパラメータを渡す時のスキーマ定義と似ています。
type Mutation {
addTodo(title: String): Todo
}
ポイント
-
addTodo
はString型のtitle
を受け取って処理を行い、結果をTodo
型で返すよということを定義している
Controller実装
もともと定義しているTODO_LIST
に要素を追加するような処理を実装してみます。
※Arrays.asListだとImmutableなListが生成されるのでnew ArrayList
でmutableなListに変換
Queryとの違いは、@QueryMapping
の代わりに@MutationMapping
を使用するところです。
@Controller
public class TodoController {
private static List<Todo> TODO_LIST = new ArrayList<>(Arrays.asList(
new Todo("1", "project1-task1"),
new Todo("2", "project1-task2"),
new Todo("3", "project1-task3"),
new Todo("4", "project2-task1"),
new Todo("5", "project2-task2"),
new Todo("6", "project2-task3")
));
@MutationMapping
Mono<Todo> addTodo(@Argument String title) {
var id = UUID.randomUUID().toString();
var todo = new Todo(id, title);
TODO_LIST.add(todo);
return Mono.just(todo);
}
}
ポイント
- メソッド名は
type Mutation
で定義した名前と対応させる - パラメータは
@Argument
を付与した引数で受け取る
動かしてみる
mutation {
addTodo(title: "task-0") {
id
title
}
}