wawoon blog

GraphQL API設計で気をつけること

追記: 2019/01/08

この記事よりも @vvakame さんによるGraphQL APIスキーマの設計ガイドがあるのでそちらを参照することがおすすめです。

https://vvakame.booth.pm/items/1576562

また、GraphQLの設計について相談できるSlackグループを開設しているので、わからないこと・相談したいことがあればぜひ参加してください

Slackグループ"GraphQLを使っている人たちの集まり"への招待リンク

また、すべてのGraphQL API設計のベストプラクティスはGitHub API v4に詰まっているので、困ったらGitHubのマネをするというスタンスでいるのがよいです。

https://developer.github.com/v4/

GraphQL で実装するときに気をつけること

ID は global で unique にする

  • クライアント側で返却するIDはユニークである必要があります。
    • たとえばUser型のidを123と返すのではなく、User:123や、これを base64 encode したVXNlcjoxMjM=とします。
    • 理由は、ApolloやRelayのようなGraphQLクライアントはキャッシュ機構を持っており、このidをキーとしてエンティティを保存するためです
    • 異なる型で同じidを共有してしまうと、キャッシュが意図せず上書きされるのでバグの原因となります

この仕様はGraphQL Global Object Identification Specificationに定義されています。 https://facebook.github.io/relay/graphql/objectidentification.htm

※ ApolloのInMemoryCacheでは, 1. _typenameと, 2. id, _idのどちらか とを組み合わせてキャッシュのキーとするため、厳密にはidをglobal uniqueにする必要はありません。

https://www.apollographql.com/docs/react/caching/cache-configuration/#assigning-unique-identifiers

Mutation の入力は input を定義する

type Mutation {
  createPost(title: String!, content: String!): PostPayload!
}

とするのではなく

type Mutation {
  createPost(input: CreatePostInput!): CreatePostPayload!
}
 
input CreatePostInput {
  title: String!
  content: String!
}

としましょう。inputを定義しておくと TypeScript の型が作られるので便利です。

TypeScriptの型を生成する方法

Mutation でデータ変更があったエンティティは、その場で返す

type Mutation {
  updatePost(input: UpdatePostInput!): UpdatePostPayload!
}
 
type UpdatePostPayload {
  postEdge: PostEdge! // ここで更新した中身を返すようにする
}
  • 変更のあったデータをmutationの返り値にすると、クライアント側でキャッシュの更新ができます
    • UpdatePostPayloadの中身でPost!を返さず、成功/失敗のbooleanだけを返すような設計にしておくと、クライアントサイドのキャッシュ更新を自分でやらないといけなくなるので不便です。
  • 元ネタ

リレーション先は ID ではなくて生のエンティティを返すようにする

type Post {
  id: ID!
  userId: ID!
  title: String!
  content: String!
}

よりも

type Post {
  id: ID!
  user: User!
  title: String!
  content: String!
}

としましょう。

  • ID を返すようにすると、もう一度クエリしないと取得できない
  • はじめから生のエンティティを返すようにすれば一度で取得できる
  • ただし、関連先を取得するのはクエリで要求されているときだけにする
    • 呼ばれたフィールドだけ関連先を実装する設計にするのは、たいていのライブラリでサポートされています。
    • こうしておくと無駄なDBアクセスを防ぐことができます

DB アクセスの N+1 問題に対処する

ページネーションに対応する

例

{
  user {
    id
    name
    friends(first: 10, after: "opaqueCursor") {
      edges {
        cursor
        node {
          id
          name
        }
      }
      pageInfo {
        hasNextPage
      }
    }
  }
}

pageInfoにhasNextPageがあるので、次のページの取得ができるかどうかをrequestをせずに知ることができる。 specificationはここ

コレクションの取得は limit をかならず掛ける

  • Relay Specificationに従っている場合
    • first, lastを設定していれば問題ない
    • もしもfirst, lastのどちらも設定されていなかった場合、first: 10, after: nullなどにフォールバックするべき
  • Relay Specificationに従っていない場合
    • 何件ほしいのかを明確にしないと、すべて返すことになってしまう
    • 件数が増えたときに対応できない

複雑なクエリをはじく

ディレクティブを活用する

GraphQL のテストを書く

参考

https://github.com/vvakame/graphql-with-go-book https://graphqlmastery.com/blog/graphql-best-practices-for-graphql-schema-design https://blog.apollographql.com/optimizing-your-graphql-request-waterfalls-7c3f3360b051 https://qiita.com/hitochan777/items/04c16ca770f7b3a84af5