algonote

There's More Than One Way To Do It

GraphQL採用アンチパターン

GraphQLは銀の弾丸か

前口上

Webやアプリ開発において、サーバー間やサーバークライアント間でデータを受け渡す方式には色々なものがあります。

古くはSOAPやXML、最近のWEB APIはほとんどの場合JSONのREST API、マイクロサービスならProtocol BuffersでgRPCが多いところでしょうか。

GraphQLはFacebookによって開発されたAPI用の言語です。一般に少ないリクエスト回数で柔軟にデータを取得できるのが利点とされています。一方で、使い所を選ぶツールでもあるので今回取り上げてみます。

GraphQLの基本構造

まずJSON API とGraphQLを比較してみましょう

JSON REST APIの例

GET /posts/1

request params

なし

response

{
  "user": {
    "name": "user1"
  }
  "title": "title1",
  "body": "body1"
}

PATCH /posts/1

request params

{
  "user_id": 123,
  "title": "title1",
  "body": "body1"
}

response

{
  "status": "success",
  "data": {
    "user_id": 123,
    "title": "title1",
    "body": "body1"
  }
}

GraphQLの例

Query

request

query {
  posts(id: 1) {
    user {
      name
    }
    title
    body
  }
}

response

{
  "data": {
    "posts": [
      {
        "user": {
          "name": "user1"
        }
        "title": "title1",
        "body": "body1"
      }
    ]
  }
}

Mutation

request

mutation {
  updatePost(id: 1, title: "title1",  body: "body1") {
    user {
      name
    }
    title
    body
  }
}

response

{
  "data": {
    "updatePost": {
      {
        "user": {
          "name": "user1"
        }
        "title": "title1",
        "body": "body1"
      }
    }
  }
}

簡単な比較

REST APIはPOST, GET, PUT, PATCH, DELETEといったHTTP verbの種類がありますが、GraphQLは読み込み相当のqueryと書き込み相当のmutationの2つで整理されています。HTTP上で投げるGraphQLは内部的にPOST限定のjsonリクエストとして扱われます。

通常のJSON APIではリクエスト側はレスポンスの仕様を細かく変えられないことが多いのですが、GraphQLでは欲しいフィールドを指定することができます。GraphQLというように連結リスト的というか事前にスキーマを定義しておけば、関連するモデルを何段でも引っ張ってくることができ、ここがGraphQLが1リクエストで必要なデータをいっぺんに取得できると言われる部分です。

JSONと同じくテキストデータではあるのでバイナリデータを送るにはbase64などでエンコードして送る必要があります。

エラーツール、監視ツールとの相性を考えずに採用してしまう

GraphQLはREST APIとは違うパラダイムを採用しています。エラーツールとの相性を考えないと全てのエラーが POST /graphqlで起きたGraphQLErrorとして扱われてしまいます。言語のライブラリーで個別の設定による分解が必要ということです。

また、GraphQLはグラフをどんどん深く辿っていけてしまうのでリクエスト側依存というか重いリクエストを投げられた時にサーバー側の負荷調整が難しいことがあります。REST APIでリソースごとにリクエストを飛ばしていたものが一つにまとまってくるので時間当たりのリクエスト数のようなもので制限しにくいです。

クエリーの複雑度をはかるQuery complexityやストアドプロシージャ相当のPersisted Queriesが使えますがREST APIとは違ったノウハウが必要です。

バックエンドヘビーなチームで採用してしまう

GraphQLの利点でよく言われるのはクライアントがサーバー側の変更を待たずに画面を柔軟に変えられる点でしょうか。あらかじめ取り決めをしておいてサーバー側でUIを制御できるようにするServer-Driven UIとは真逆の仕組みです。

GraphQLはある種ORMのレイヤーがバックエンドからクライアントに移る側面があります。サーバー側でロジックを吸収できないのでチームの構成メンバーの内バックエンド側が多いチームで採用するとクライアント側の実装待ちが増え、開発速度が上がらない可能性があります。

また、iOSやAndroid(やSPA)があるアプリでGraphQLをReactNativeやFlutter, Kotlin Multiplatformのようなクロスプラットフォームツールを使用せずに使ってしまうと、当然ながらiOSとAndroidで二重に処理を書かなければいけなくなります。REST APIで複雑な処理をバックエンドが吸収していた部分をクライアント側に出すことでトータル幸せになるかは考えたほうが良いです。

素のテーブルを何でもGraphQLに繋いでしまう

GraphQLにはGraphileやHasuraのようなノーコードよりに使うユースケースがあります。立ち上がり期のバックエンドの開発を省くためにGraphQLを使う。

Firebaseより手間ではあるのですが、PostgreSQLのデータベースからAPIをはやす仕組みのため開発後期によりバックエンド側で複雑な処理をしたくなった際に枯れたRDBにデータが溜まっているので潰しが効くようになります。

そういったお手軽ツールだけではないのですが、素のテーブルを何でもGraphQLに繋いでしまうとスキーマの変更に弱く、変更=メンテナンスになってしまうリスクがあります

Netflixの事例でもViewを作ってそこにGraphQLを繋いでいるようで変更耐性についてはよく考えた方がいいです。

CQRSアーキテクチャーでないシステムで採用してしまう

GraphQLはqueryとmutation、読み込みと書き込みが一番最初の切り口として用意されています。

Webバックエンドのアーキテクチャーとしてスモールチームで使われるMVCフレームワークは基本的に書き込みも読み込みも同じDB、同じモデルを使用しています。一方で、ある程度規模の大きいシステムでは書き込みと読み込みでDBや体系を分けるCQRSアーキテクチャーが採用されていることもあります。

勝手に作ったのですがWeb APIの設計はインフラの設計と一致していると実装が綺麗になる法則というんでしょうか、先の立ち上がりのユースケースとはズレるのですが、GraphQLがFacebook発のように最初の切り口がどのテーブルかがくるActiveRecordパターンのユースケースよりも、MVCパターンの限界を超えてしまったCQRSで整理されているシステムの方がGraphQLと親和性が高いと感じる時もあります。

同じく大規模サービスではマイクロサービスのアグリゲーションとしてBackend for Frontendを自前で実装していた部分をGraphQLでやるGraphQL Federationというアーキテクチャーもあります。

全てのAPIをGraphQLにしてしまう

以上見たようにGraphQLは銀の弾丸ではなく、ユースケースを選ぶというのが自分の見解です。そういう意味でAPIにGraphQLを採用する際は0か1かでなく部分的に採用してみるのも一つの作戦かもしれません。

認証のような硬めの実装が必要で柔軟性が入らない部分ならREST APIで十分ですし、頻繁にA/Bテストをやる画面用のAPIの部分ならGraphQLがはまる部分もあるかもしれません。

その柔軟性から管理画面のみで使う事例もあります。

所感

間違いあればご指摘ください。

Future Work

gRPCについてまとめる