はじめに
こんにちは、コマース福岡開発部で金融サービスのバックエンドを担当しているチョ ジョンミンと申します。
「Yahoo! JAPAN Tech Advent Calendar 2018」に寄稿させていただく機会をいただきましたので、前から興味のあったGraphQLのAPIを実装してみました。本記事では、GraphQLの概要とAPIの作り方を紹介します。
ターゲット
本記事は、次の読者を想定して書かれています。
- GraphQLに興味はあるが、作成した経験がない方
- Spring BootでGraphQLのAPIを作成してみたい方
目次
利用している技術
APIはJavaで実装しました。利用したJavaのバージョンとライブラリは次の通りです。
- Java
- Java:1.8.0_162
- Spring Boot:2.1.0.RELEASE
- Mysql:5.7
- GraphQL For Java
- graphql-spring-boot-starter:5.2.0
- graphql-java-tools:5.1.0
- graphiql-spring-boot-starter:5.2.0
- graphql-spring-boot-starter:5.2.0
GraphQL概要
APIを作る前に簡単にGraphQLの概要を説明します。
GraphQLとは
- Clientからサービスに必要なデータを記述し取得できるAPIのためのQuery言語(みなさんが考えるDBのSQLとは違うもの)
- 2012年Facebookから開発し2015年にリリースされた一つのデータ通信規約
- GithubのAPI(v4)がGraphQLを採用!!
GraphQLの特徴
- 単一Entry Point、Single End Point(絶対ではありません。)
- 必要なデータだけ取得
- 型システム(GraphQL Type)
Schema、Type、QueryとMutation
GraphQL APIの定義をするため基本的な要素の「Schema、Type、QueryとMutation」について簡単に説明します。文字だけだとわかりにくいと思いますので、ここでは概要だけ知っていただければ大丈夫です。
Type
GraphQL APIで扱うデータのことです。個人的にはQueryで取得したり、Mutationで登録/更新/削除の時に扱うデータという認識です。
Query
RESTでいうGetです。データを取得する定義をするものです。
Mutation
RESTでいうPost/Patch/Put/Deleteです。データの登録/更新/削除の定義をするものです。
Schema
TypeやQuery、MutationなどのAPIの振る舞いを定義するところです。
GraphQLのメリットとデメリット
メリット
- 欲しいデータだけRequestで取得できる
- Queryでfieldをclientで自由に指摘できる
- リクエスト一発で欲しいデータ全部持ってこれるので通信回数を減らせる
- 別途I/FなどのDocumentの作成が必要ない
- Schemaの定義そのものがDocument
デメリット
- 学習コストが必要
- HTTPキャッシングが難しい(キャッシングを支援するライブラリはあります)
GraphQLのAPI実装
長くなりましたが、ここからが本番です。
作成するAPI概要
- 著者・本のデータを扱うAPI
- 機能
- 著者Idから情報取得
- 著者情報および本の情報が取得できること
- 著者登録
- 著者名が登録できること
- 本登録
- 著者IDから本の情報が登録できること
- 著者Idから情報取得
- 機能
DB構成
- Database作成
create database sample
- Author Table
CREATE TABLE `author` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL,
PRIMARY KEY (`id`),
KEY `author_idx1` (`id`)
)
- Boot Table
CREATE TABLE `book` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL,
`author_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`author_id`) REFERENCES author(`id`),
KEY `book_idx1` (`id`)
)
- 自分はDocker利用して作成しました。参考ページ
作成の流れ
MybatisによるDB連携の部分は省略します
ディレクトリ構成
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── demo
│ │ ├── DemoApplication.java
│ │ ├── infrastructure
│ │ │ ├── AuthorRepositoryImpl.java
│ │ │ ├── BookRepositoryImpl.java
│ │ │ └── mapper
│ │ │ ├── AuthorEntity.java
│ │ │ ├── AuthorMapper.java
│ │ │ ├── BookEntity.java
│ │ │ └── BookMapper.java
│ │ ├── model
│ │ │ ├── AuthorFactory.java
│ │ │ ├── AuthorRepository.java
│ │ │ ├── BookFactory.java
│ │ │ ├── BookRepository.java
│ │ │ ├── RegistAuthorFactory.java
│ │ │ ├── RegistBookFactory.java
│ │ │ └── value
│ │ │ ├── AuthorId.java
│ │ │ ├── Id.java
│ │ │ └── Name.java
│ │ ├── resolvers
│ │ │ ├── AuthorMutationResolver.java
│ │ │ ├── AuthorQueryResolver.java
│ │ │ ├── BookMutationResolver.java
│ │ │ ├── BookResolver.java
│ │ │ └── Query.java
│ │ └── types
│ │ ├── Author.java
│ │ ├── Book.java
│ │ ├── RegistAuthor.java
│ │ └── RegistBook.java
│ └── resources
│ ├── application.yml
│ ├── graphql
│ │ └── query.graphqls
│ └── mapper
│ ├── AuthorMapper.xml
│ └── BookMapper.xml
└── test
├── java
│ └── com
│ └── example
│ └── demo
│ └── DemoApplicationTests.java
└── resources
Gradle定義
GraphQLAPIを作成するため、graphql、graphiqlおよびlombokのライブラリを定義します。
dependencies {
compileOnly "org.projectlombok:lombok'"
// 省略
// graphql-java-toolsはgraphql-spring-boot-starterに含まれている
compile "com.graphql-java-kickstart:graphql-spring-boot-starter:5.1"
compile "com.graphql-java-kickstart:graphiql-spring-boot-starter:5.1"
testCompile "com.graphql-java-kickstart:graphql-spring-boot-starter-test:5.1"
}
Schema定義
ここからは作成するAPI概要に書いている通りにTypeやQuery、Mutationの定義をSchemaに書いていきます。
Schemaファイルの作成
- Schemaを配置するディレクトリを次の階層で作成してください。
- src/main/resources/graphql
- Schemaファイルを作成してください。
- src/main/resources/graphql/schema.graphqls
- 参考:サンプルコードのschema.graphqls
作ったschema.graphqlsの中に次から説明するTypeやQuery、Mutationの定義を書いていきます。実際はSchema定義ファイルは目的に応じてファイルを分けるのがいいですが、今回はschema.graphqlsにまとめて定義しています。
Type定義
GraphQL概要のTypeで説明した通りにAPIで扱うデータを定義します。
- src/main/resources/graphql/schema.graphqlsの中に定義します。
Author(著者)、Book(本)、登録結果ResponseのTypeの定義
# Author(著者)定義
type Author {
id: ID!
name: String!
books: [Book]
}
# Book(本)定義
type Book {
id: ID!
name: String!
}
# Author(著者)の登録結果ResponseのTypeの定義
type RegistAuthorResponse {
name: String!,
isRegist: Boolean!,
errorMessage: String
}
# Author(著者)の登録結果ResponseのTypeの定義
type RegistBookResponse {
authorId: ID!,
name: String!,
isRegist: Boolean!,
errorMessage: String
}
Query定義
- GraphQL概要のQueryで説明した通りにAPIでのデータ取得の定義をします。
- src/main/resources/graphql/schema.graphqlsの中に定義します。
type Query {
getAuthorById(id: ID!): Author
}
Mutation定義
GraphQL概要のMutationで説明した通りにAPIでのデータの登録/更新/削除の定義をします。
- src/main/resources/graphql/schema.graphqlsの中に定義します。
Author登録やBookの登録のMutationの定義
type Mutation {
registAuthor(name: String!): RegistAuthor
registBook(authorId: ID!, name: String!): RegistBook
}
Data Class作成
ここまでSchemaの定義が完了したので、実際のTypeのClassを作っていきます。Data ClassとはGraphQL Java ToolではSchemaに定義したTypeのJava Classのことです(以降はData Classと記載します)。Schemaに定義したTypeの通りにDataを定義します。Dataの定義は下記で作ったtypesの中で作っていきます。
src/main/com/example/demo/types
AuthorのData Class定義
- com.example.demo.typesのPackage以下にAuthor.java を作成
@Builder
@Data
public class Author {
private int id;
private String name;
}
- BookのData Class定義
- com.example.demo.typesのPackage以下にBook.java を作成
@Builder
@Data
public class Book {
private int id;
private String name;
private String authorId;
}
- RegistAuthorResponseのData Class定義
- com.example.demo.typesのPackage以下にRegistAuthorResponse.java を作成
@Builder
@Data
public class RegistAuthorResponse {
private String name;
private String errorMessage;
private boolean regist;
}
- RegistBookResponseのData Class定義
- com.example.demo.typesのPackage以下にRegistBookResponse.java を作成
@Builder
@Data
public class RegistBookResponse {
private int authorId;
private String name;
private String errorMessage;
private boolean regist;
}
Resolver作成
ResolverとはGraphQL Java Toolで提供するものです。TypeのMappingは基本Data ClassのMethodやFiledで行います。
しかし、Mappingの対象が外部との連携(他のData Classを参照、RepositoriesやConnectionsなど連携)をするものとなるとData Classが複雑になると思います。
それを解決するためData Class内だけではなくMappingできるようにしたものがResolverとなります。
詳細はResolvers and Data Classesをご参照ください。
これからResolverでQuery、Mutationの実装をしていきます。実装するPackageは下記のようになりますので、Packageを作成してください。
- com.example.demo.resolvers
SchemaのAuthorのbooks定義
下記のように実装をすることでGraphQLのTypeをAuthor Data ClassにMappingするbooksのところをBookResolverの中のbooks関数の結果をMappingするようになります。
- Authorに定義したbooksの実装
- com.example.demo.resolversのPackage以下にBookResolver.java Classを作成
@Component
@AllArgsConstructor
public class BookResolver implements GraphQLResolver<Author> {
private final BookRepositoryImpl bookRepositoryImpl;
public List<Book> books(Author author) {
AuthorId authorId = AuthorId.of(author.getId());
return bookRepositoryImpl.getBooksByAuthorId(authorId);
}
}
Root Resolver
Root ResolversとはQueryやMutationなどはRoot GraphQLのObjectであるため、連携されるData Classがありません。そのような時に利用するのがRoot Resolverです。
Root ResolverにQueryを定義する
Root ResolverにQueryを実装します。QueryのRoot ResolverはGraphQLQueryResolverをimplementsすることで作れます。
- getAuthorById Queryを定義
- com.example.demo.resolversのPackage以下にAuthorQueryResolver.java Classを作成
@Component
@AllArgsConstructor
public class AuthorQueryResolver implements GraphQLQueryResolver {
private final AuthorRepositoryImpl authorRepositoryImpl;
public Author getAuthorById(int id) {
Id idValue = Id.of(id);
return authorRepositoryImpl.getAuthorById(idValue);
}
}
ResolverでMutationの定義
Root ResolverにMutationを実装します。MutationのRoot ResolverはGraphQLMutationResolverをimplementsすることで作れます。
- registAuthor Mutationを定義
- com.example.demo.resolversのPackage以下にAuthorMutationResolver.java Classを作成
@Component
@AllArgsConstructor
public class AuthorMutationResolver implements GraphQLMutationResolver {
private final AuthorRepositoryImpl authorRepositoryImpl;
public RegistAuthorResponse registAuthor(String name) {
Name nameValue = Name.of(name);
return authorRepositoryImpl.registAuthor(nameValue);
}
}
- registBook Mutationを定義
- com.example.demo.resolversのPackage以下にBookMutationResolver.java Classを作成
@Component
@AllArgsConstructor
public class BookMutationResolver implements GraphQLMutationResolver {
private final BookRepositoryImpl bookRepositoryImpl;
private final AuthorRepositoryImpl authorRepositoryImpl;
public RegistBookResponse registBook(String authorId, String name) throws Exception {
// 存在チェック
Id id = Id.of(Integer.valueOf(authorId));
Author author = authorRepositoryImpl.getAuthorById(id);
if (Objects.isNull(author)) {
throw new Exception("not find author. authorId:" + authorId);
}
AuthorId authorIdValue = AuthorId.of(Integer.valueOf(authorId));
Name nameValue = Name.of(name);
return bookRepositoryImpl.registBook(authorIdValue, nameValue);
}
}
起動
これから実装したAPIを動かしてみましょう。
下記のようにbootRunコマンドでサーバーを立ち上げた後にアクセスURLにアクセスすると
APIの動作確認ができます。
- bootRunコマンド
./gradlew bootRun
- アクセスURL
まとめ
実際にAPIを作ってみると、思っていたよりも簡単に実装することができました。何より自分の知らない技術を使ってAPIを作る過程が楽しかったです!!今回、GraphQLを使ってAPIを実装してみましたが、今後もGraphQLのメリットが活かせるプロダクトを実装する際にはGraphQLの導入を検討したいと感じました。この記事を読んでいただいた皆さんにもGraphQLに興味を持っていただけたら幸いです。
参考
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました