RESTで溺れる者がGraphQLを掴む
あまりマジョリティになっていく印象がないGraphQLですが、RESTを使い続けていて修正や拡張を重ねる場面でジワジワ辛さを感じる点もあり一度逃げて試しておこうかなぁという主旨です。
サーバサイドKotlinで試してみようと思ったものの、あまり情報が多く無いため記録として残しておきます。
リポジトリは以下
github.com
GraphQLの超ざっくりイメージ
- エンドポイントは単一である
- 型システム持ち
- クエリ言語とスキーマ言語の組み合わせ
- 単一リクエストで関連するオブジェクトを一括で取得できる
- 取得したいフィールドやオブジェクトに対してそれぞれ引数パラメータを渡すことができる
- 引数には型を与えることができ、デフォルト型に加えてカスタムした型を用意することもできる
- 認証も単一にできる
- Fragmentとして再利用可能な共通オブジェクトを作っておける
他にも多くの特徴があり、当然課題解決として銀の弾丸ではないのは認識した上でデメリットについては一旦触れないものとして触っていく。
準備
サーバサイドはKotlinで用意する。
公式ページに掲載されているライブラリgraphql-kotlinを利用する
調べたところコアライブラリであるgraphql-java以外はgraphql-javaのメンテナンスから独立してgraphql-java-kickstart
で開発が続けられている。SpringBoot+Kotlinの環境ではgroupidはcom.graphql-java
ではなくcom.graphql-java-kickstart
を使うのが良さそう。
参考:GraphQL Java
dependencies
graphql-spring-boot-starter
SpringBootアプリケーションをGraphQLサーバーに変換するgraphiql-spring-boot-starter
GraphiQLスキーマのイントロスペクションとクエリのデバッグ
build.gradle.kts
implementation ("com.graphql-java-kickstart:graphql-spring-boot-starter:7.0.1") runtimeOnly ("com.graphql-java-kickstart:altair-spring-boot-starter:7.0.1") runtimeOnly("com.graphql-java-kickstart:graphiql-spring-boot-starter:7.0.1") runtimeOnly("com.graphql-java-kickstart:voyager-spring-boot-starter:7.0.1") implementation ("com.graphql-java-kickstart:graphql-java-tools:6.0.2")
Schema
.graphqls
ファイルを作成しスキーマ定義する。
Intellijでファイル作成時に選択可能なGraphQL File
だと拡張子が .graphql
で生成されスキーマ定義として認識されないので注意が必要。resolverで作成したfunctionをtype Queryとして定義することでqueryとして呼び出すことが可能になる。
なお、GraphQLではデータ取得はQuery,サーバサイドのデータ変更はMutationと呼ぶ。
今回試すquery以外は割愛します。
models.graphqls
type Query { getBooks: [Book] getBook(id: String!): Book getUsers: [User] }
User.graphqls
type User { id: ID! full_name: String! first_name: String! last_name: String! age: Int! employee_number: String! email: String! organizations: [Organization] }
Organization.graphqls
type Organization { id: ID! userId: ID! code: String! name: String! }
Data Classes
GraphQLのtype定義したスキーマにマッピングするdata classを定義する。
ポイントとしては、入れ子にするOrganizationエンティティはUser data classに含めないこと。
User.kt
data class User( val id: String, val full_name: String, val first_name: String, val last_name: String, val age: Int, val employee_number: String, val email: String )
Organization.kt
data class Organization( val id: String, val userId: String, val code: String, val name: String )
Root Resolvers
今回はqueryのみのため、GraphQLQueryResolverインターフェースを実装するクラスを用意します。
functionをgetXxxx のように作成することでtype Queryの定義と合わせる必要がある。
UserQueryResolver.kt
@Component class UserQueryResolver(): GraphQLQueryResolver { val userRepo = UserRepo() fun getUsers(): List<User> { return userRepo.findAll() } }
Resolvers
今回はUserに外部キーとして持つ想定のOrganizationのマッピング用としてGraphQLResolverインターフェースを実装するクラスを用意します。
UserクラスをDataClassに指定し、OrganizationのListを取得する処理を実装。
UserResolver.kt
@Component class UserResolver: GraphQLResolver<User> { val organizationRepo = OrganizationRepo() fun getOrganizations(user: User): List<Organization>{ return organizationRepo.findByUserId(user.id) } }
ローカル実行
GraphQL Voyager
voyagerを起動することで定義したSchemaをビジュアル化してくれる。
URLはapplication.ymlに記述します。
GraphiQL
クエリの実行はGraphiQLを使ってブラウザ経由で確認できる。
同じくURLはapplication.ymlに記述します。
クエリ実行
Bookの一覧を取得
# query { getBooks{ name } } # response { "data": { "getBooks": [ { "name": "name10" }, { "name": "name20" }, { "name": "name30" }, { "name": "name40" }, { "name": "name50" } ] } }
ID指定で単一のBookを取得
# query { getBook(id: "100") { id name } } # response { "data": { "getBook": { "id": "100", "name": "bookName100" } } }
Userの一覧(フルネームのみ)を取得
# query { getUsers{ full_name } } # response { "data": { "getUsers": [ { "full_name": "鈴木太郎" }, { "full_name": "田中一郎" } ] } }
Userの一覧(項目追加)を取得
# query { getUsers{ id full_name first_name employee_number organizations { userId name } } } # response { "data": { "getUsers": [ { "id": "1", "full_name": "鈴木太郎", "first_name": "太郎", "employee_number": "1000", "organizations": [ { "userId": "1", "name": "情報システム部" }, { "userId": "1", "name": "新規事業部" } ] }, { "id": "2", "full_name": "田中一郎", "first_name": "一郎", "employee_number": "2000", "organizations": [ { "userId": "2", "name": "クラウドサービス部" } ] } ] } }
複数クエリを一括実行
# query { getBooks{ name } getBook(id: "10") { id name } } # response { "data": { "getBooks": [ { "name": "name10" }, { "name": "name20" }, { "name": "name30" }, { "name": "name40" }, { "name": "name50" } ], "getBook": { "id": "10", "name": "bookName10" } } }
雑感まとめ
今回は単純なqueryの実行、複数クエリの一括発行、複数エンティティの入れ子構造の取得などを試しました。
- 先の複数クエリの一括実行のサンプルは有用性の薄い情報でしたが、1画面で必要な情報のデータソースが外部に点在しているようなシステムの場合は、クライアントサイドでの恩恵は大いにありそう。必要なフィールドに絞ったレスポンスを得られるのもシンプルに良い。
- 一方で、あまり一括にすぎるとエラー処理やパフォーマンス面では混乱が起きそうな予感もしており、そのあたりの感触は運用してみて初めて解るツラミがあるかもしれない。
- 調べきれていない点も多い。クラウドインフラのオブジェクトストレージなら問題無さそうだがバイナリデータの扱いはどうなるんだろうか?
- RESTのような規約ではなくスキーマとクエリ仕様がカッチリ決められているため、実装の揺れは少なそうなのは良い感じ。
- typeで外部キーまで全て定義してしまうと折角の柔軟性のあるリクエストが阻害される予感がした。queryを一括で複数発行できる強みを活かすならtypeを別定義として適度な疎結合を保つのが良さそう。
参考: Spring Boot + GraphQLでAPIを作成してみよう! - Yahoo! JAPAN Tech Blog