GraphQLを導入してみて得た知見と雑感。GraphQLはタイタニックの救命ボードになりえるかも

GraphQLは実装内容に合えばタイタニックの救命ボードのように混沌から救い出してくれる。だからと言って全てのプロジェクトがタイタニックな訳ではないので、使い所が合わなければそんな救命ボードにもあまり意味は無い、という話。

先日、個人開発して公開したプロジェクト「node-node-node」のバックエンドはRails APIにGraphQLを使っていて、このプロジェクト内容に対しては最高の親和性を発揮してくれた。

GraphQLのメリットを一言で言えば「クライアント=サーバー間での複雑なトランザクション処理の全てをGraphQLが吸収してくれる」ということに尽きる。ややこしい技術の詳細を書いたところでメリットはこれ以外に無い。

/usersや/postsというそれぞれのエンドポイントにリクエストを投げていたのがRESTful。
1494257479002-RestfulServer.png

GraphQLにするとエンドポイントを気にすることなく「これとこれが欲しい」という感じで投げればGraphQLが理解してレスポンスを返してくれる。
1494257483003-GraphQLServer.png

なぜ個人開発のプロジェクトにGraphQLが大ハマりしたかを書く。このプロジェクト実装のポイントはRESTで言う複雑なGETにあった。
親子関係が何回も続く階層構造になったノードがサーバー側のDBにあって、クライアントは適切なノード情報をサーバーから取り寄せる必要がある。全てのノードは以下のようになっていて、親子関係がずっと続く。

ジジイ、ババア、ジジイ、ババア
|
親、親、親、親
|
本人、兄弟、兄弟、兄弟
|
子、子、子、子
|
孫、孫、孫

できるだけサクサク感を出すためにクライアントはユーザが今アクセスしているノードから次にクリックするかもしれないノードの情報を非同期でサーバーから集めている。ユーザーがクリックするころには既に裏側で集めておいた情報を出すだけ、とした。
この設計の場合、ユーザの置かれた状況によって、裏側で集めるべきノードが異なってくる。

例えば上のジジイ側から本人へ下りてきたユーザの場合、裏側で集めるべきノード情報は子や孫だけになる。なぜならジジイ側から下りてくる過程で既に本人より上の親ノードの情報は持っているから。
逆に孫側から上ってきたユーザーの場合は集めるべきは親やジジイになる。上がってくる過程で子ノード以下の情報は持っていることになるから。
兄弟の兄側から来た場合は末っ子側のノード情報が必要になる。

で、これを丁寧にRESTfulで書いていくとすごい数のエンドポイントになっていった。

ざっくりと親から、子から、兄弟からと3パターンだけ示したが、それ以外にもLikeした情報がある場合、無い場合、とかそれぞれのエンドポイントに独自の情報の有り無しを加えていったのでパラメータまで煩雑な状態となっていた。私はDHHのルーティングを支持しているのでCRUDアクションindex、show、new、edit、create、update、destroyのみを使うようにして、これ以外が必要な時は新しいコントローラーを作っていた。するとスゲー数のエンドポイントになって、Reactで作ったクライアントは状況に合わせてエンドポイントを使い分ける必要があって、どれがどれか混乱してきて「キー!」となった。

で、これをGraphQLに移行することでクライアント=サーバー間にあったややこしい部分をクエリ言語が全て吸収してくれた。クライアントからは欲しい情報を書いてリクエストすれば、それをそのままサーバーが返してくる仕組み。
そしてエンドポイントはたったひとつに、バージョンも何もない、ただひとつ!

例えば親側から来たユーザー向けに子情報が欲しい時のリクエストがこれ。要はchildrenをよこせ、と。

# client => server へのリクエスト(query)

query {
  Nodes {
    id
    message
    children {
      id
      message
    }
  }
}

するとサーバーからのレスポンスはこうなる

# client <= server レスポンス
 "data": {
    “Nodes”: {
      “id”: 3,
      “message”: “Hi I’m CEO.”,
      "children": [
        {
          “id”: 4,
          “message”: “Hi, I’m CTO.”
        }
      ]
    }
  }

つまりリクエストとまったく同じ形でレスポンスが返ってくる。
親情報が欲しければただリクエストにparentをつければいいだけ。めちゃ楽ちん。

ページネーションはこんな感じに実装した。例えばたくさん子ノードがあって3ページ目が欲しい場合はchildren(page: 3)とするように。

query {
  Nodes {
    id
    children(page: 3) {
      id
      message
    }
  }
}

ほとんどのウェブサイトにおいてクライアントが欲しい情報が1種類ではない。記事情報とユーザー情報とそのユーザーへの通知情報、となったりする。RESTfulの場合はそれぞれを

GET api/v1/users
GET api/v1/posts
GET api/v1/notifications

という感じで3回リクエストしてそれぞれの情報を集める。

GraphQLの場合は全てを1発のリクエストにして放り込める。

例えばこんな感じで、PostsとUsersとNotificationsをまとめてよこせよ、と。

query {
  Posts(page: 0) {
    id
    message
  }
  User {
    id
    name
  }
  Notifications {
    id
    status
  }
}

するとこの型のまま記事とユーザーと通知の情報の入ったレスポンスがひとかたまりで得られる。クライアントからすれば欲しい時に指定した欲しい情報だけが取れる。(最初これを実装して本当にその通りにレスポンスが返ってきた時になぜが声出して笑った。)

限りなく薄いGraphQLが理想

ある記事でRailsにGraphQLを入れた場合、MVCで言うところのMVC全てがGraphQLに集約されてしまう、みたいなのを読んだ。これ明らかにおかしい。初めてGraphQLに出会った際には「これスゲー!」と興奮してなんでもGraphQLに入れ込みたい気持ちは分かるが、そんなことしろなんて公式サイトのどこ見ても書いてない。ファットコントローラーと同じくファットGraphQLはメンテナンス不能でたちが悪い。

GraphQLの作者Lee Byron氏も言うように「なんでもできるGraphQLだが、GraphQLはできるだけ薄く保つのがいい」と。GraphQLの責務と役割はquery(RESTfulのGET)とmutation(POSTもしくはPATCH)だけであって、それ以外は無い。したがってresolve ->(obj, args, ctx)の中にロジックがあったら、その責務はどこが持つべきか考えてそこに移すべき。

GraphQLが遅いって?それ設計の問題では

GraphQLのディスり記事にスピードに関する言及が多かった。例えば「まともにキャッシュが効かねー」とか。これは間違い。もう既に数々のキャッシュ方法がクライアント、サーバー共に提案されている。実際、前述のnode-node-nodeにおいてもキャシュを入れている。大きめのqueryでキャッシュ無しだと300msかかっているレスポンスがキャッシュで3msとかで返すことができている。そこはキャッシュ使うので当たり前なんだけど、スピードを議論するのであればキャッシュどうこうではなく、クライアントが欲しい情報を何msで取れるのか、とすべき。GraphQLのあるクエリがちょっと遅かったとしてもリクエストを1発でまとめて入れてるのでRESTfulで5、6回繰り返して「はいできました」となるまでにかかる時間と比較して必ずしも遅いとはならない。

N+1問題

GraphQLはN+1問題を誘発する、というディスり記事もあった。もうこれはハッキリしていてN+1問題なんて出てる時点でそれをクエリ言語のせいにするのはお門違い。確かに気付かないところでN+1を含むおそれはあるが、百歩譲ってもそれGraphQLじゃなくて、あなたの設計の問題ですから、と。

GraphQLはちゃんとハマればハッピー

本記事の冒頭でGraphQLをタイタニックの救命ボートに例えたが、既存のプロジェクトの船底に穴が空いて沈みかけてるのに船の上で上品なお食事を楽しんでいる人達みたいに気付いていないだけ、というのがよくある。熱い技術なのでGraphQLが現行のプロジェクトに合うかどうかぐらいは確かめた方がいいと思う。

以上がGraphQLを使ってみて得た雑感でした。

これがGraphQLで実装したプロジェクト「エンジニアの集合知をノードグラフで図解するSNS」です。よろしければぜひお試しください!

エンジニアの集合知をノードグラフで図解するSNS。
「node-node-node(ノード ノード ノード)」
 https://www.node-node-node.com/

良くても悪くても賞賛も批評も罵りも罵倒も、どんなコメントでもお待ちしています。(反応無しが一番辛い。。。)

49contribution

graphqlのversioningとか、どうやって対外的に公開していくのかが気になりました。

例えば、REST APIであればエンドポイント毎に仕様があってリンゴに関する情報を取りたいユーザーはリンゴエンドポイントの仕様を把握していれば良いのですが、graphqlの場合はリンゴエンドポイントがそもそも存在しないので、APIのversionが変わった際に「自分が利用しているリンゴ取得クエリに影響が無いかどうか」を確認する必要があるのかなと考えています。
もしそうだった場合は全ユーザーにテストをしてもらう必要があるのかなぁとか考えています。

この記事を読んでgraphqlスゲーと思い、今仕事でやっている業務アプリに取り入れられないかなふと思ったのですが、その業務アプリを利活用しているアクターは数が多く把握しきれないことと、利活用ユーザーがどういうクエリを投げているのかを利活用される側は知らない為、仮にgraphql移行後に改修を行いたい場合、影響範囲を見積もることは実質的には不可能な気がしました。
改修はRESTでも大変なのですが、影響範囲を確定できないのではなかなと。

何かスマートなversioningとか移行の施策とかはあるのでしょうか。すみません、RESTですらあまり運用経験は長くないので無知なのですが。。

2918contribution

k-shimojiさん、ご質問ありがとうございます。
GraphQLにはRESTであるようなバージョンという発想は無いです。私も最初REST脳だったので理解ができなかったのですが、こう言われて気が付きました。
「APIになぜv1やらv2とバージョンがあるのか?」答えは「破壊的な変更があって、その度にバージョンを上げる必要があるから」と。
つまりは「GraphQLにすれば破壊的な変更は無いのでバージョニングする必要がない」となります。
クライアントが欲しい情報をリクエストして、そのままレスポンスを返すGraphQLでは基本的に変更をかけても破壊的ではないです。今まであったfieldを削除するとかは破壊的かもしれませんがGraphQLではadd-only approachを推奨しています。つまり加えるのみ。

なので今回作ったrailsのルートはBest Practice にのっとり
graphql POST /api/v1/graphql
とかではなくて
graphql POST /graphql
としています。清々しいです。対外的にも「バージョンは無いからな」と宣言することにもなります。
今までRESTfulでAPIのバージョンを上げることの痛みをずっとガマンしてきましたので、このversionless APIという発想はとても気に入っております。ちなみにGitHubのGraphQL APIはv4となってますが、以前のバージョン(v1,v2,v3)がRESTでv4がGraphQLでずっとこのままv4という理解です。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.