SpringBootとGraphQLで素敵なAPI作成
概要
何を勉強してれば今風のwebエンジニアて言えるのかと思ってBack-end Roadmap読んでたら、GrapgQLとかいう知らない言葉が出てきたから調べて実装してみた。
GraphQLとは
ざっくりイメージ
小難しい解説は色んなサイトに載ってると思うんで、ざっくりとした自分のイメージだけ
- GraphQLはRESTとかとおんなじAPIの実装方式
- RESTはURLやHTTPメソッド(GET, POST等)で取得したいリソースとか色々明示されてるけど、GraphQLは同じURLにPOSTでアクセス。その際に渡すjsonの書き方に規則性をもたせてやりたいAPIを実現する
具体的なイメージ
一定の規則性(クエリ)を記載して対象のサーバーにリクエストするとよろしく情報をとってきてくれる。
{
members(name: "test") {
id
name
createdAt
updatedAt
}
ポイントはこんな感じ
- 関数風に指定して引数を渡すことができる
- 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-alpine→tomcat: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な空気があり、わざわざ学習コストをかけ流必要あるのか?とか言われそうなので世の中的に広まってくかはわかりませんが、私はこれ使ってこうと思いまっす