Spring Boot + GraphQLでAPIを作成してみよう!

  • このエントリーをはてなブックマークに追加
Yahoo! JAPAN Tech Advent Calendar 2018の9日目の記事です。一覧はこちら

はじめに

こんにちは、コマース福岡開発部で金融サービスのバックエンドを担当しているチョ ジョンミンと申します。

「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概要

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から本の情報が登録できること

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`)
)

作成の流れ

  1. Gradle定義
  2. Schema定義
  3. Data Class作成
  4. Resolver作成
  5. 起動

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するようになります。

@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

まとめ

実際にAPIを作ってみると、思っていたよりも簡単に実装することができました。何より自分の知らない技術を使ってAPIを作る過程が楽しかったです!! 👏👏👏 今回、GraphQLを使ってAPIを実装してみましたが、今後もGraphQLのメリットが活かせるプロダクトを実装する際にはGraphQLの導入を検討したいと感じました。この記事を読んでいただいた皆さんにもGraphQLに興味を持っていただけたら幸いです。

参考

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加