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な空気があり、わざわざ学習コストをかけ流必要あるのか?とか言われそうなので世の中的に広まってくかはわかりませんが、私はこれ使ってこうと思いまっす