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

GraphQLスキーマ設計の勘所

Yuku Kotani
January 21, 2023

 GraphQLスキーマ設計の勘所

Burikaigi 2023
https://burikaigi.dev/

Yuku Kotani

January 21, 2023
Tweet

More Decks by Yuku Kotani

Other Decks in Technology

Transcript

  1. GraphQL スキーマ設計の勘所 @yukukotani 2023/01/21 - Burikaigi 2023 #burikaigi_h

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

        ・技術戦略、アーキテクト、認証基盤 ・Hobby Guitarist (2020/06~) ・Student @ Univ. Tsukuba (2019/04~)     ・情報科学類 自己紹介
  3. 目次 1. 本セッションについて 2. プラクティスの紹介 3. まとめ

  4. 目次 1. 本セッションについて 2. プラクティスの紹介 3. まとめ

  5. これから GraphQL API を作るときの大失敗を避ける GraphQL はプロトコルなので後方互換性が必要。ミスっても無邪気にリファクタしにくい。 クライアント/サーバー双方の都合を理解して設計するための最低限を知る 本セッションの目標 話さないこと “

    GraphQL そのものの紹„ “ クライアント/サーバーの設計や実装詳r “ モデリング一般の話題
  6. Apollo Client を前提に話します 圧倒的にシェアが大きいし、iOS / Android / Web 共通 ただし、一部

    Relay や urql にも触れる npmtrends.com
  7. どこまで厳密にやるかは要件次第。落とし所は必要 本セッションでは温度感を3分類する プラクティスの分類 絶対に準拠すべき。やらないと破綻する MUST 準拠したほうがより GraphQL を活かせる SHOULD 場合によっては準拠しても良いかも

    MAY
  8. 目次 1. 本セッションについて 2. プラクティスの紹介 3. まとめ

  9. 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回リクエストを送らないと 会社名に辿り着けない
  10. 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 実体への直接参照でグラフ構造を作ると、一気に取れる スキーマ定義 オペレーション 一発で会社名まで辿り着ける
  11. リソースをグラフ構造にする MUST Q: Company が不要な場合に、無駄な計算コストが発生しないか? A: フィールド単位のリゾルバによって回避できる const = return

    { User: { ( ) { companyRepository. (parent.companyId); }, }, }; resolvers company find parent query { ( : ) { } } user id id name "yukukotani" リゾルバ定義 オペレーション company フィールドを含んでいないので リゾルバは実行されない
  12. グローバルにユニークな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`
  13. グローバルにユニークなIDを振る MAY Apollo Client ではクエリ単位で再フェッチをするため、 無理に準拠する必要はない Relay ではフラグメント単位の再フェッチなどのために必須となっている。 Relay 以外でも

    `node` クエリで任意のリソースを取得できるのは開発時に役立つ
  14. コラム: 各クライアントのキャッシュ機構 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}` を キーとして参照グラフを形成
  15. コラム: 各クライアントのキャッシュ機構 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 の実体は残る ミューテーションの結果が 自動で反映される
  16. コラム: 各クライアントのキャッシュ機構 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 クエリ単位で再フェッチ ミューテーションと同様に クエリ結果は自動反映
  17. コラム: 各クライアントのキャッシュ機構 Relayのキャッシュ機構 Apollo のように正規化されたグラフキャッシュを持つ。 加えて、`node` クエリによって任意のリソース単位の再フェッチが可能 urqlのキャッシュ機構 デフォルトでは、クエリ(と与えた変数)をキーとした、 1階層のシンプルな

    Key-Value キャッシュを持つ。 `id` は見ずに、ミューテーションによって更新された型を Value に含むキャッシュを全て破棄。 `@urql/exchange-graphcache` を導入することで Apollo に似たグラフキャッシュを導入できる
  18. ミューテーションは専用の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 以外に変更すると壊れる
  19. ミューテーションは専用の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! 単にフィールドを追加するだけで 追加のリソースを返せる
  20. ミューテーションは専用の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 がキャッシュに反映される
  21. ミューテーションは専用の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 は一部の古いミューテーションのみ準拠
  22. スキーマにエラーを含める 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 にしておくと 後からエラーを追加できる
  23. スキーマにエラーを含める SHOULD 呼ぶ側はインラインフラグメントでエラーを受け取り、`errors` のサイズでチェック mutation ... on ... on ...

    on { ( : , : ) { { } { { } { } { } } } } changeUserId currentUserId newUserId user id name errors message message suggestedIds message "buri" "toyama" UserNotFoundError IdAlreadyTakenError Error Error インターフェースを default 的にキャッチ。 サーバー側でエラーの種類が増えても壊れなくなる
  24. スキーマにエラーを含める 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 インターフェースを使う
  25. スキーマにエラーを含める SHOULD Q: トップレベルのエラーはいつ使う? A: 個別にハンドルできないネットワークエラー等や
 ユーザーに見せない開発者向けのエラーで使う。
 スキーマを汚さずに済む

  26. ビジネスロジックをフィールドで表現する SHOULD 一部の画面でしか使わないロジックも、積極的にフィールドとリゾルバで表す type ! ! ! { : :

    : } User ID Int Boolean id age isAdult const = return >= { User: { ( ) { parent.age ; }, }, }; resolvers 20 isAdult parent 従来のRESTではオーバーフェッチによる無駄な計算コストを無視できなかったが、 GraphQLの部分フェッチにより可能になった。 クライアントはさらにJSON色付けに専念するようになる `isAdult`が参照されたときのみ計算。 この程度の計算コストなら問題ないが、 DBアクセスが入ると無視できなくなる
  27. カーソル・ページネーションでは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
  28. カーソル・ページネーションではConnectionを使う SHOULD 戻り値を後から Connection にすると破壊的変更になってしまう。 リストを返す場合は、後々ページネーションが必要にならないか 予めよく検討する必要がある

  29. 目次 1. 本セッションについて 2. プラクティスの紹介 3. まとめ

  30. まとめ ・スキーマを に育てていくために、初期設計が大事 ・クライアント/サーバー双方の視点で設計しないとすぐに破綻する ・世に溢れるプラクティスの背景を理解して、取捨選択していく 連続的

  31. 参考文献 ・Production Ready GraphQL Book ・Production Ready GraphQL Blog ・Yelp

    Schema Design Guidelines ・Shopify GraphQL Design Tutorial ・GraphQLスキーマ設計ガイド 第2版 ・GraphQL Client Architecture Recommendation 社外版 全て必読レベルです
  32. スーパー採用中 Ubieでは以下の職種を募集中です! 採用サイトをご覧いただくか、懇親会でお声がけください!楽しいよ! ・QA エンジニア ・ソフトウェアエンジニア(デリバリー&サクセス) ・基盤開発エンジニア ・SRE ・プロダクト開発エンジニア https://recruit.ubie.life/engineer

  33. ありがとうございました! 懇親会でGraphQL話(それ以外の話も)ぜひしましょう