ヤフー株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。LINEヤフー Tech Blog

テクノロジー

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

はじめに

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

「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に興味を持っていただけたら幸いです。

参考

こちらの記事のご感想を聞かせください。

  • 学びがある
  • わかりやすい
  • 新しい視点

ご感想ありがとうございました

このページの先頭へ