Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GraphQLスキーマ設計の勘所

 GraphQLスキーマ設計の勘所

Burikaigi 2023
https://burikaigi.dev/

Yuku Kotani

January 21, 2023
Tweet

More Decks by Yuku Kotani

Other Decks in Technology

Transcript

  1. 小谷 優空 - @yukukotani ・Software Engineer @ Ubie, Inc. (2019/05~)

        ・技術戦略、アーキテクト、認証基盤 ・Hobby Guitarist (2020/06~) ・Student @ Univ. Tsukuba (2019/04~)     ・情報科学類 自己紹介
  2. type ! ! ! ! type ! ! type !

    ! { ( : ): ( : ): } { : : : } { : : } Query ID User ID Company User ID String ID Company ID String user id company id id name companyId id name query { ( : ) { } } user id companyId "yukukotani" # ubie query { ( : ) { } } company id name "ubie" # Ubie, Inc. リソースをグラフ構造にする MUST ID参照を用いた構造化では複数リクエストが必要になってしまう スキーマ定義 オペレーション 逐次に2回リクエストを送らないと 会社名に辿り着けない
  3. type ! ! type ! ! type ! ! {

    ( : ): } { : : : } { : : } Query ID User User ID String Company Company ID String user id id name company id name # Direct! query { ( : ) { { } } } user id company name "yukukotani" # Ubie, Inc. リソースをグラフ構造にする MUST 実体への直接参照でグラフ構造を作ると、一気に取れる スキーマ定義 オペレーション 一発で会社名まで辿り着ける
  4. リソースをグラフ構造にする MUST Q: Company が不要な場合に、無駄な計算コストが発生しないか? A: フィールド単位のリゾルバによって回避できる const = return

    { User: { ( ) { companyRepository. (parent.companyId); }, }, }; resolvers company find parent query { ( : ) { } } user id id name "yukukotani" リゾルバ定義 オペレーション company フィールドを含んでいないので リゾルバは実行されない
  5. グローバルにユニークなIDを振る MAY 全リソースを横断で特定できるIDを振り、 任意のリソースを取得できる `node` クエリを定義することで リソース単位で機械的に再フェッチできる type ! !

    ! ! ! ! interface ! type implements ! ! ! type implements ! ! { ( : ): ( : [ ] ): [ ] } { : } { : : : } { : : } Query ID Node ID Node Node ID User Node ID String Company Company Node ID String node id nodes ids id id name company id name スキーマ定義 GitHubの場合 `{version}:{__typename}{id}` の形式を base64 エンコードした値。
 例: `04:User16265411` → `MDQ6VXNlcjE2MjY1NDExCg==` Shopifyの場合 `gid://shopify/{__typename}/{id}` の形式。
 例: `gid://shopify/Product/123`
  6. コラム: 各クライアントのキャッシュ機構 user(“yukukotani”) user(“buri”) User:yukukotani name=”Yuku Kotani” company=Company:ubie User:buri name=”Taro

    Buri” company=Company:ubie Company:ubie name=”Ubie, Inc.” query { ( : ) { { } } ( : ) { { } } } users user id id name company id name user id id name company id name "yukukotani" "buri" Query level cache Node level cache Apollo Client のキャッシュ構築 オペレーション クライアントキャッシュ クエリをルートとした グラフ構造に正規化 Node は `{__typename}:{id}` を キーとして参照グラフを形成
  7. コラム: 各クライアントのキャッシュ機構 user(“yukukotani”) user(“buri”) User:yukukotani name=”Yuku Kotani” company=Company:ubie User:buri name=”Taro

    Buri” company=null Company:ubie name=”Ubie, Inc.” mutation { ( : ) { { { } } } } leaveCompany id user id name company id name "buri" "data" "buri" "Taro Buri" : { : { : { : , : , : } } } "leaveCompany" "user" "id" "name" "company" null Apollo Client のキャッシュ更新 via ミューテーション オペレーション クライアントキャッシュ あくまで参照が消えるだけなので Company:ubie の実体は残る ミューテーションの結果が 自動で反映される
  8. コラム: 各クライアントのキャッシュ機構 user(“yukukotani”) user(“buri”) User:yukukotani name=”Yuku Kotani” company=Company:ubie User:buri name=”Taro

    Toyama” company=null Company:ubie name=”Ubie, Inc.” await client. ({ include: [ ], }); refetchQueries "users" Apollo Client のキャッシュ更新 via 再フェッチ 部分的に再度クエリ 一度叩いたクエリを明示的に再フェッチ 再フェッチについて: https://www.apollographql.com/docs/react/data/refetching/ クライアントキャッシュ query { ( : ) { { } } } users user id id name company id name "buri" # Taro Toyama クエリ単位で再フェッチ ミューテーションと同様に クエリ結果は自動反映
  9. コラム: 各クライアントのキャッシュ機構 Relayのキャッシュ機構 Apollo のように正規化されたグラフキャッシュを持つ。 加えて、`node` クエリによって任意のリソース単位の再フェッチが可能 urqlのキャッシュ機構 デフォルトでは、クエリ(と与えた変数)をキーとした、 1階層のシンプルな

    Key-Value キャッシュを持つ。 `id` は見ずに、ミューテーションによって更新された型を Value に含むキャッシュを全て破棄。 `@urql/exchange-graphcache` を導入することで Apollo に似たグラフキャッシュを導入できる
  10. ミューテーションは専用のPayload型を返す MUST 直接リソースを返り値にすると 追加でリソースを返す場合に破壊的変更となる type ! ! ! type !

    ! { ( : : ): } { : : } Mutation ID String User User ID String updateUserName userId name id name スキーマ定義 オペレーション mutation { ( : , : , ) { } } updateUserName userId name id name "yukukotani" "Yuku Kotani" updateUserName が User を返す前提になっている。 後から User 以外に変更すると壊れる
  11. ミューテーションは専用のPayload型を返す MUST 予めミューテーションごとに専用のPayload型にしておくことで、 フィールドの追加が可能になる type ! ! ! type !

    { ( : : ): } { : } Mutation ID String UpdateUserNamePayload UpdateUserNamePayload User updateUserName userId name user BAD GOOD type ! ! ! type ! ! { ( : : ): } { : : } Mutation ID String UpdateUserNamePayload UpdateUserNamePayload User String updateUserName userId name user previousName # ← NEW! 単にフィールドを追加するだけで 追加のリソースを返せる
  12. ミューテーションは専用のPayload型を返す MUST キャッシュ更新のため、Payload 型には更新されたリソースを含める type ! ! ! type !

    { ( : : ): } { : } Mutation ID String UpdateUserNamePayload UpdateUserNamePayload Boolean updateUserName userId name isSuccess BAD type ! ! ! type ! { ( : : ): } { : } Mutation ID String UpdateUserNamePayload UpdateUserNamePayload User updateUserName userId name user GOOD Apollo Client によって 新しい User がキャッシュに反映される
  13. ミューテーションは専用のInput型のみ引数に取る MAY 同様に引数でも専用のInput型を受け取るようにする type ! ! input ! ! type

    ! { ( : ): } { : : } { : } Mutation UpdateUserNameInput UpdateUserProfilePayload UpdateUserNameInput ID String UpdateUserNamePayload User updateUserName input userId name user スキーマ定義 Payload型のようにクリティカルな実利はないため、 無理に準拠する必要はない 歴史的経緯 Relayの古いバージョン等で要求されていた `clientMutationId` 周りの仕様の名残で残っている Public GraphQL API から学ぶ GitHub はこれに完全に準拠している。 Shopify は一部の古いミューテーションのみ準拠
  14. スキーマにエラーを含める SHOULD 個別にハンドルしてユーザーに見せるべきアプリケーションエラーは トップレベルのエラーではなく、スキーマで表す type ! ! type ! !

    union type implements ! type implements ! ! ! interface ! { ( : , : ): } { : : [ ] } = | { : } { : : [ ] } { : } Mutation ID ID ChangeUserIdPayload! ChangeUserIdPayload User ChangeUserIdError ChangeUserIdError UserNotFoundError IdAlreadyTakenError UserNotFoundError Error String IdAlreadyTakenError Error String String Error String changeUserId currentUserId newUserId user errors message message suggestedIds message """取得可能な類似ID""" エラーごとの追加情報等も スキーマで表せる = 型を自動生成可能 最初はエラーが1つだったとしても union にしておくと 後からエラーを追加できる
  15. スキーマにエラーを含める SHOULD 呼ぶ側はインラインフラグメントでエラーを受け取り、`errors` のサイズでチェック mutation ... on ... on ...

    on { ( : , : ) { { } { { } { } { } } } } changeUserId currentUserId newUserId user id name errors message message suggestedIds message "buri" "toyama" UserNotFoundError IdAlreadyTakenError Error Error インターフェースを default 的にキャッチ。 サーバー側でエラーの種類が増えても壊れなくなる
  16. スキーマにエラーを含める SHOULD そこまで厳密でなくて良い場合、ナイーブに Error 型のみ用意するパターンもある type ! ! type !

    ! type implements ! type implements ! ! ! interface ! { ( : , : ): } { : : [ ] } { : } { : : [ ] } { : } Mutation ID ID ChangeUserIdPayload! ChangeUserIdPayload User Error UserNotFoundError Error String IdAlreadyTakenError Error String String Error String changeUserId currentUserId newUserId user errors message message alternativeIds message """ 取得可能な類似ID """ ミューテーションごとの Error union を作らず、 Error インターフェースを使う
  17. ビジネスロジックをフィールドで表現する SHOULD 一部の画面でしか使わないロジックも、積極的にフィールドとリゾルバで表す type ! ! ! { : :

    : } User ID Int Boolean id age isAdult const = return >= { User: { ( ) { parent.age ; }, }, }; resolvers 20 isAdult parent 従来のRESTではオーバーフェッチによる無駄な計算コストを無視できなかったが、 GraphQLの部分フェッチにより可能になった。 クライアントはさらにJSON色付けに専念するようになる `isAdult`が参照されたときのみ計算。 この程度の計算コストなら問題ないが、 DBアクセスが入ると無視できなくなる
  18. カーソル・ページネーションではConnectionを使う SHOULD サーバー側のパフォーマンス、クライアント側の決定性の観点で カーソル・ページネーションが推奨されている。 Relay の Connection Spec に従うと、クライアント自動生成の恩恵を受けやすい type

    ! ! type ! ! ! type ! ! { ( : , : ): } { : [ ] : } { : : } Query Int String UserConnection UserConnection UserEdge PageInfo UserEdge String User users first after edges pageInfo cursor node type ! ! ! ! { : : : : } PageInfo Boolean String Boolean String # 次方向にこれ以上ノードがあるかどうか # ページの最後のノードのカーソル hasNextPage endCursor hasPreviousPage startCursor
  19. 参考文献 ・Production Ready GraphQL Book ・Production Ready GraphQL Blog ・Yelp

    Schema Design Guidelines ・Shopify GraphQL Design Tutorial ・GraphQLスキーマ設計ガイド 第2版 ・GraphQL Client Architecture Recommendation 社外版 全て必読レベルです