元営業WEBエンジニアのアプリ開発日記

営業出身のWEB系エンジニアが気になったものから作ってはメモを残してくブログ

SpringBootとGraphQLで素敵なAPI作成

概要

何を勉強してれば今風のwebエンジニアて言えるのかと思ってBack-end Roadmap読んでたら、GrapgQLとかいう知らない言葉が出てきたから調べて実装してみた。

GraphQLとは

ざっくりイメージ

小難しい解説は色んなサイトに載ってると思うんで、ざっくりとした自分のイメージだけ

  • GraphQLはRESTとかとおんなじAPIの実装方式
  • RESTはURLやHTTPメソッド(GET, POST等)で取得したいリソースとか色々明示されてるけど、GraphQLは同じURLにPOSTでアクセス。その際に渡すjsonの書き方に規則性をもたせてやりたいAPIを実現する
    • 厳密なRESTだと複数リソースにアクセスしたい場合、2つのURLで2本API呼ばなきゃいけないけど、GraphQLだと1本のAPIで複数リソース処理することも気にならない
    • 返却項目を指定できるので、必要なものを必要なだけゲットできる

具体的なイメージ

一定の規則性(クエリ)を記載して対象のサーバーにリクエストするとよろしく情報をとってきてくれる。

{
  members(name: "test") {
      id
      name
      createdAt
      updatedAt
}

ポイントはこんな感じ

  1. 関数風に指定して引数を渡すことができる
  2. id, nameとか欲しいものを明示しとけばそれだけ返ってくる

こんな感じでレスポンスが戻ってくる

{
  "data": {
    "members": {
      "id": "1",
      "name": "test"
      "createdAt": "2018-08-24T17:51:20",
      "updatedAt": "2018-08-24T17:51:20",
    }
  }
}

SpringBootでGraphQL(お試し編)

実際にSpringBootで実装してみる。
ここからはTomcatでHelloWorldを表示するSpringBootアプリケーションを作るでアプリケーション作ってる前提

必要な依存関係を設定

/future/build.gradleに4つの依存関係を追加

...
dependencies {
...
    // to embed GraphQL
    compile 'com.graphql-java:graphql-spring-boot-starter:5.0.2'
    // to embed GraphiQL tool
    compile 'com.graphql-java:graphiql-spring-boot-starter:5.0.2'
    // to embed Voyager tool
    compile 'com.graphql-java:voyager-spring-boot-starter:5.0.2'
    // to GraphQL Java Tools
    compile 'com.graphql-java:graphql-java-tools:5.1.0'
}   

スキーマを定義

testに対してresponseという文字列を返却する以下schemaメソッドを追加する

/future/src/main/java/xyz/ucwork/future/FutureApplication.java

...
  public class FutureApplication {
...
  
    @Bean
    GraphQLSchema schema() {
        return GraphQLSchema.newSchema()
                .query(GraphQLObjectType.newObject().name("query").field(
                        field -> field.name("test").type(Scalars.GraphQLString)
                                .dataFetcher(environment -> "response"))
                        .build())
                .build();
    }
}

Nginxの設定を編集

GraphQLの動作を確認できるGraphiQLを動作させるために以下設定

nginx.conf

server {
    listen 80;
    server_name future.local;

    access_log /var/log/nginx/tomcat_access.log;
    error_log /var/log/nginx/tomcat_error.log;

    location /future/ {
        proxy_set_header X-Forwarded-Host $host:$server_port;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_pass http://servlet01:8080;
    }

    location /subscriptions {
        proxy_pass http://servlet01:8080/future/subscriptions;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

ブラウザでアクセスしてみる

http://future.local/future/graphiqlにブラウザからアクセスするとGraphiQLの画面が表示される。
左の枠に以下文字列貼り付けて再生ボタンクリックすると

query{
  test
}

設定したレスポンスが返ってくる!

{
  "data": {
    "test": "response"
  }
}

(余談)websocketのsubscriptions

余談だが/subscriptionの扱いで相当壁にぶつかった。。
そもそもNginxを適切に設定しないとブラウザのconsoleでずっとエラー出てきたし GraphiQL表示するとアプリケーションログにこんな感じのエラーログが毎度出てくる。

ERROR 8 --- [http-apr-8080-exec-11] graphql.servlet.GraphQLWebsocketServlet  : Error in websocket session: 54

DockerのTomcatイメージをtomcat:8.0-jre8-alpinetomcat:8.5.32-jre8-alpineバージョンアップすることでエラーログ出力が初期起動時のみとなったが、根本的に解決してない。。。。。。
とりあえず動いてるから良いけどWebsocket実装するときにちゃんと考えよう

SpringBootでGraphQL(会員テーブル操作編)

この先はSpringBootとMybatisの記事の内容を実装している想定で書いていきます。

Schemaを設定

GraphQL Java Toolsをbuild.gradleに追加したおかげでresourceにSchemaを定義するとリクエストをよろしく処理してくれる
/future/src/main/resources/graphqls/配下に以下3ファイルを追加する

!を付けると必ず値が必要な項目となる。
あとエラー受け取ったらハンドリングしたいので、下の例ではerrorsを定義してます

schema.graphqls

schema {
    query: Query
    mutation: Mutation
}

# Common
type Error {
  code: String
  message: String
}

mutation.graphqls

type Mutation {
    # Regist member
    registMember(
      name: String!
    ): RegistMemberResponse!
}

type RegistMemberResponse {
    registMember: RegistMember
    errors: [Error]
}

type RegistMember {
  id: String!
  name: String!
  createdAt: String!
}

query.graphqls

type Query {
    # Find by name
    members(
      name: String!
    ): MembersResponse!
}

type MembersResponse {
    members: [Member]
    errors: [Error]
}

type Member {
  id: String!
  name: String!
  createdAt: String!
  updatedAt: String
}

typesを定義

Schemaに定義したtypeに紐づくオブジェクトを定義する。

lombokを導入

getter, setter書くの面倒なのでlombokを導入(必須じゃないっす)

build.gradle

...
dependencies {
...
    // To remove Getter and Setter Method
    compile 'org.projectlombok:lombok:1.18.0'
}

typesオブジェクトを作成

/future/src/main/java/xyz/ucwork/future/types/配下に以下5ファイルを生成

Error.java

package xyz.ucwork.future.types;

import lombok.Data;

@Data
public class Error {
    private String code;
    private String message;
}

Member.java

package xyz.ucwork.future.types;

import lombok.Data;

@Data
public class Member {
    private String id;
    private String name;
    private String createdAt;
    private String updatedAt;
}

MembersResponse.java

package xyz.ucwork.future.types;

import java.util.List;

import lombok.Data;

@Data
public class MembersResponse {

    private List<Member> members;
    private List<Error> errors;

}

RegistMember.java

package xyz.ucwork.future.types;

import lombok.Data;

@Data
public class RegistMember {
    private String id;
    private String name;
    private String createdAt;
}

RegistMemberResponse.java

package xyz.ucwork.future.types;

import java.util.List;

import lombok.Data;

@Data
public class RegistMemberResponse {

    private RegistMember registMember;
    private List<Error> errors;
}

resolversを定義

スキーマに紐づくリクエストが来たときにどんな処理するのか記載する。
本当はserviceパッケージとかにロジック分けたいけどサンプルなんで直に書いてます。

ModelMapperを追加

DBからとってきたオブジェクトをGraphQLのtypeオブジェクトに詰め替えるのに毎度getとかsetとかしてると面倒なんでmodelMapper使う

build.gradle

...
dependencies {
...
    // Bean Mapper
    compile 'org.modelmapper:modelmapper:2.1.0'
}

/future/src/main/java/xyz/ucwork/future/FutureConfig.java

package xyz.ucwork.future;

import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FutureConfig {
    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}

resolversを設定

/future/src/main/java/xyz/ucwork/future/resolvers/に以下設定

渡された名前をテーブルにインサート Mutation.java

package xyz.ucwork.future.resolvers;

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;

import xyz.ucwork.future.domain.mapper.ext.ExtMembersMapper;
import xyz.ucwork.future.domain.model.Members;
import xyz.ucwork.future.types.RegistMember;
import xyz.ucwork.future.types.RegistMemberResponse;

@Component
public class Mutation implements GraphQLMutationResolver {
    @Autowired
    private ExtMembersMapper extMembersMapper;
    @Autowired
    private ModelMapper modelMapper;

    @Transactional
    public RegistMemberResponse registMember(String name) {
        // テーブルにインサート
        Members insertMembers = new Members();
        insertMembers.setName(name);
        extMembersMapper.insertWithName(insertMembers);
        // レスポンスにセット
        RegistMemberResponse registMemberResponse = new RegistMemberResponse();
        registMemberResponse.setRegistMember(modelMapper.map(insertMembers, RegistMember.class));
        return registMemberResponse;
    }
}

渡された名前でテーブルからレコード取得 Query.java

package xyz.ucwork.future.resolvers;

import java.util.List;
import java.util.stream.Collectors;

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.coxautodev.graphql.tools.GraphQLQueryResolver;

import xyz.ucwork.future.domain.mapper.ext.ExtMembersMapper;
import xyz.ucwork.future.domain.model.Members;
import xyz.ucwork.future.types.Member;
import xyz.ucwork.future.types.MembersResponse;

@Component
public class Query implements GraphQLQueryResolver {

    @Autowired
    private ExtMembersMapper extMembersMapper;
    @Autowired
    private ModelMapper modelMapper;

    public MembersResponse getMembers(String name) {
        // nameからレコード取得
        List<Members> membersRow = extMembersMapper.selectByName(name);
        // レスポンス返却
        MembersResponse membersResponse = new MembersResponse();
        if (!membersRow.isEmpty()) {
            List<Member> members = membersRow.stream().map(member -> modelMapper.map(member, Member.class))
                    .collect(Collectors.toList());
            membersResponse.setMembers(members);
        }
        return membersResponse;
    }
}

ブラウザで確認

http://future.local/future/graphiqlにアクセスしてGraphiQLで確認
いろいろ実験してみると良いと思います!

テーブルにインサート

リクエス

mutation {
  registMember(name: "testName") {
    registMember {
      id
      name
    }
    errors {
      code
      message
    }
  }
}

レスポンス

{
  "data": {
    "registMember": {
      "registMember": {
        "id": "11",
        "name": "testName"
      },
      "errors": null
    }
  }
}

テーブルから取得

リクエス

{
  members(name: "testName") {
    members {
      id
      name
      createdAt
      updatedAt
    }
    errors {
      code
      message
    }
  }
}

レスポンス

{
  "data": {
    "members": {
      "members": [
        {
          "id": "11",
          "name": "testName",
          "createdAt": "2018-08-22T23:20:23",
          "updatedAt": null
        }
      ],
      "errors": null
    }
  }
}

まとめ

以前RESTでAPI書いた時「これ更新するけど本質的には取得してるだけだからGETなのかなぁ」とか、「URLの命名規則はRESTfulになってんのかなぁ」とか、割と宗教的な部分で悩んだので、基本的にはその辺のルールが決まっててクエリで内容定義できるgraphQLはいいなと思いました。

会社だと暗黙的にRESTな空気があり、わざわざ学習コストをかけ流必要あるのか?とか言われそうなので世の中的に広まってくかはわかりませんが、私はこれ使ってこうと思いまっす