17
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Production Ready GraphQLまとめ

Last updated at Posted at 2022-07-13

Production Ready GraphQLという洋書の4章の「GraphQL Schema Design」を自分なりにまとめたもの。

Design First

  • お気に入りのライブラリでいきなり実装を始めるのではなく、設計から始める
  • 実装から始めると、実装の詳細と密結合した設計になりがち
  • 担当ドメインの専門家の話を聞きながら、APIを設計できるのがベスト
  • 設計から始めることで破壊的な変更の頻度を減らせる

Client First

  • 設計をする際は、Clientのユースケースを念頭に置く
  • Clientのニーズの要求を満たし、簡単にAPIを使えるようにし、間違った使い方はなるべくできないようにする
  • 開発のなるべく早い段階からClientと結合して、フィードバックをもらうこと
  • バックエンドの実装の詳細などのClientが知る必要のない情報は公開しない
  • データベースからGraphQL APIを生成するツールはたくさんあるが、Client Firstの観点からはあまり意味はない
    • 生成されたスキームは、バックエンドの実装と密に結合している
    • 一般的すぎるAPIが生成されがち
    • Clientの要望を全く意識していない
    • 不要な情報を公開しすぎる
  • Open APIをGraphQLスキームに変換するツールも良くはない

Naming

  • 良い命名をしていると、ドキュメントを見なくても動作が分かる
  • 良い命名は正しい設計につながる
  • 一貫性が一番大切
    • 悪い例:Queryに動詞が付いていたりいなかったりする
      • products
      • findProducts
    • 悪い例:表記ゆれ
      • BlogPost
      • Post
    • 悪い例:Mutationの動詞の揺れ1
      • addProduct
      • createPost
  • APIに対称性をもたせる
    • publishPostときたら、反対の操作はunpublishPostとする
  • 命名は具体的にする
    • 具体的な命名は、大規模なAPI廃止を避けられるし、使い方も理解しやすい
    • 一般的すぎる命名の例:"Event"、"User"

Descriptions

  • APIの使い手にとって、Descriptionは外部のドキュメントより見つけやすくて良い
  • ほとんどのエンティティにDescriptionを書くと良い
  • 良いDescriptionは、typeが何を表現するか、mutationは何をするかを明瞭にする
  • とはいえ、Descriptionをあまり読まなくても、APIを理解して使えるようにすべき
  • Descriptionに、エッジケースや独特な振る舞いをする条件が記述されていると良くない兆候2

Use the Schema, Luke!

  • enumを使う
  • stringやcustom encoding scalarにJSON文字列のような半構造化データを持たせるのではなく、typeを定義する
  • custom scalarは役に立つかも (DDDの値型のような立ち位置)

Expressive Schema

  • フィールドは一つの仕事をこなすようにし、一般的だったり利口すぎるフィールドは避ける3
  • なるべくスキーマで正しい使い方を強制する
  • 関連したフィールドや引数の集まりを表現するために、型を定義する
  • optional inputsや引数を使うときは、デフォルト値を定義する

以下、具体例。

  • 例1
    • いまいち
      • findProduct(id: ID, name: String): Product
      • IDとnameを両方とも指定していないときの動作が読み取れない
    • 良い
      • productByID(id: ID!): Product
      • productByName(name: String!): Product
  • 例2
    • いまいち
      • products(sort: SortOrder): [Product!]!
      • SortOrderを指定しないときの並びが読み取れない
    • 良い
      • products(sort: SortOrder = DESC): [Product!]!

Specific or Generic

  • おおむね特化したAPIの方が良い
    • いまいち
      • posts(first: Int!, includeArchived: Boolean): [Post!]!
    • 良い
      • posts(first: Int!): [Post!]!
      • archivedPosts(first: Int!): [Post!]!
  • booleanの引数は、汎化されすぎている兆候かもしれない
  • 汎化したAPIは便利なこともあるが、使い方が分かりづらい、性能に難がある、といった問題もある4

Anemic GraphQL

  • 他のフィールドから計算される値はフィールドとして定義する
    • Clientに計算させるのではなくServerで計算させるということ
    • ルールが変わるとClientを修正しないといけないから
  • Mutationで万能Update API + XXXInputを定義するのではなく、ユースケース別にAPIやInputを定義する
    • optionalなフィールドがなくなる
    • APIの動作が分かりやすくなる
    • Inputに不正な値の組み合わせを設定できなくなる

List&Pagination

Pagination

Offset Paginationをサポートする必要がないなら、Cursor Paginationが良い。

  • Offset Pagination
    • Good: 実装が楽、ユーザがページの飛ばし読みができる
    • Bad: 走査するデータ数が増えるので性能に難あり、リストの閲覧中に項目数が変わると整合性が取れなくなる
  • Cursor Pagination
    • Offset Paginationと特徴の裏返し
    • ほとんどのGraphQL APIはこちらの方式

Relay Connection

以下のようなスキーマ設計のこと。

type Query {
  products(limit: Int!, after: String): ProductConnection!
}

type ProductConnection {
  edges: [ProductEdge]
  pageInfo: PageInfo!
  """
  これ以外のフィールドを持たせるのもあり。
  たとえば、node一覧へのショートカット。
  nodes: [Product!]!
  """
}

type ProductEdge {
  cursor: String!
  node: Product!
}

type PageInfo {
  endCursor: String
}

"""リスト対象"""
type Product {
  name: String!
}

EdgeやConnectionに情報を足せるので、色々なユースケースにも対応しやすい。

Sharing Types

  • 型を使い回すことに自信がないなら、使い回さない方が良い
  • 型の使い回しは便利なときもあるが、後々、問題を起こすこともある

Global Identification

  • 慣例
    • Node interfaceを作り、それを実装する型にはグローバルに一意なIDを設定する
    • node(id: ID!) Nodenodes(ids: [ID!]!) [Node!]!で取得できるようにする
    • IDには、型を判別するための情報も含める
  • API ClientとしてRelayをサポートしないなら慣例に従うのは必須ではないが、Relayを使わなくても良い設計パターンになりえる
  • IDはOpaque にした方が良い5
    • API ResponseのIDをBase64でエンコードにしておくと、使い手にOpaqueな値ということを思い出させるのに良い

Nullability

  • non-nullのメリット
    • Schemaから読み取れる情報が多くなる
    • API Client側の過度な防御コードを減らせる
  • 注意
    • non-nullからnullableの変更は破壊的変更
    • どのフィールドがnullになるのか否かの予測は難しい
      • タイムアウト、API Rate Limit起因でフィールドがnullになることも
    • nullableな値をnullで返したときの挙動が独特
      • ルールに違反したフィールドの先祖をさかのぼって、最初に見つけたnullableなフィールドの値がnullになる
  • ガイドライン
    • 引数
      • non-nullにすると良いことが多い
      • 既存のQuery/Mutationに引数を追加するときは互換性を保つためにnullableにするのが良い
    • Objectフィールド
      • Database、networks callのように失敗する可能性があるものから取得しているなら、nullableにした方が良い
    • 単純なスカラ値のフィールド
      • 大体、non-nullで大丈夫

Abstract Types

  • interfaceは共通の振る舞いを表現するのに使うこと
    • 例: GitHubのGraphQL APIのStarable
  • interfaceの濫用はしないこと
    • 単に共通のフィールドを持っているからという理由で、interfaceを定義するのはいまいち
    • 実装時にコードを再利用したいからといってinterfaceを定義するのはいまいち

Designing for Static Queries

  • GraphQLのQueryは動的に組み立てるのではなく、静的に記述するのが良い(Queryに渡す引数を変えるのはありらしい)
  • 静的Queryの利点
    • Clientが必要としていることが分かりやすい
    • Lint、IDEなどのツールの恩恵を受けられる
    • サーバがQueryを保存できる
  • IDのリストからオブジェクトを一括取得するときに動的Queryを使いたくなるかもしれないが、それはBatch Read用のQueryを用意することで避けられる

Mutations

  • mutationごとに固有のPayload型を定義し、それを戻り値にする
    • 付随的な情報を返せるのがメリット
  • mutationごとに固有のInputを1つ定義し、それを引数にする
    • mutation時に渡すパラメータを増やしやすい

以下、具体的イメージ。

input PostMessageInput {
  roomID: String!
  text: String!
}

type PostMessagePayload {
  message: Message
  errors: [UserError!]!
}

type Mutation {
  postMessage(input: PostMessageInput!): PostMessagePayload
}

Fine-Grained or Coarse-Grained(粒度)

  • mutationについて、どれくらいの粒度が良いかは状況次第
  • 作成mutationの粒度は荒く、更新mutationの粒度は細かい方が良いことが多い
  • 複数のデータの更新が必要 かつ 整合性が求められる場合は、1つの粒度の粗い更新mutationを作る
    • 別解として、1つのmutationのInputのフィールドに、実行したい操作のリストを持たせるやり方もある

Errors

  • Developer/Clientエラーは、GraphQLの標準のerrorsで表現すると良い
    • Timeout、Rate Limitedなど
  • Userエラーは、独自の表現が良い
    • サインアップ画面のパスワードの文字種が間違っている、2重支払いをしたなど
  • mutationのErrorを独自に表現する方法には2つある
    1. カスタムエラー型を定義し、Payload型のフィールドにその配列を持たせる
    2. mutationの結果となるPayload型をunionとして定義する
      union SignUpPayload = SignUpSuccess | UserNameTaken | PasswordTooWeak
      
      どういうエラーが起きるか分かりやすい
  • エラー型を定義するときは、interfaceを定義しそれを実装する形にすると良い
    • エラー型を追加したとき、古いAPI Clientでもハンドリングできる
  • 上記2つのエラーの定義方法のどちらを選択するかは好み

Schema Organization

  • Namespaces
    • 適切な名付けをしていればnamespaceは大体いらない
    • namespaceのようなものが欲しいのであれば、Prefixを付ける
      • 例:Instagram_User、Fracebook_User
    • GraphQLスキーマとサーバ側の実装は別物
      • サーバ側の実装としては、namespace、モジュール、共通関数を使うことは当然あり
  • Mutations
    • グループを表す言葉を先頭にするのではなく、読みやすい命名にする
      • いまいち:productCreate、productDelete
      • Good:createProduct、deleteProduct
    • directiveの"tags"のようなものを作り、それでmutationのgroupを表現する考えもある

Asynchronous Behavior

Data-Driven Schema vs Use-Case-Driven Schema

  • Use-Case-Driven Schemaの方が良い
  • GitHubのGraphQL APIのスキーマは、Use-Case-Drivenの良い例
  1. 動詞の揺れは悪くないと思っている。一貫性をもたせようとすると新規登録は全部createになりそうだが、createは一般的すぎる。チャットのAPIを作るとして、チャットルームを作るAPIはCreateChatで、メッセージ投稿APIはPostMessageというような命名の方が分かりやすい。メッセージ投稿をCreateMessageにすると、メッセージ送信なのかメッセージの下書きを作るのか分かりづらい。

  2. "Aの場合はαという動作になる、Bの場合はβという動作になる、Cの場合はγになる、・・・"というようにエッジケースがつらつら書かれている場合の話だと思う。一覧取得APIがあって、limitで"巨大な値を設定しても1000に設定する"というようなDescriptionなら有りだと思う。

  3. フィールドを安直にnullableにしてしまうと、一般的でお利口なスキーマになりがちな印象。本で示されている悪い例はnullableなスカラー型のフィールドが多かった。

  4. APIの使い手が多いときは汎化もありだと思う。

  5. IDの値の形式をAPI Clientから意識させないようにするということ。

17
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?