Spring GraphQL を触ってみた
はじめに
2021 年 8 月 6 日に開催された JSUG勉強会 2021年その2 Spring GraphQLをとことん語る夕べ に参加し、Spring GraphQL について初めて学んできました。 新しく学べたことがたくさんあったので、復習も兼ねて、実際に手を動かして実装してみました。 このページで実装したリポジトリはこちらになります。
また勉強会で使用された資料やデモ実装はこちらの方の Github リポジトリにて公開されています。
Graph QL とは?
そもそも、この勉強会に参加してみて、初めて GraphQL というものを知りました。 GraphQL とは、API 用に設計されたクエリ言語で、API の速度、柔軟性、開発者にとっての使いやすさを向上させるために設計された1 ようです。 特徴2 としては、
- 型を定義して、それに対してクエリを実行する => データベースに依存しない
- 必要な型に対してのみクエリを実行できる => クライアントが必要なデータのみ取得可能
- 一度のリクエストで複数のリソースを取得できる => REST の場合はリソース毎にエンドポイントを用意して、それぞれにリクエストを投げる必要あり
- API をバージョンなしで進化させられる => クライアントは必要な型のみを定義してクエリを実行するため、新たなフィールドが型に追加されたとしても、クライアントにはなんの影響も出ない。もし、型のフィールドを削除する場合にも、deprecated アノテーションをつけるなど、対応策がある。
が挙げられます。
実装してみる
環境
- Java: 11
- Spring Boot: 2.5.3
- graphql-spring-boot-starter: 5.0.2
プロジェクト作成
プロジェクトは、Spring Initializer を使用して作成します。 group や artifact などは好きに設定して OK です。 Depedency には以下を追加しました。
これらに加えて、今回使用する Spring GraphQL の depedency を追加します。
<dependency> <groupId>org.springframework.experimental</groupId> <artifactId>graphql-spring-boot-starter</artifactId> <version>1.0.0-M1</version> </dependency> <repositories> <repository> <id>spring-milestones</id> <url>https://repo.spring.io/milestone</url> </repository> <repository> <id>spring-snapshots</id> <url>https://repo.spring.io/snapshot</url> <releases> <enabled>false</enabled> </releases> </repository> </repositories>
型の定義
まずは今回扱う型の定義を行います。
SpringBoot では、クラスパス上の graphql
ディレクトリに存在する
.graphqls .graphql .gpl .gpls
を型定義の対象ファイルとして扱うので、src/main/resources/graphql
以下に型定義ファイルを作成します。
今回は、各ゲーム会社についてデータを作成しようと思うので、Company と Game という型を作っていきます。
type Game { id: ID! name: String! companyName: String kind: String } type Company { companyId: ID! name: String! games: [Game!]! } type Query { game(id: ID!): Game games: [Game!]! company(companyId: ID!): Company companies: [Company!]! } type Mutation { createCompany(companyId: ID!, name: String!): Company createGame(id: ID!, name: String!, companyName: String!, kind: String!): Game }
これらは GraphQL の文法になるので、詳細や他の文法については公式ページを見ていただくとして、今回使用した type XXX
と type Query
、type Mutation
について簡単に説明します。
type XXX
: type を用いて宣言を行うと、その名前の型を定義することができます。Java でいう class XXX
としているようなものです。今回の例では、id, name, companyName, kind というフィールドを持つ Game 型と companyId, name, games というフィールドを持つ Company 型を定義したことになります。
それぞれのフィールドは フィールド名: データタイプ
という形式で記述します。データタイプは、公式ページより、以下が指定できるようです。
- ID: The ID scalar type represents a unique identifier (= データを一意に特定するためのもの。SQL の主キー。)
- Int: A signed 32‐bit integer
- Float: A signed double-precision floating-point value
- String: A UTF‐8 character sequence.
- Boolean:
true
orfalse
type Query
: これは特別な型宣言で、この Query type の中でデータを取得するときの query を宣言します。
type Mutation
: これも特別な型宣言で、この Mutation type の中でデータを更新、作成、削除するときの query を宣言します。
データの挿入
今回はデモができれば良いだけなので、利便性を重視して、インメモリデータベースの h2 を使用します。 インメモリではありますが、設定次第で、作成したデータをファイルに保存して永続化ができるので、今回はそちらで対応します。
まずは、下記の SQL でテーブルを作成します。
-- GAME テーブルの作成 CREATE TABLE GAME( ID INT PRIMARY KEY, NAME VARCHAR(100) NOT NULL, COMPANY_NAME VARCHAR(100) NOT NULL, KIND VARCHAR(10) ); -- COMPANY テーブルの作成 CREATE TABLE COMPANY( COMPANY_ID INT PRIMARY KEY, NAME VARCHAR(100) NOT NULL );
次にテストデータを作成します。
-- GAME data INSERT INTO GAME (ID, NAME, COMPANY_NAME, KIND) VALUES(1, 'ドラゴンクエスト XI 過ぎ去りし時を求めて', 'SQUEA ENIX', 'RPG'); INSERT INTO GAME (ID, NAME, COMPANY_NAME, KIND) VALUES(2, 'FINAL FANTASY X', 'SQUEA ENIX', 'RPG'); INSERT INTO GAME (ID, NAME, COMPANY_NAME, KIND) VALUES(3, '桃太郎電鉄 ~昭和 平成 令和も定番!', 'ハドソン', 'ボードゲーム'); INSERT INTO GAME (ID, NAME, COMPANY_NAME, KIND) VALUES(4, 'マリオテニスエース', '任天堂', 'スポーツゲーム'); -- COMPANY data INSERT INTO COMPANY (COMPANY_ID, NAME) VALUES (1, 'SQUEA ENIX'); INSERT INTO COMPANY (COMPANY_ID, NAME) VALUES (2, 'ハドソン'); INSERT INTO COMPANY (COMPANY_ID, NAME) VALUES (3, '任天堂');
データを確認します。
SELECT * FROM GAME;
SELECT * FROM COMPANY;
画像の通りテストデータを作成することができました。
次は、このデータを取得するまでの流れを作成します。
データフェッチ部分の実装
まずは各 type に対応する Entity クラスを作成します。
// Company @Entity public class Company { @Id Integer companyId; String name; public Company() { } public Company(Integer companyId, String name) { this.companyId = companyId; this.name = name; } public Integer getCompanyId() { return companyId; } public void setCompanyId(Integer companyId) { this.companyId = companyId; } public String getName() { return name; } public void setName(String name) { this.name = name; } } // Game @Entity public class Game { @Id Integer id; String name; String companyName; String kind; public Game() { } public Game(Integer id, String name, String companyName, String kind) { this.id = id; this.name = name; this.companyName = companyName; this.kind = kind; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCompanyName() { return companyName; } public void setCompanyName(String companyName) { this.companyName = companyName; } public String getKind() { return kind; } public void setKind(String kind) { this.kind = kind; } }
次に、データをフェッチしてくる部分の実装です。
RuntimeWiringBuilderCustomizer
クラスの customize
メソッドをオーバーライドすることで実現できます。
@Component public class GameDataWiring implements RuntimeWiringBuilderCustomizer { private GameRepository gameRepository; private CompanyRepository companyRepository; public GameDataWiring(GameRepository gameRepository, CompanyRepository companyRepository) { this.gameRepository = gameRepository; this.companyRepository = companyRepository; } @Override public void customize(RuntimeWiring.Builder builder) { builder.type("Query", builder1 -> builder1.dataFetcher("game", env -> { Integer id = Integer.valueOf(env.getArgument("id")); return gameRepository.findById(id); }) .dataFetcher("games", env -> { return gameRepository.findAll(); }) .dataFetcher("company", env -> { Integer companyId = Integer.valueOf(env.getArgument("companyId")); return companyRepository.findById(companyId); }) .dataFetcher("companies", env -> { return companyRepository.findAll(); }) ); builder.type("Company", builder1 -> builder1.dataFetcher("games", env -> { Company company = env.getSource(); return gameRepository.findByCompanyName(company.getName()); }) ); builder.type("Mutation", builder1 -> builder1.dataFetcher("createCompany", env -> { Integer companyId = Integer.valueOf(env.getArgument("companyId")); String companyName = env.getArgument("name"); Company company = new Company(); company.setCompanyId(companyId); company.setName(companyName); return companyRepository.save(company); }) .dataFetcher("createGame", env -> { Integer id = Integer.valueOf(env.getArgument("id")); String gameName = env.getArgument("name"); String companyName = env.getArgument("companyName"); String gameKind = env.getArgument("kind"); Game game = new Game(); game.setId(id); game.setName(gameName); game.setCompanyName(companyName); game.setKind(gameKind); return gameRepository.save(game); }) ); } }
ポイントとしては、最初に定義した型に対する dataFetcher を全て定義する ということです。
今回の例では、"Query" として定義した中に、"game"、"games"、"company"、"companies" という子要素を定義しました。
そのため、builder に対して "Query" を type メソッドで宣言し、その中でそれぞれの子要素に対して対応するデータ処理を書いていきます。
"game" の場合は、引数として与えられた id を基に、DB 上からレコードを引っ張ってくることになるので、return で gameRepository.findById(id) を返しています。
一方で、Company を呼び出した際には、"games" が呼び出されて、その会社が販売している Game データを紐付けます。このような場合には、builder に対して "Company" を type メソッドで宣言し、対応する Game データを取得します。
このようにして、自分が定義したデータ型に対して、全ての要素に対応するデータ取得方法を実装していきます。
実行
ここまでで実行までの準備が完了です。
REST や Controlller といった実装に慣れている方は少し戸惑うかもしれないのですが、GraphQL の場合は、デフォルトで /graphql
に対して POST エンドポイントがマッピングされていて、特に Controller の設定をしなくても大丈夫です。これに加えて、/graphiql
という GUI のパスもデフォルトで設定されているので、容易に動作検証を行うことができます。
では、それぞれのクエリについて動作を見ていきましょう。
game(id: ID)
query { game(id: 1) { id name companyName kind } }
games
query { games { id name companyName kind } }
copmany(id: ID)
query { company(companyId: 1) { companyId name games { id name kind } } }
対応する Game データの一覧が取得できているのがわかります。
companies
query { companies { name games{ id name kind } } }
Mutation も同様にクエリを構築して、リクエストを投げることができます。
createCompany
mutation { createCompany(companyId: 4, name: "KONAMI") { companyId name } }
H2 database 上でも確認してみます。 正しくデータが追加されていることがわかります。
createGame
mutation { createGame(id:5, name: "パワフルプロ野球 2020", companyName: "KONAMI", kind: "スポーツゲーム"){ id name kind } }
こちらも、H2 database 上でも確認してみます。 正しくデータが追加されていることがわかります。
まとめ
この記事では、Spring GraphQL を使用して、簡単な Query と Mutation を作成してみました。 実際に実装してみた感想としては、GraphQL に関わる箇所の実装がそこまで複雑じゃないのに、柔軟なエンドポイントを作成できたので、とても利便性が高いと感じました。 特にクライアントが欲しいデータのみリクエストできるところが、魅力的に感じました。
ただ、実際に業務で使用するとなると、
- エンドポイントのセキュリティはどうするか => Spring Security を使えば実装できるようだが、結構込み入った話になってくるので、ここに実装の工数が多くかかりそう
- データが大量にある場合に、どうやってパフォーマンスを上げるか => DataLoader を使うことでパフォーマンスの向上を図れるが、複雑なデータ型だったりした場合にはうまくいかないかも?
のあたりをどうするか要検討ポイントだと感じました。
ただ、GraphQL でうまくいかなそうなところは、Spring MVC をベースとした REST API を組んだりするなど、方向転換が容易なところが Spring GraphQL の大きな魅力なのかなとも思いました。
参考文献
*1: GraphQL とは - 概要、特徴、メリット・デメリット | Red Hat
*2: https://backpaper0.github.io/spring-graphql-introduction/