- Published on
GraphQLを導入してみて得た知見と雑感。GraphQLはタイタニックの救命ボードになりえるかも
- Authors
- ジャバ・ザ・ハットリ
GraphQL は実装内容に合えばタイタニックの救命ボードのように混沌から救い出してくれる。だからと言って全てのプロジェクトがタイタニックな訳ではないので、使い所が合わなければそんな救命ボードにもあまり意味は無い、という話。
先日、個人開発して公開したプロジェクト「node-node-node」は GraphQL を使っていて、このプロジェクト内容に対しては最高の親和性を発揮してくれた。
GraphQL のメリットを一言で言えば「クライアント=サーバー間での複雑なトランザクション処理の全てを GraphQL が吸収してくれる」ということに尽きる。ややこしい技術の詳細を書いたところでメリットはこれ以外に無い。
/users や/posts というそれぞれのエンドポイントにリクエストを投げていたのが RESTful。
GraphQL にするとエンドポイントを気にすることなく「これとこれが欲しい」という感じで投げれば GraphQL が理解して返してくれる。
なぜ個人開発のプロジェクトに 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
}
Notification {
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。
「node-node-node(ノード ノード ノード)」
https://node-node-node.netlify.com/