今年GitHubがGraphQL APIを正式公開したあたりから、GraphQLが去年とかに比べちょっと流行り始めたように感じる。idobataがGraphQL APIを公開したり、Kibelaも公開APIをGraphQLで作ることを宣言している。
利用者側からすると使えるインターフェースの中から必要なものを調べて使うだけなのであまり考えることはないのだが、自分がAPIを提供する立場になると話は変わってくる。REST APIとGraphQL APIはどちらかがもう一方のスーパーセットという風にはなっておらず、どちらかを選択すると何かを捨てることになるので、要件に応じてどちらを選ぶのが総合的に幸せなのか考える必要がある。
以前趣味でGitHub連携のあるサービスを作っており、それを最近GraphQL API v4を使うように移行し、そこでついでにそのサービスのGraphQL APIを書いてみたりした結果GraphQLができること・できないことが少し見えてきたので、僕の現在の「GraphQLはREST APIに比べどういう用途に向いているか」についての考えをまとめておく。
REST APIと比較したGraphQL
この比較では、REST APIがJSON Schemaと同時に使われうることも想定して書く。理由は、GraphQLが解決している問題の一部をREST APIで実現するためにJSON Schemaが使われることがしばしばあり、逆にGraphQLでは元々それに近い機能がありまず使わないと思うので、現実世界の問題を解く上ではそれらをセットにして比較した方がいいと考えたため。
なお、対比する上で本質的な部分となる「仕様上の問題」と、時間が解決しうる(が現実世界では当然考慮が必要な)「現在のエコシステムの問題」は分けて記述する。エコシステムに関しては筆者の都合でサーバー側はgraphql-rubyを念頭に置いている。
GraphQLにしても解決できない問題
仕様上の問題
- GraphQLはありとあらゆるリソースをリクエスト一発で取得できる夢の技術ではない
- 正直触る前は大体そういうイメージだった
- 例えばページネーションなしに1種類のリソースを6000個取得しようとするとレスポンスに1分かかりリバースプロキシ(かunicorn, rack-timeout等)でタイムアウトになるREST APIが世の中には実在したのだけど、それをそのままGraphQLのクエリで再現したとして確実に同じ時間がかかる。つまりページネーションは確実に必要で、クライアント側でそのリソースに関してループを回す必要があり、そのリソースに関して何度かリクエストが必要になる。
- 単に1つのリソースをページネーションしないといけない場合だけでなく、N個あるリソースにそれぞれM個リソースがネストしていてぞれそれにorderが必要な場合、これは確実にN+1回クエリが必要なわけだけど、そうやって裏側で非効率なクエリが走るようなクエリを投げてしまうと一回のリクエストに時間がかかりすぎてタイムアウトするリスクがあるので、ある程度は分けないといけない。実際僕もGitHubのGrpahQL APIを叩いているとリトライをしても結構タイムアウトを見た。
- 後述するようにリクエスト数は減ることは多いが、必ず1回にできる銀の弾丸ではないという主張
現在のエコシステムの問題
- 現時点ではAPIクライアントを自動生成できるライブラリは限られており、アプリ側にいちいち長いクエリを書く必要がある
- GitHubがAPIをGraphQL化したモチベーションの1つに“We wanted to generate clients instead of manually supplying patches to our Octokit suite”というのがあるが、いまだにOctokitは自動生成されてない。
- もともとGtiHub API v3で採用されていたHypermedia APIにもAPIクライアントの実装の自動化というモチベーションが多少はあったんじゃないかと思っているが、それを活用するのは人類には難しすぎたんじゃないかという気がする。
- 一方でGraphQLだと自動生成に必要そうな情報がおおむねスキーマから取れるので、やれば割とうまくいくと思う
- awesome-graphqlを眺めると、npmのgraphql-auto-mutationは割とそれっぽい。
GraphQLにすると困る問題
仕様上の問題
クエリをパースしないとキャッシュの可否を判定できないため、HTTPキャッシュが難しいREST APIであれば、同じとみなせるGETリクエストをVarnish等でキャッシュすることが容易かつ効率的にできるが、GraphQLだとリクエストボディのJSONをパースし、その中に入っているqueryをパースし、そこにmutationがあるかどうかをチェックする必要がある。ワークアラウンドとしてquery fieldが生えてるエンドポイントとmutation fieldが生えてるものを分ける等が考えられるが、エンドポイントを分ける(REST APIに近づける)ほど当然キャッシュのコントロールがしやすくなるわけで、HTTPキャッシュがないと困る用途には向かないと思う。- 追記: id:yamitzky さんのコメントで知りましたが、
GET /graphql?query=...
といった形でのリクエストも仕様上想定しているため、これは誤解のようです。graphql-rubyのgeneratorだと生えないので勘違いしていました。
- HTTPのメソッドやステータスコードによる挙動の予測ができなくなる
- queryとmutationしかないということは、HTTPメソッドのGETかPOSTしかない状態に等しく、mutationの中でそれがリソースの追加・更新・削除のうちどれなのかを表現する方法は別に仕様レベルでは標準化されておらず、実装した本人以外から見たら挙動が予測しにくくなる。
- クエリの結果がエラーになっても大体
200 OK
が返ってくる。(OKとは)- 他にもGitHubの場合
502 Gad Gateway: This may be the result of a timeout, or it could be a GitHub bug
といったエラーが結構頻繁に返ってくるんだけど、timeoutの場合はリトライしたいしGitHub bugならリトライすべきではないのでこれはHTTPステータスコードで区別して表現してほしい。本当はどちらも(バグを含む何かが原因の)タイムアウトなのかなあ(そのうちサポートに確認する)。 - errorオブジェクトのルールをちゃんと決めて実装すれば解決できるけど、このあたりに標準的な仕様やガイドラインが存在しない結果そういうレスポンスを生んでしまうのは問題だと思う。
- 他にもGitHubの場合
- 必要なfieldを必ず明示しなければいけないので、自動生成しない限りはAPIクライアントを書くのに必要なコードの文字数・行数は増えそう
- 後述するようにIDEのGraphiQLのアシストがあるのでそこまで大変ではない
現在のエコシステムの問題
- GraphQL Proを使わないとモニタリングが難しい
- そこまで高くはない($900/year)ので仕事でやってるなら普通に金払えばいいんだけど、NewRelicとかで詳しくモニタリングしたかったらGraphQL::Proを使う必要がある
- graphql-rubyとかにはinstrumentationの仕組みがあるので、まあ困ったら自分で実装することは可能
- Railsで使ったらMVCのレールのうちVCから割と外れる
- N+1クエリの解決方法がいつもと違う感じになる
- 普通はActiveRecordでSQLのAST(Arel)を組み立て、そこにこのリソースをeager loadingするよという情報を埋め込むことで
ActiveRecord::Associations::Preloader
やActiveRecord::Associations::JoinDependency
になんとかさせるんだけど、これは使わなくなる - かわりに、こういう感じでクエリのトラバース中に必要なIDを集めておいて、最後にgraphql-batchのミドルウェアを通してまとめてクエリすることになる。ネストしたリソースをクエリしてくるのに必要な情報を、親のリソースの一覧を参照することで取ってこれる限りはどうにかできるので、REST APIで問題にならないようなリクエスト(しかできないようにスキーマを制限した場合)ならGraphQLでも問題にならないような気がしている。しかし、例えばコントローラーから使うことを想定してN+1を解決するpreloaderを独自に書いていた場合は使えなくなる可能性があると思う。
- graphql-batch相当のライブラリがない言語でやる場合、特にそれが静的型付け言語とかだとちょっと面倒かもしれない
- 普通はActiveRecordでSQLのAST(Arel)を組み立て、そこにこのリソースをeager loadingするよという情報を埋め込むことで
- graphql.jsを使う場合はFacebookのBSD+Patentsライセンスに同意する必要がある
どちらでも大差のない好みの問題
仕様上の問題
- 「REST APIのバージョン管理」 vs 「GraphQLの
@deprecated
」- GraphQLのサイトにEvolve your API without versionsと書かれているが、個人的にはあまりこれが優れている点だと感じない。
/v1
や/v2
みたいなバージョンを更新していくかわりにフィールドに@deprecated
をつけていくと更新の粒度は細かくできるが、例えばStringだったfieldを同じ名前でObjectにしようとすると、REST APIなら新しいバージョンを1つ生やせば済むが、GraphQLの場合は同じ名前空間でやらないといけないので1度別のfieldを用意してそちらに移行し元のfieldを直すという2ステップ必要になる。どっちが楽かはどう変更していきたいかによる。 - そもそも普通にアプリを書いているとRundeckみたいにAPIのバージョンをバシバシ上げる必要ってそんなに感じなくて、新たにfieldやエンドポイントを足すような後方互換性のある変更が多く、バージョンを上げるとしたらそれこそREST API→GraphQLくらい全体的に大きな変更がないとやらない気がしていて、もともとこれがそんなに問題ではない
- GraphQLのサイトにEvolve your API without versionsと書かれているが、個人的にはあまりこれが優れている点だと感じない。
現在のエコシステムの問題
- 「JSON Schema」 vs 「GraphQLの型」
- ドキュメントの自動生成のしやすさ
- ユーザーのパラメータ・レスポンスのプロパティにおける型安全性
- GitHub APIのGraphQL化のモチベーションにWe wanted assurances of type-safety for user-supplied parametersがあげられているが、これも別にJSON Schema書いてそれをユーザー入力のバリデーションに使えばいい話で、別に必ずしもGraphQLの仕様が解決する話ではなく、単にRESTの時にサボっていただけと見ることができる
GraphQLの方がより良く解決している問題
仕様上の問題
- いわゆるfields paramよりもインターフェースがより柔軟で記述力も高い
- fields paramというのは
?fields=id,name,..
みたいに返すfieldを指定するパラメータのことを言っている。cookpad/garageにこういうのがあるのだが、miyagawaさん曰くこれはgraphQL になる前の Facebook graph API のやつをまねたらしいので、ちゃんとGraphQLになったものがより洗練されてるのは頷ける話 - GraphQLのクエリは普通改行するが、スペース区切りでも書けるし、あまりネストしていないようなケースでそうした場合はリクエストする側のコードの見た目はどちらもあまり変わらない(見やすい)感じになる。
- fields paramというのは
- APIのリクエスト数やround trip timeを減らしやすくなる
- 余計なfieldのリクエストが減りやすい
- いちいち指定する面倒くささとのトレードオフになるが、仕様上使うfieldを記述することが強制されるので、(そもそも強制されていない、省略可能な)fields paramに比べたら使うfieldはちゃんとメンテされる傾向になりやすいと思う。
- GitHubのIt seemed like our responses simultaneously sent too much data and didn’t include data that consumers neededという問題意識は割とうまく解決されるんじゃないかという感じがする
- 各fieldをクライアントが使っていないことを明示できるので、fieldの利用状況を調べやすい
現在のエコシステムの問題
- クライアントキャッシュを実装するためのGlobally Unique IDsなどガイドラインが示されており、実際にRelayのようなそれを念頭に置き活用するフレームワークが存在する
- クエリのIDE的な機能を持つGraphiQLに型がちゃんと活用されている
- そのため、毎回必要なfieldを指定しないといけない割には、クエリを書くのはそこまで苦痛ではない
- 一方でGraphiQLはいろいろキーバインドが潰されてるので普通のテキストのエディットはとてもやりにくい
GraphQLではないと解決できない問題
- 思いつかなかった
まとめ
GraphQLを使う場合の前提条件として、HTTPキャッシュを使わないケース*1である必要があり、また現時点だとGraphQL Proに$900/yearを払うかAPIの詳細なモニタリングを諦める必要がある。
その上で、サーバー側に型の記述を強制しクライアント側にfieldの記述を強制することにより、以下の例のように双方が幸せになると判断した場合は好みに応じて使えばいいと思う。REST APIかGraphQLのどちらかを使わないとすごく困るという状況は上記の前提以外はあまりなさそう。