追記: 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を共有してしまうと、キャッシュが意図せず上書きされるのでバグの原因となります
- たとえばUser型の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 を定義する
1type Mutation {2 createPost(title: String!, content: String!): PostPayload!3}4
とするのではなく
1type Mutation {2 createPost(input: CreatePostInput!): CreatePostPayload!3}45input CreatePostInput {6 title: String!7 content: String!8}9
としましょう。inputを定義しておくと TypeScript の型が作られるので便利です。
TypeScriptの型を生成する方法
- Relayを使っている場合
- relay-compilerが型を作成してくれる
- その他の場合(Apolloなど)
- graphql-code-generatorを利用すると型を生成できます
- https://github.com/dotansimha/graphql-code-generator
Mutation でデータ変更があったエンティティは、その場で返す
1type Mutation {2 updatePost(input: UpdatePostInput!): UpdatePostPayload!3}45type UpdatePostPayload {6 postEdge: PostEdge! // ここで更新した中身を返すようにする7}8
- 変更のあったデータをmutationの返り値にすると、クライアント側でキャッシュの更新ができます
- UpdatePostPayloadの中身でPost!を返さず、成功/失敗のbooleanだけを返すような設計にしておくと、クライアントサイドのキャッシュ更新を自分でやらないといけなくなるので不便です。
- 元ネタ
リレーション先は ID ではなくて生のエンティティを返すようにする
1type Post {2 id: ID!3 userId: ID!4 title: String!5 content: String!6}7
よりも
1type Post {2 id: ID!3 user: User!4 title: String!5 content: String!6}7
としましょう。
- ID を返すようにすると、もう一度クエリしないと取得できない
- はじめから生のエンティティを返すようにすれば一度で取得できる
- ただし、関連先を取得するのはクエリで要求されているときだけにする
- 呼ばれたフィールドだけ関連先を実装する設計にするのは、たいていのライブラリでサポートされています。
- こうしておくと無駄なDBアクセスを防ぐことができます
DB アクセスの N+1 問題に対処する
- GraphQL の各リゾルバは、1 クエリで 100-500 回呼ばれることもありうる
- 各リゾルバが勝手に DB アクセスすると、SQL が 100-500 回呼ばれることになる。
- これをバッファリングして DB アクセスをバッチ化する仕組みが必要。
- JS実装
- Go実装
ページネーションに対応する
- コレクションへのクエリはページネーションできるようにしなくてはいけない
- edges
- node
- https://facebook.github.io/relay/docs/en/graphql-server-specification.html
例
1{2 user {3 id4 name5 friends(first: 10, after: "opaqueCursor") {6 edges {7 cursor8 node {9 id10 name11 }12 }13 pageInfo {14 hasNextPage15 }16 }17 }18}19
pageInfoにhasNextPageがあるので、次のページの取得ができるかどうかをrequestをせずに知ることができる。 specificationはここ
コレクションの取得は limit をかならず掛ける
- Relay Specificationに従っている場合
- first, lastを設定していれば問題ない
- もしもfirst, lastのどちらも設定されていなかった場合、
first: 10, after: null
などにフォールバックするべき
- Relay Specificationに従っていない場合
- 何件ほしいのかを明確にしないと、すべて返すことになってしまう
- 件数が増えたときに対応できない
複雑なクエリをはじく
- 複雑なクエリを実行させないようにする
- depthで制限をかける
- queryのcomplexityを計算して制限をかける
- 例
ディレクティブを活用する
- データアクセス制限をかけるために使う
- 例
GraphQL のテストを書く
- 手動で書くのは難しいので request と output のログを記録しておき、スキーマなどの変更をしたあとに差分がないかを確認する(ゴールデンテスティング)
- 実例
参考
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