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

GraphQLスキーマの設計で考えたこと

AnaTofuZ
January 26, 2022

 GraphQLスキーマの設計で考えたこと

2022/01/26 Hatena Engineer Seminar #18
https://hatena.connpass.com/event/235821/

AnaTofuZ

January 26, 2022
Tweet

More Decks by AnaTofuZ

Other Decks in Technology

Transcript

  1. GraphQLのスキーマ • SDL (Schema Definition Language)で 定義する • interfaceをもつ ◦

    開発チームで考えると ▪ Interface Member • 共通部分 ▪ Type Engineer • エンジニア • 好きなプログラミング言語をもつ ▪ Type Designer • デザイナー • Adobe IDを必ずもつ interface Member { id: ID! name: String! githubID: String! } type Engineer implements Member { id: ID! name: String! githubID: String! favoriteProgramingLanguage: String } type Designer implements Member { id: ID! name: String! githubID: String! adobeID: String! }
  2. GraphQLのスキーマ • nullable(null許容)な値と non-nullable(非null許容)な値がある ◦ 型名の後ろに ! がついているとnon-nullable ◦ 👉の例ではid,

    nameはnon-nullable ▪ favoriteProgramingLanguageはnullable • バックエンド/フロントエンドともに 共通してスキーマを使う ◦ 型情報は両者ともに共通して知っている interface Member { id: ID! name: String! githubID: String! } type Engineer implements Member { id: ID! name: String! githubID: String! favoriteProgramingLanguage: String } type Designer implements Member { id: ID! name: String! githubID: String! adobeID: String! }
  3. GraphQLのfragment • GraphQLはfragmentを使うと、必要なデータを 宣言的に定義することができる • 👉ではInterface Memberの中から必要なデータを WebAccountとして宣言している ◦ 全MemberはgithubIDを取得できる

    ◦ … on 型名でInterfaceの具体的な型の場合取得するデータを 宣言することができる ▪ Type DesignerならAdobe IDが取得できる fragment WebAccount on Member { id githubID ... on Designer { adobeID } }
  4. GraphQLをフロントエンドで使う • GraphQL Code Generatorなどを使うとスキーマからTypeScriptの型を生成できる ◦ スキーマ上の型に対応したTypeScriptの型が生成される ◦ fragmentに対応した型(fragmentで拾ってきたい情報)も自動生成される •

    スキーマ上の型がTypeScriptの型に直接対応する ◦ InterfaceやInterfaceの実装の型はいい感じにTypeScriptの型表現にマッピングされる ◦ nullable, non-nullableな型も考慮される • 先述のfragmentをうまく使うと、コンポーネントに必要なデータをモジュール化できる ◦ fragmentはコンポーネントに閉じるので、具体的に何が必要か使う側では知らずに渡せる
  5. GraphQLとフロントエンド • fragmentで引いた値をそのままコンポーネントに渡すことができる ◦ GraphQLスキーマとコンポーネント (UI)がうまく対応できるようにスキーマを考える必要がある 👆コンポーネント使う側 👆コンポーネントの引数 memberにfragmentで引いた値をいれるだけでよい interface

    Props { member: WebAccountFragment; } export const WebAccount: React.VFC<Props> = ({ member }) => { return ( <div className={styles.hoge}> {user.__typename === 'Designer' && <span>Adobe ID: {member.adobeID} </span>} <GitHubName member={member} /> </div> ); }; fragment GitHubName on Member { id githubID } 👆コンポーネント定義側 fragment WebAccount on Member { id githubID ... on Designer { adobeID } }
  6. GraphQLとフロントエンド • fragmentで引いた値をそのままコンポーネントに渡すことができる ◦ GraphQLスキーマとコンポーネント (UI)がうまく対応できるようにスキーマを考える必要がある 👆コンポーネント使う側 👆コンポーネントの引数 memberにfragmentで引いた値をいれるだけでよい interface

    Props { member: WebAccountFragment; } export const WebAccount: React.VFC<Props> = ({ member }) => { return ( <div className={styles.hoge}> {user.__typename === 'Designer' && <span>Adobe ID: {member.adobeID} </span>} <GitHubName member={member} /> </div> ); }; fragment GitHubName on Member { id githubID } 👆コンポーネント定義側 fragment WebAccount on Member { id githubID ... on Designer { adobeID } } … fragment名で、fragmentに定義され ているデータを持ってくることができる
  7. GraphQLとフロントエンド • fragmentで引いた値をそのままコンポーネントに渡すことができる ◦ GraphQLスキーマとコンポーネント (UI)がうまく対応できるようにスキーマを考える必要がある 👆コンポーネント使う側 👆コンポーネントの引数 memberにfragmentで引いた値をいれるだけでよい interface

    Props { member: WebAccountFragment; } export const WebAccount: React.VFC<Props> = ({ member }) => { return ( <div className={styles.hoge}> {user.__typename === 'Designer' && <span>Adobe ID: {member.adobeID} </span>} <GitHubName member={member} /> </div> ); }; fragment GitHubName on Member { id githubID } 👆コンポーネント定義側 fragment WebAccount on Member { id githubID ... on Designer { adobeID } } コンポーネントに必要な fragmentを引い ているので、そのままオブジェクトを渡す ことができる
  8. GraphQLとフロントエンド • fragmentで引いた値をそのままコンポーネントに渡すことができる ◦ GraphQLスキーマとコンポーネント (UI)がうまく対応できるようにスキーマを考える必要がある 👆コンポーネント使う側 👆コンポーネントの引数 memberにfragmentで引いた値をいれるだけでよい interface

    Props { member: WebAccountFragment; } export const WebAccount: React.VFC<Props> = ({ member }) => { return ( <div className={styles.hoge}> {user.__typename === 'Designer' && <span>Adobe ID: {member.adobeID} </span>} <GitHubName member={member} /> </div> ); }; fragment GitHubName on Member { id githubID } 👆コンポーネント定義側 fragment WebAccount on Member { id githubID ... on Designer { adobeID } } Interfaceが 特定の型だったときのみ 持ってくるデータが書ける … on Designer と対応した記述ができる (designerだったらAdobe IDを表示する)
  9. GraphQLまとめ • GraphQLにはスキーマがある • スキーマにはInterface、Interfaceの実装の型がある • フィールドはnullableとnon-nullableのものがある • フロントエンドではgraphqlのfragmentを使うとコンポーネント内に必要なデータを 閉じ込めることができる

    ◦ コンポーネントに1オブジェクトだけ渡せば済むようになり最高 • Interfaceが特定の型だったときに色々する条件がかける ◦ 特定のデータを持ってくる、型で分岐が書ける
  10. 検討1: 1つの型で表現仕切る • pros ◦ 1つの型で表現仕切ることができる ▪ DBスキーマと対応 ◦ nullableな型をうまく利用している

    • cons ◦ スキーマ上で「読めない限定近況ノートの場合、必ず本文にアクセスできないこと」 が表現できない ▪ ともすればアクセスできてしまうのではという不安 ◦ 「限定近況ノートでかつ読めない場合」をフロントで表現しようとすると、判断に必要 なフィールドが多い ▪ 型レベルで読める読めないの判断がしづらい • フロントエンドはTypeScriptで開発しているので型を活かしたい
  11. 検討2: 普通/限定の近況ノートで型を分ける interface AbstractUserNewsEntry { id: ID! title: String! }

    # 普通の近況ノート type NormalUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がNormal用 body: String! } # 限定近況ノート type LimitedUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がNormal用 body: String # ないケースもあるので null許容 }
  12. 検討2: 普通/限定の近況ノートで型を分ける interface AbstractUserNewsEntry { id: ID! title: String! }

    # 普通の近況ノート type NormalUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がNormal用 body: String! } # 限定近況ノート type LimitedUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がNormal用 body: String # ないケースもあるので null許容 } 共通してアクセス可能な部 分をInterfaceに集約する
  13. 検討2: 普通/限定の近況ノートで型を分ける interface AbstractUserNewsEntry { id: ID! title: String! }

    # 普通の近況ノート type NormalUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がNormal用 body: String! } # 限定近況ノート type LimitedUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がNormal用 body: String # ないケースもあるので null許容 } 普通の近況ノートの場合は 本文は読めるのでbodyは nonnullable
  14. 検討2: 普通/限定の近況ノートで型を分ける interface AbstractUserNewsEntry { id: ID! title: String! }

    # 普通の近況ノート type NormalUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がNormal用 body: String! } # 限定近況ノート type LimitedUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がNormal用 body: String # ないケースもあるので null許容 } 対して限定近況ノートの場合 は読めない可能性があるの で本文はnull許容
  15. 検討2: 普通/限定の近況ノートで型を分ける • pros ◦ 型レベルで普通か限定かを判断できる ▪ 共通部分はInterface化しているので扱いやすい ◦ nullableな値を活かしている

    • cons ◦ 限定近況ノートの場合、bodyがnullかどうかで読めるかどうかを判定 する必要がある ◦ 画一的にbodyにアクセスしたい場合、分岐が複雑
  16. このUIを実現しようとするとエラーが • フロントこのUIを満たすfragmentを定義したところバチバチに怒られる ◦ 同じbodyというフィールドなのに型が String!とStringで違うから無理!!! ◦ ◦ ◦ •

    Interfaceの実装の型において同名のフィールドは同じ型の方が望ましそう ◦ 読めない限定近況ノートの場合は bodyがnullの可能性がある ◦ 今回のスキーマのままいくと、普通の近況ノートでも bodyをString!にしなければならない • 今回は明らかにこのスキーマだと作りたいUIが表現できないので再考 GraphQLDocumentError: Fields "body" conflict because they return conflicting types "String!" and "String". Use different aliases on the fields to fetch both if this was intentional.
  17. 検討3: 限定近況ノートを読める/読めないで分ける interface AbstractUserNewsEntry { id: ID! title: String! }

    type NormalUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! #近況ノート本文 body: String! } type LimitedUserNewsEntryCanRead implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がLimitedUserNewsEntryCanRead用 body: String! } type LimitedUserNewsEntryCanNotRead implements AbstractUserNewsEntry { id: ID! title: String! } 公開情報をinterfaceに
  18. 検討3: 限定近況ノートを読める/読めないで分ける interface AbstractUserNewsEntry { id: ID! title: String! }

    type NormalUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! #近況ノート本文 body: String! } type LimitedUserNewsEntryCanRead implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がLimitedUserNewsEntryCanRead用 body: String! } type LimitedUserNewsEntryCanNotRead implements AbstractUserNewsEntry { id: ID! title: String! } 普通の近況ノートは必ず 本文にアクセスできる
  19. 検討3: 限定近況ノートを読める/読めないで分ける interface AbstractUserNewsEntry { id: ID! title: String! }

    type NormalUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! #近況ノート本文 body: String! } type LimitedUserNewsEntryCanRead implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がLimitedUserNewsEntryCanRead用 body: String! } type LimitedUserNewsEntryCanNotRead implements AbstractUserNewsEntry { id: ID! title: String! } 読める近況ノートも同様 に本文にアクセスできる
  20. 検討3: 限定近況ノートを読める/読めないで分ける interface AbstractUserNewsEntry { id: ID! title: String! }

    type NormalUserNewsEntry implements AbstractUserNewsEntry { id: ID! title: String! #近況ノート本文 body: String! } type LimitedUserNewsEntryCanRead implements AbstractUserNewsEntry { id: ID! title: String! # ここから下がLimitedUserNewsEntryCanRead用 body: String! } type LimitedUserNewsEntryCanNotRead implements AbstractUserNewsEntry { id: ID! title: String! } 読める近況ノートも同様 に本文にアクセスできる 読めない限定近況ノート はInterfaceの内容(公開 情報)しかアクセス出来 ない
  21. interface AbstractUserNewsEntry { id: ID! author: User! title: String! #

    限定近況ノートであるかどうか isLimited: Boolean! } # 本文を読むことができる近況ノート type ReadableUserNewsEntry implements AbstractUserNewsEntry { id: ID! author: User! title: String! # 限定近況ノートであるかどうか isLimited: Boolean! # 以下 ReadableUserNewsEntry用 # 近況ノート本文 body: String! } # 本文を読むことができない近況ノート # 公開情報しか参照することができない type UnreadableUserNewsEntry implements AbstractUserNewsEntry { id: ID! author: User! title: String! # 限定近況ノートであるかどうか isLimited: Boolean! }
  22. interface AbstractUserNewsEntry { id: ID! author: User! title: String! #

    限定近況ノートであるかどうか isLimited: Boolean! } # 本文を読むことができる近況ノート type ReadableUserNewsEntry implements AbstractUserNewsEntry { id: ID! author: User! title: String! # 限定近況ノートであるかどうか isLimited: Boolean! # 以下 ReadableUserNewsEntry用 # 近況ノート本文 body: String! } # 本文を読むことができない近況ノート # 公開情報しか参照することができない type UnreadableUserNewsEntry implements AbstractUserNewsEntry { id: ID! author: User! title: String! # 限定近況ノートであるかどうか isLimited: Boolean! } 常にアクセスできる公開情報を Interfaceとして定義
  23. interface AbstractUserNewsEntry { id: ID! author: User! title: String! #

    限定近況ノートであるかどうか isLimited: Boolean! } # 本文を読むことができる近況ノート type ReadableUserNewsEntry implements AbstractUserNewsEntry { id: ID! author: User! title: String! # 限定近況ノートであるかどうか isLimited: Boolean! # 以下 ReadableUserNewsEntry用 # 近況ノート本文 body: String! } # 本文を読むことができない近況ノート # 公開情報しか参照することができない type UnreadableUserNewsEntry implements AbstractUserNewsEntry { id: ID! author: User! title: String! # 限定近況ノートであるかどうか isLimited: Boolean! } 常にアクセスできる公開情報を Interfaceとして定義 限定近況ノートであるかどうかは Booleanのフィールドとして定義
  24. interface AbstractUserNewsEntry { id: ID! author: User! title: String! #

    限定近況ノートであるかどうか isLimited: Boolean! } # 本文を読むことができる近況ノート type ReadableUserNewsEntry implements AbstractUserNewsEntry { id: ID! author: User! title: String! # 限定近況ノートであるかどうか isLimited: Boolean! # 以下 ReadableUserNewsEntry用 # 近況ノート本文 body: String! } # 本文を読むことができない近況ノート # 公開情報しか参照することができない type UnreadableUserNewsEntry implements AbstractUserNewsEntry { id: ID! author: User! title: String! # 限定近況ノートであるかどうか isLimited: Boolean! } 読める近況ノート == 必ず本文に アクセスできるのでnonnullで宣言 している
  25. interface AbstractUserNewsEntry { id: ID! author: User! title: String! #

    限定近況ノートであるかどうか isLimited: Boolean! } # 本文を読むことができる近況ノート type ReadableUserNewsEntry implements AbstractUserNewsEntry { id: ID! author: User! title: String! # 限定近況ノートであるかどうか isLimited: Boolean! # 以下 ReadableUserNewsEntry用 # 近況ノート本文 body: String! } # 本文を読むことができない近況ノート # 公開情報しか参照することができない type UnreadableUserNewsEntry implements AbstractUserNewsEntry { id: ID! author: User! title: String! # 限定近況ノートであるかどうか isLimited: Boolean! } 読める近況ノート == 必ず本文に アクセスできるのでnonnullで宣言 している 読めない近況ノートは公開範囲 (Intefaceに定義されている内容 )し かアクセスできない
  26. 検討4: 読める/読めないでの型定義 • pros ◦ 読めない場合にアクセスできない範囲がスキーマで十分表現できてい る ▪ 逆に読める場合にアクセスできる範囲も表現できている ◦

    型の恩恵がフロントエンドで得やすくなった ▪ 例えば「読める場合は特定のUIをだす」のような分岐が簡単 ◦ 読める/読めない判定が完全にバックエンドの責務になっている ◦ 型での表現とフィールドを使った表現のバランスを取っている • cons ◦ Interfaceっぽくはない使い方....
  27. まとめ • 素朴にDBスキーマと合わせるのが必ずしも正解ではない • 型で表現するのとフィールド(素朴なbooleanなど)で表現した方が自然なのかはモ ノによって異なる ◦ 近況ノートでは読めるかどうかは型にすることでアクセス制限が作れた ◦ 限定近況ノートかどうかはフィールドで判断することで素直にアクセス出来た

    • スキーマの構成はフロントエンドの使われ方も考慮する必要がある ◦ 実現したいUIをスキーマで素朴に表現できるのか ◦ 使ってみるとエラーが出るケースがある ◦ バックエンド/フロントエンドの責務とスキーマが自然に対応しているか