一から勉強させてください

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです

GraphQL Schema Design @ Scale (Marc-André Giroux)メモ

最近は GraphQL やっていきな感じです。ただスキーマ設計が色々と悩ましく、設計の参考にするためにGraphQL Schema Design @ Scale (Marc-André Giroux) を観たのでかなり雑にメモ。

※ 英語のプレゼン動画をさくっと観ただけなので、正しく理解できていない所もあるかもしれません。

なぜ GraphQL を使うのか

  • 型があるので推論しやすい API をつくることができる
  • クライアントが必要な情報を宣言的にリクエストできてよい

GraphQL スキーマデザインについての誤解

  • 複雑なアプリケーションにおいて、GraphQL はそのまま DB のインターフェースになることはめったにない
  • GraphQL スキーマは既存の REST API のリソースにマッチする必要はない
  • UI と 1:1 でマッピングされる必要はない
    • GitHub のような大きなアプリケーションになると、1 つのユースケースや 1 つの UI レイヤーにフォーカスしたスキーマをつくるのは難しい

GraphQL の真の力

  • GraphQL は我々のコアドメインのインターフェースをモデリングすることができる
    • 我々のアプリケーションは何をするためのものなのか (e.g. GitHub は何をするためのものなのか)

良い GraphQL スキーマをつくるための 2 つのポイント

  1. アプリケーションのドメインのエキスパートになり、知識を身につける
  2. GraphQL 特有の設計のエキスパートになる

そしてこれらを同じ人間が担当することはめったにない。

GitHub のアプローチは

  • API ファースト
  • プロダクトチームで API スキーマを作る (1,2 それぞれのエキスパートのメンバーで協力して)

Guiding Principles

データよりも振る舞いやユースケースを重視した設計

type PullRequest {
  title: String!
  description: String!
  ciStatus: CIStatus!
  reviews: [Review!]!
}

クライアントがこの PR がマージ OK かどうか知りたい場合、以下のようなチェックが必要になるはず。

fun mergeable?(pr)
  pr.ciStatus == CI_STATUSES.GREEN && pr.reviews.all? { |r| r.approved }

でもクライアントはただマージできるのかどうか知りたいのでスキーマ的には以下のような形で十分。

type PullRequest {
  title: String!
  description: String!
  ciStatus: CIStatus!
  reviews: [Review!]!
  isMergeable: Boolean!
}

マージ OK の条件が今後変わっても、それはサーバ側で吸収することでクライアントはただマージできるかどうか見ていればよい。

スマートで汎用的なフィールドよりも高度に最適化されたフィールド

type Query {
  user(id: ID, login: String): User
}

nullable なidloginを受け取れるような設計より、それぞれのアプローチに最適化されたクエリを定義して型の恩恵を受けたほうがよい。

type Query {
  userById(id: ID!): User
  userByLogin(login: String!): User
}

Tips

SDL のチェック

$ bin/dump-graphql-schema
> config/schema.public.graphql

$ git add config/schema.public.graphql

みんなスキーマをどんどん更新していくが、GitHubgraphql-rubyを使っているのでスキーマにどのような変更があったのかぱっと見てわかりにくい。上記のようなアプローチによって

  • PR 上でどういうスキーマ変更があったかすぐわかる
  • 実際のスキーマと現状の graphql ファイルの比較テストをしてマッチしない場合、CI を落とすことでメンテする

スキーマ設計ミスった時や breaking changes を入れたい場合

deprecated(
  start_date: ...,
  reason: ...,
  superseded_by: ....
)

type Repository {
  databaseId: ID! @deprecated(reason: ...)
}