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

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

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: ...)
}