Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

小谷 優空 - @yukukotani ・Software Engineer @ Ubie, Inc. (2019/05~)     ・技術戦略、アーキテクト、認証基盤 ・Hobby Guitarist (2020/06~) ・Student @ Univ. Tsukuba (2019/04~)     ・情報科学類 自己紹介

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Apollo Client を前提に話します 圧倒的にシェアが大きいし、iOS / Android / Web 共通 ただし、一部 Relay や urql にも触れる npmtrends.com

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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回リクエストを送らないと 会社名に辿り着けない

Slide 10

Slide 10 text

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 実体への直接参照でグラフ構造を作ると、一気に取れる スキーマ定義 オペレーション 一発で会社名まで辿り着ける

Slide 11

Slide 11 text

リソースをグラフ構造にする MUST Q: Company が不要な場合に、無駄な計算コストが発生しないか? A: フィールド単位のリゾルバによって回避できる const = return { User: { ( ) { companyRepository. (parent.companyId); }, }, }; resolvers company find parent query { ( : ) { } } user id id name "yukukotani" リゾルバ定義 オペレーション company フィールドを含んでいないので リゾルバは実行されない

Slide 12

Slide 12 text

グローバルにユニークな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`

Slide 13

Slide 13 text

グローバルにユニークなIDを振る MAY Apollo Client ではクエリ単位で再フェッチをするため、 無理に準拠する必要はない Relay ではフラグメント単位の再フェッチなどのために必須となっている。 Relay 以外でも `node` クエリで任意のリソースを取得できるのは開発時に役立つ

Slide 14

Slide 14 text

コラム: 各クライアントのキャッシュ機構 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}` を キーとして参照グラフを形成

Slide 15

Slide 15 text

コラム: 各クライアントのキャッシュ機構 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 の実体は残る ミューテーションの結果が 自動で反映される

Slide 16

Slide 16 text

コラム: 各クライアントのキャッシュ機構 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 クエリ単位で再フェッチ ミューテーションと同様に クエリ結果は自動反映

Slide 17

Slide 17 text

コラム: 各クライアントのキャッシュ機構 Relayのキャッシュ機構 Apollo のように正規化されたグラフキャッシュを持つ。 加えて、`node` クエリによって任意のリソース単位の再フェッチが可能 urqlのキャッシュ機構 デフォルトでは、クエリ(と与えた変数)をキーとした、 1階層のシンプルな Key-Value キャッシュを持つ。 `id` は見ずに、ミューテーションによって更新された型を Value に含むキャッシュを全て破棄。 `@urql/exchange-graphcache` を導入することで Apollo に似たグラフキャッシュを導入できる

Slide 18

Slide 18 text

ミューテーションは専用の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 以外に変更すると壊れる

Slide 19

Slide 19 text

ミューテーションは専用の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! 単にフィールドを追加するだけで 追加のリソースを返せる

Slide 20

Slide 20 text

ミューテーションは専用の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 がキャッシュに反映される

Slide 21

Slide 21 text

ミューテーションは専用の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 は一部の古いミューテーションのみ準拠

Slide 22

Slide 22 text

スキーマにエラーを含める 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 にしておくと 後からエラーを追加できる

Slide 23

Slide 23 text

スキーマにエラーを含める SHOULD 呼ぶ側はインラインフラグメントでエラーを受け取り、`errors` のサイズでチェック mutation ... on ... on ... on { ( : , : ) { { } { { } { } { } } } } changeUserId currentUserId newUserId user id name errors message message suggestedIds message "buri" "toyama" UserNotFoundError IdAlreadyTakenError Error Error インターフェースを default 的にキャッチ。 サーバー側でエラーの種類が増えても壊れなくなる

Slide 24

Slide 24 text

スキーマにエラーを含める 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 インターフェースを使う

Slide 25

Slide 25 text

スキーマにエラーを含める SHOULD Q: トップレベルのエラーはいつ使う? A: 個別にハンドルできないネットワークエラー等や
 ユーザーに見せない開発者向けのエラーで使う。
 スキーマを汚さずに済む

Slide 26

Slide 26 text

ビジネスロジックをフィールドで表現する SHOULD 一部の画面でしか使わないロジックも、積極的にフィールドとリゾルバで表す type ! ! ! { : : : } User ID Int Boolean id age isAdult const = return >= { User: { ( ) { parent.age ; }, }, }; resolvers 20 isAdult parent 従来のRESTではオーバーフェッチによる無駄な計算コストを無視できなかったが、 GraphQLの部分フェッチにより可能になった。 クライアントはさらにJSON色付けに専念するようになる `isAdult`が参照されたときのみ計算。 この程度の計算コストなら問題ないが、 DBアクセスが入ると無視できなくなる

Slide 27

Slide 27 text

カーソル・ページネーションでは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

Slide 28

Slide 28 text

カーソル・ページネーションではConnectionを使う SHOULD 戻り値を後から Connection にすると破壊的変更になってしまう。 リストを返す場合は、後々ページネーションが必要にならないか 予めよく検討する必要がある

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

参考文献 ・Production Ready GraphQL Book ・Production Ready GraphQL Blog ・Yelp Schema Design Guidelines ・Shopify GraphQL Design Tutorial ・GraphQLスキーマ設計ガイド 第2版 ・GraphQL Client Architecture Recommendation 社外版 全て必読レベルです

Slide 32

Slide 32 text

スーパー採用中 Ubieでは以下の職種を募集中です! 採用サイトをご覧いただくか、懇親会でお声がけください!楽しいよ! ・QA エンジニア ・ソフトウェアエンジニア(デリバリー&サクセス) ・基盤開発エンジニア ・SRE ・プロダクト開発エンジニア https://recruit.ubie.life/engineer

Slide 33

Slide 33 text

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