GraphQLに少し詳しくなったあれこれや参考文献
はじめに
先日から改めてGraphQLに入門しました!
というものの、今までもGraphQLを利用して開発をしたことはありました(利用しようと思えば、まあ使えるんですよねこういうものって……)が、最近がっつり使うことになり改めて向き合ってみると、なかなか実装する時に「このフィールドってどのクエリにつければ良いのだっけ」「今のクエリやミューテーションはなぜこんな形になっているのだろう」といった、悩みや疑問が多く出てきました。
改めてがっつりGraphQLの設計思想や利用方法に浸ってみたい、GraphQLらしさを学びたい。
ということでドキュメントや有名ブログ、書籍を読み漁ってみた結果、私なりに感じたGraphQL Wayを備忘録的にまとめてみようと思います。
REST APIとの比較といった王道な特徴の紹介から、実装時に気をつけたいTips(主に未来の私に向けて😌)、ちょっとしたつぶやき感想までを記事にしてみます。
この記事は私見をまとめるというよりも、有名な特徴や設計についての考察をまとめたものとなっております。あまり具体的な実装については出てきません。
最後に参考文献を載せますので、もっとGraphQLを知りたい方はぜひそちらにも目を通していただけると良さそうです。
GraphQLの特徴
網羅するということではなく、GraphQLが公式に推す特徴のうち、自分の中で特に強烈に印象に残ったものを2つ代表で、そちらを中心に掘り下げる形にしてみました。
フロントエンドファースト
Ask for what you need,
get exactly that
必要なものを問い合わせたら、それらが手に入ります(GraphQL公式)
Get many resources
in a single request
1つのリクエストでたくさんのリソースを取得します(GraphQL公式)
好きな時に、好きなものを、好きなだけ。これがGraphQLの核となる設計思想です。
REST API はリソースベースの設計という側面が強いため、データベースの形がそのままエンドポイントになっていくことが多いです。しかし時には、複雑な画面でRecipe
というリソースだけではなく、Recipe
(料理レシピ) リソースに従属するMaterial
(材料)リソースも欲しいこともあるかもしれませんと。この時、リソースベースで綺麗な形に設計された REST API においては、Recipe リソースするエンドポイントで要素を取得した上で、Material リソースのエンドポイントへリクエストし、情報をフロントエンド側でよしなに構築します。このRecipe
とMaterial
の従属関係があまりにも色々な画面で頻発して利用される場合、エンドポイントを1つにする処置が検討されそうですね。
GraphQLのクエリも基本的にはデータベースの形に寄ることが多いですが、クエリの形はより自由です。
GraphQLでは、上記のようなRecipe
とMaterial
を表示する画面があった場合は、それが一発のクエリで取れるように設計することがベストプラクティスとされています。
type RecipeQuery {
id: ID!
name: String!
}
type MaterialQuery {
id: ID!
name: String!
amount: String!
calorie: Number!
}
REST API 的には綺麗に見えますが……
type RecipeQuery {
id: ID!
name: String!
materials: [Material!]!
}
type Material {
id: ID!
name: String!
calorie: Number!
}
こんな形にすればRecipeQuery
を一度リクエストして終わりだから、二度リクエストする手間なくて良いよね。こっちの方がよりGraphql Wayですよ、と、そういう思想です。
さらに、この料理レシピの総カロリーを表示する場所もありそうです。材料配列のカロリーをすべて足し合わせればフロントエンドでも算出できますが
type RecipeQuery {
id: ID!
name: String!
materials: [Material!]!
calorie: Number!
}
GraphQL的にはこうした方が良いみたいです。先ほど「GraphQLのクエリも基本的にはデータベースの形に寄ることが多いですが」と言ったのはこの辺りのお話で、このcalorie
はデータベースのカラムには登録する必要はなさそうですが、フロントエンドの表示に必要なら生やしちゃって良いということです。
GraphQLによるスキーマ設計は画面に優しくあるべき、というアプローチに近しくなります。何度もリクエストを発行して、必要なものを計算で求めて、画面に必要な要素を組み立てるというパフォーマンスやフロントエンドでロジックを持つことの複雑さは、バックエンド側で吸収するべきということですね。
複雑な画面でのバックエンド↔︎フロントエンドのやりとりを少なくするとどうなりそうかというと、気になってくるのが以下です。
- パフォーマンス
- セキュリティ
サーバーサイドのフィールドの構成によっては、N+1問題がめちゃくちゃ多くなりそうなのは、すぐに想像がつきそうです。これはREST APIも同じような問題に当たったりするのですが、GraphQLは公式でもパフォーマンスにふれており、特に注意するよう私たちに教えてくれています。
また、REST APIよりもシビアになりそうだなと思ったものとして、セキュリティが挙げられます。1つのフィールドの中にたくさんのフィールドを含むことができるのがGraphQLの良いところですが、当然そうなれば一回のクエリで実行される処理は多くなりがちです(データベースへのアクセスはもちろんですが、データを利用してフロントエンドに優しいロジックをバックエンドで作ってあげる、なんてこともオンになってくるわけですもんね)。
回数制限だけではなく、クエリの深さという観点で制限をかけることを奨励されており、こちらはGraphQLならではの考慮事項だと感じました。
スキーマ駆動開発と型安全なクエリ
Describe what's possible
with a type system
型システムを通じてできることを説明します(GraphQL公式)
GraphQLはスキーマの定義を元に開発を進めます。フロントエンドはスキーマを元にクエリを書きますが、このクエリはとても表現力が高く、予測可能なレスポンスとして抜群の可読性を発揮します。
この特徴は REST API にはないものと感じたため、2つ目に挙げてみました。
というと、「いやいや、開発されたサーバーのコードを元にスキーマを出力する逆バージョンもありますやん」という方もいらっしゃるかもしれません。
実際私が出会ったGraphQLの現場は、例に漏れずどこもコードファーストで開発→スキーマを出力という逆順でした。
ただ、先に挙げたようにGraphQLの大きな特徴はフロントエンドファーストです。(開発上そういう流れになるのは分からなくはないのですが)バックエンド先行で開発が進むのはある意味ではGraphQLの思想と少し離れるのかなと感じています。
REST APIはリソースベースの設計になりがちで、比較的DBをさわることが多いバックエンドがイニシアチブを持って設計を進める場面が多くあります。GraphQLを利用して開発の場合は、バックエンドとフロントエンドは双方の話し合いによりDSLによる宣言的なスキーマ宣言を取りまとめ、取り決めを元にお互いの開発に入るという開発パターンが合うのかなと感じました。
GraphQLのリクエストはコードベースでこのように書いていくのですが、とてもわかりやすいですよね。
レスポンスがそのままリクエストした形で来てくれるので、コード上でも表現がより豊かになりそうです。
{
recipe {
id
name
materials {
name
calorie
}
calorie
}
}
GraphQLはリクエスト↔︎レスポンスの分かりやすさをとても大切にしています。
また、わかりやすいというやや似た意味では、ドキュメンテーションに関連する特徴について2つの観点を挙げることができます。
1つ目は、スキーマファイルに関するドキュメンテーションをとても大切にしていることです。
クエリの各フィールドやミューテーションの説明はもちろんですが、それだけでなく独自のディレクティブを利用し非奨励になったフィールドをマークすること等もできます。また、GraphiQL
と呼ばれる、GraphQLのクエリを試すためのインタラクティブな開発ツールも簡単に利用し始めることができます。GraphQLのクエリを簡単に試すことができたり、ドキュメントを検索したりすることができます。この検索結果において、ドキュメンテーションされたGraphQLはそのまま仕様書のような形でクエリやフィールド、ミューテーションの説明をしてくれることになります。
2つ目は、エラーハンドリングにおいても開発者にとって分かりやすいエラーを示すことを奨励していることです。
GraphQLは性質上、一部は返すことはできるが一部はエラーになる、というケースが存在します。
この場合レスポンス自体は200が返りますが、data
というレスポンスが入ったフィールドだけでなくerrors
というエラー内容が書かれたものが必ず返ってきます。このエラー内容は、フロントエンド開発者が即座にリクエストを修正できるよう分かりやすくするのが良いとされています(当たり前と言っては当たり前なのですが…)。ちなみに返せるデータが1つもない場合はエラーのステータスコードになって返却されます。
エラーをわかりやすくするというのは当たり前と言えば当たり前ですが……必死に開発しているとおざなりになったり忘れたりしちゃうので頑張りたいところです。
設計パターンTips
以上、GraphQLについてよく知るべく学ぶ中で、私が強く感じた特徴を2つ挙げさせてもらいました。以降は(主に私が開発中に見直しをかけるリスト表示にするべく……笑)気をつけるべきTipsを紹介します。
これらはすでにここまでで出てきたTipsなので、それ以外のより細かいところに目を向けてみました。
- フロントエンドファーストで設計をする
- (可能なら)スキーマファーストで考える
- パフォーマンス(特にN+1)に気をつける
- (パブリックなAPIの場合特に)セキュリティに制限を持たせられないか考える
- ドキュメンテーションする
- 使えるディレクティブがないか考える
- 分かりやすいエラーでフロントエンドに優しくする
クエリは並列実行、ミューテーションは直列実行
つまり、クエリには副作用を持たせないようにしようね、ということです。
クエリ操作には読み取り専用のものをおき、副作用を持たせるものはミューテーションに寄せるのが鉄則です。
ミューテーションはPayloadとInput型を定義する
また、REST APIの場合はあんまり詳しいことが決まっていないような気がするのですが、GraphQLの場合副作用を伴う操作の場合も、更新された対象データをレスポンスとして返すのが奨励されていそうでした。
リクエストにはXXXInput
、レスポンスにはXXXPayload
という型を、ミューテーションに対して一対一で定義するのが良さそうです。
よく見かけるものとして、横着してモデルやドメインの型で返してしまうことがあるのですが、きちんと型を定義して必要分を返したいところですね。特にCreate, Updateあたりの概念は、ドメイン(≒データベース)とPayloadが似たオブジェクトになりがちな運命ではあるのですが、プレゼンテーションレイヤーへのin/outとドメインロジックは分離するべきなので、徹底したいところだと思いました。
(スキーマ駆動ではなくコードベース駆動でスキーマ定義していると、こういう楽に見える実装になりがちなイメージです)
ミューテーションは具体的にする
type Mutation {
updateRecipe(recipe: Episode): Recipe
}
これよりも
type Mutation {
updateRecipeName(name: String!): Recipe
}
これが良いということみたいです。
queryは1つのフィールド内にUIと関係があるパーツをもりもり入れるみたいですが、ミューテーションの場合は何がどういう副作用なのかを明確にした操作名が奨励されるみたいですね。
なかなか諸説ありそうだと思ったりしましたが、たしかに変更対象でないものも一緒くたにして送ると、よく分からない不具合を生み出してしまう可能性があるので、気をつけたいところです。
UI上は閲覧できるフィールドも、場合によってシステムでしか更新しないものやユーザーが更新できるものなど、さまざまな性質のものがあります。
一旦関係ないけどフロントエンドから毎回更新時にシステムでしか更新しないもの(たとえば、 updated_at
とか。笑)も一緒に送らないとなんかエラー出るあるいは変なデータになる、とかめっっっちゃ嫌ですもんね(遭遇したことあるけれど)。
すべてのフィールドがデフォルトでnull許容になる
スキーマ定義において、以下のように書くとすべてのフィールドはnull
を許容します。
GraphQLは基本的に、すべての値は、何らかの理由で取れないことって、ありうるよね、ということで普通に書くとnull
を許容してしまいます。
type RecipeQuery {
id: ID
name: String
materials: [Material]
calorie: Number
}
闇雲にnullを許容し続けるフィールドを定義している場合、リクエストユーザー側としてはnullが帰ってきた時に、それがエラーによるものなのか「ない」というUIパターンがあるのかを、APIを利用する側のユーザーは推し量ることができません(同じような理由で、私は空文字とnullを識別しないデータベースのカラム定義が嫌いです、JavaScript
のhoge?.fuga
という表現も……以下略)。ということで、!
のnull比許容は表現力の一貫としてきちんと利用した方が良い、ということです。
ドメインとロジックを分離する
レイヤーの意識が大切というお話で、スキーマの設計とドメインの設計は分離して疎結合にするのが良いというお話。
こちらは先ほどのPayload
型にモデルやドメインのフィールドは使わないでね、というお話とも被ってきます。
当たり前ですが、将来的に公開しているGraphQLがめちゃくちゃ有名になったので、REST APIバージョンも公開したいなとなった時に便利なのです。レイヤーごとの責務大切に。特にドメイン(突き詰めると難しいけれど)。
感想
最後に何にもならないちょっとしたつぶやきをおいて終わります。
Federationすごい
昨今のマイクロサービス化と似たような雰囲気で、分散型のGraphQLオリジナルアーキテクチャとして出張っているのが、Federationという仕組みです。
上記の公式サイトが最もわかりやすいです。簡単に言いますと、以下のような仕組みです。
- サブグラフと呼ばれる各サービスは独自にGraphQLサーバーを立ち上げている
- 各サブグラフを結合したどでかい1つのGraphQLサーバーを作る
- フロントエンドはサービスの場所を意識することなく、どでかいサーバーのGraphQLスキーマ定義を見てリクエストを送れば良くなる
GraphiQL
が良い例ですが、GraphQLはドキュメントとしてのスキーマはあくまで1つの場所に統合し、ユーザーに1つの場所を通して仕様を理解させたり検索させたりすることにこだわりを感じます。
私がGraphQLをよく知らなかった頃は、スキーマやミューテーションが1つのエンドポイント内にモリモリにある状況がかなり違和感で「サービス違うんだったらエンドポイントを思い切ってhttp://example.com/hoge/graphql
とhttp://example.com/fuga/graphql
に分割した方が良いのでは? クエリとかミューテーションが1つのAPIにくっつきすぎるのダメそう」というとんちんかんなことを考えていたりしました。笑
BFFの概念にも近しいですが、フロントエンドが1つのサービスとドキュメントだけを意識すればOKというところが、新鮮で良いなと思いました。
特にGraphQLはリソースベースではなくUIベースにフィールドを取りに行くので、複数のドメインに対して1つでリクエストで完結させることができるとなると、GraphQL設計思想的にとても美しいエンドポイントが出来上がりそうかも。
また、バックエンド側のモノリスを解決する意味でもとても良さそうです。
RSCとの相性問題
調べている途中からあれ?あれあれ?という感じで思っていたことなのですが……どうやらGraphQLとRSC(React Server Component)は相性が悪そうですね。
RSCは「そのコンポーネントで必要なものはそのコンポーネントが自由に取ってね」という概念です。リクエストは複数回あって当然です。
それに対して、GraphQLは1つのページにおいて1つのリクエストで完結させるべく色々とやる的な概念です。なんか水と油感あります。
さらに言えば、GraphQLのスキーマがやっていることは、RSCが各コンポーネントでやっていることとかなり似ているんですよね。
(GraphQLクライアントとして一強を誇るapolloもサーバーコンポーネントでは動かない様子)
このあたりの話は気になってググってみまして、やはり同じ感想をお持ちの方がかなりいました。安心しました。
合わせ方としては、「Next.jsのPage Router や Remix SPA モード × GraphQL」「Next.js の App Router × REST API」という形になりそうですね。Next.jsのApp Routerがなんとなく推されているかなという今、新しくGraphQLをという選定自体はやや向かい風になっていると思います。
参考文献
ここまで読んでいただき、ありがとうございました。
参考文献は、私もすべてにおいて「これいいよ」の又聞きですが、超おすすめです!以下の読み物のおかげで、非常にGraphQL全体の解像度が上がりましたのでぜひ参考にしてください。
- GraphQL公式
- 「GraphQLスキーマ設計ガイド 改訂第2版」
- GraphQLスキーマ設計の勘所
- GraphQLを導入する時に考えておいたほうが良いこと
GraphQLなかなか奥が深くて良いですね。がっつりさわる機会が今後増えそうなので、基礎的な設計概念を叩き込んでおくことにより、私のなかの処理速度がちょっと上がるかもと思いました。
Discussion