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

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

Ffd3f0ebea474176dfbe876216a793f9?s=47 AnaTofuZ
January 26, 2022

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

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

Ffd3f0ebea474176dfbe876216a793f9?s=128

AnaTofuZ

January 26, 2022
Tweet

More Decks by AnaTofuZ

Other Decks in Technology

Transcript

  1. GraphQLスキーマの設計で 考えたこと 2022/01/26 Hatena Engineer Seminar #18 id:AnaTofuZ

  2. 今日の内容

  3. GraphQLスキーマの設計は難しい

  4. 今日の内容 • GraphQLスキーマの設計は難しい • DBスキーマと合わせて設計するのが正解? それとも? • 型やフィールドで何を表現するか • バックエンド、フロントエンドの事情とスキーマの表現

  5. こんにちは • id:AnaTofuZ • Webアプリケーションエンジニア • 2021年新卒 ◦ それまでは沖縄にいました ◦

    出身は山梨です... • アカウント名はアナグラです
  6. もくじ • GraphQLとは • カクヨムとGraphQL • 1から考えるスキーマ設計 • まとめ

  7. GraphQL • Facebookによって作られたクエリ言語 • GraphQLスキーマがある • スキーマの内扱いたい情報をfragmentで部分的に宣言できる • オブジェクトの取得はQuery/ 更新はMutationで行う

    • 詳しくは ◦ GraphQL Highway
  8. GraphQL • Facebookによって作られたクエリ言語 • GraphQLスキーマがある • スキーマの内扱いたい情報をfragmentで部分的に宣言できる • オブジェクトの取得はQuery/ 更新はMutationで行う

    • 詳しくは ◦ GraphQL Highway
  9. 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! }
  10. 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! }
  11. GraphQLのfragment • GraphQLはfragmentを使うと、必要なデータを 宣言的に定義することができる • 👉ではInterface Memberの中から必要なデータを WebAccountとして宣言している ◦ 全MemberはgithubIDを取得できる

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

    スキーマ上の型がTypeScriptの型に直接対応する ◦ InterfaceやInterfaceの実装の型はいい感じにTypeScriptの型表現にマッピングされる ◦ nullable, non-nullableな型も考慮される • 先述のfragmentをうまく使うと、コンポーネントに必要なデータをモジュール化できる ◦ fragmentはコンポーネントに閉じるので、具体的に何が必要か使う側では知らずに渡せる
  13. 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 } }
  14. 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に定義され ているデータを持ってくることができる
  15. 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を引い ているので、そのままオブジェクトを渡す ことができる
  16. 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を表示する)
  17. GraphQLまとめ • GraphQLにはスキーマがある • スキーマにはInterface、Interfaceの実装の型がある • フィールドはnullableとnon-nullableのものがある • フロントエンドではgraphqlのfragmentを使うとコンポーネント内に必要なデータを 閉じ込めることができる

    ◦ コンポーネントに1オブジェクトだけ渡せば済むようになり最高 • Interfaceが特定の型だったときに色々する条件がかける ◦ 特定のデータを持ってくる、型で分岐が書ける
  18. もくじ • GraphQLとは • カクヨムとGraphQL • 1から考えるスキーマ設計 • 動かしながらスキーマを変更する •

    まとめ
  19. カクヨム • KADOKAWA様とはてなで共同開発しているWeb小説サイト ◦ Webアプリ以外にスマホアプリも提供している

  20. カクヨムとGraphQL • スマホアプリで先行してGraphQLを利用 • 最近Webサイト部分でもGraphQLを利用しはじめた

  21. もくじ • GraphQLとは • カクヨムとGraphQL • 1から考えるスキーマ設計 • 動かしながらスキーマを変更する •

    まとめ
  22. 1から考えるスキーマ設計 • カクヨムの「近況ノート」に新機能を提供予定 • この新機能を加味したGraphQLスキーマを1から考えた ◦ 今日はこのスキーマ設計で考えたことを話します

  23. 近況ノートとは • 近況ノート ◦ 小説の紹介や更新情報、読書履歴やおすすめ作品の紹介を投稿でき る機能 ◦ 最近画像が投稿できるようになった

  24. カクヨムロイヤルティプログラム第二弾

  25. 限定近況ノート • 普通の近況ノートは誰でも読める • 限定近況ノートは読めないユーザーもいる • 共通して次のようなUIを作りたい

  26. 作りたいUI例 タイトル 本文です タイトル [限定] タイトル 本文です [限定]

  27. 作りたいUI例 タイトル 本文です タイトル [限定] タイトル 本文です [限定] 普通の近況ノートは 本文が読める

  28. 作りたいUI例 タイトル 本文です タイトル [限定] タイトル 本文です [限定] 読めない限定近況ノートはタ イトル

    + 限定であることがわ かる
  29. 作りたいUI例 タイトル 本文です タイトル [限定] タイトル 本文です [限定] 読める限定近況ノートは 本文と限定であることが表示

    される
  30. 👉GraphQLスキーマの前に DBスキーマを確認

  31. データベース上の限定近況ノートの扱い • データベース上では限定近況ノートかそうでないかは、近況ノートのテーブルのフラ グで管理している ◦ 実際にこの近況ノートが読めるかどうかはバックエンドで管理している • データベースのスキーマとGraphQLのスキーマを 一致させる目線に立つと、1つの型で表現する必要 性が出てくる

    id title body is_limited author_account_id
  32. 👉GraphQLスキーマを設計していくぞ!!!

  33. 今日の内容 • GraphQLスキーマの設計は難しい • DBスキーマと合わせて設計するのが正解? それとも? • 型やフィールドで何を表現するか • バックエンド、フロントエンドの事情とスキーマの表現

  34. 検討1: 1つの型で表現仕切る • DBの近況ノートのスキーマをGraphQLで表現する形

  35. 検討1: 1つの型で表現仕切る • DBの近況ノートのスキーマをGraphQLで表現する形 type UserNewsEntry { id: ID! title:

    String! body: String isLimited: bool! visitorCanRead: bool! }
  36. 検討1: 1つの型で表現仕切る • DBの近況ノートのスキーマをGraphQLで表現する形 ◦ 読めないケースもあるので bodyをnull許容 ◦ 閲覧者が読めるかどうかを別でもたせた type

    UserNewsEntry { id: ID! title: String! body: String isLimited: bool! visitorCanRead: bool! }
  37. 検討1: 1つの型で表現仕切る • pros ◦ 1つの型で表現仕切ることができる ▪ DBスキーマと対応 ◦ nullableな型をうまく利用している

    • cons ◦ スキーマ上で「読めない限定近況ノートの場合、必ず本文にアクセスできないこと」 が表現できない ▪ ともすればアクセスできてしまうのではという不安 ◦ 「限定近況ノートでかつ読めない場合」をフロントで表現しようとすると、判断に必要 なフィールドが多い ▪ 型レベルで読める読めないの判断がしづらい • フロントエンドはTypeScriptで開発しているので型を活かしたい
  38. 検討2: 普通/限定の近況ノートで型を分ける • 1つの型ではなくて型を分ける方針で考えた • 今回の型定義は普通/限定に着目した

  39. 検討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許容 }
  40. 検討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に集約する
  41. 検討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
  42. 検討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許容
  43. 検討2: 普通/限定の近況ノートで型を分ける • pros ◦ 型レベルで普通か限定かを判断できる ▪ 共通部分はInterface化しているので扱いやすい ◦ nullableな値を活かしている

    • cons ◦ 限定近況ノートの場合、bodyがnullかどうかで読めるかどうかを判定 する必要がある ◦ 画一的にbodyにアクセスしたい場合、分岐が複雑
  44. ヨッシャこれで決まり!!

  45. ヨッシャこれで決まり!! ではない!!!

  46. 作りたいUI例 タイトル 本文です タイトル [限定] タイトル 本文です [限定]

  47. 作りたいUI例 タイトル 本文です タイトル [限定] タイトル 本文です [限定] body: String!

  48. 作りたいUI例 タイトル 本文です タイトル [限定] タイトル 本文です [限定] body: String!

    body: String
  49. 作りたいUI例 タイトル 本文です タイトル [限定] タイトル 本文です [限定] body: String!

    body: String bodyの型が コンフリクト
  50. この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.
  51. 検討3: 限定近況ノートを読める/読めないで分ける • フィールドの型を同じにしたい • 極力型を使って状態を表現したい ◦ 近況ノートと閲覧者の状況を素直に型にするのを考えた • 状態を型にマッピングすることを考えた

  52. 検討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に
  53. 検討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! } 普通の近況ノートは必ず 本文にアクセスできる
  54. 検討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! } 読める近況ノートも同様 に本文にアクセスできる
  55. 検討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の内容(公開 情報)しかアクセス出来 ない
  56. 検討3: 限定近況ノートを読める/読めないで分ける • pros ◦ 読めない場合にアクセスできない範囲がスキーマで十分表現できてい る ▪ 逆に読める場合にアクセスできる範囲も表現できている ◦

    型の状況と近況ノートの状態がマッチしている • cons ◦ 型が多い!!!!!!
  57. 型が多いことによる問題 Normal LimitedCanRead Limited CanNotRead 読める近況ノート 限定近況ノート

  58. 型が多いことによる問題 Normal LimitedCanRead Limited CanNotRead 読める近況ノート 限定近況ノート 型と状態のグループ分けをバックエンド /フロントエンド共に行わないといけない !!!

    👉 状態を決定する責務がバックエンド /フロントに散ってしまう
  59. 再考

  60. 検討4: 読める/読めないでの型定義 • 閲覧者が対象の近況ノートを読めるか読めないかで型を分けることを考えた • 限定近況ノートはフィールドで表現する

  61. 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! }
  62. 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として定義
  63. 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のフィールドとして定義
  64. 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で宣言 している
  65. 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に定義されている内容 )し かアクセスできない
  66. 検討4: 読める/読めないでの型定義 • pros ◦ 読めない場合にアクセスできない範囲がスキーマで十分表現できてい る ▪ 逆に読める場合にアクセスできる範囲も表現できている ◦

    型の恩恵がフロントエンドで得やすくなった ▪ 例えば「読める場合は特定のUIをだす」のような分岐が簡単 ◦ 読める/読めない判定が完全にバックエンドの責務になっている ◦ 型での表現とフィールドを使った表現のバランスを取っている • cons ◦ Interfaceっぽくはない使い方....
  67. フロントエンドでも安心 • bodyにアクセスしたい場合は必ずReadableであることを示す必要がでる ◦ 型の絞り込みをせずにフィールドにアクセスするとエラーになる ◦ TypeScriptで開発している限りは型で安心してコードが書ける

  68. 最終的なスキーマ • 「読める/読めないで型を分ける」を採用した ◦ バックエンドは閲覧者の状況によって型をいい感じに返すように ◦ 型を分けたことで必ず公開情報しかクライアントに渡らないような安心感が得られた • 今日プロフィールページがReactで動くようになったので実際に動作するように

  69. もくじ • GraphQLとは • カクヨムとGraphQL • 1から考えるスキーマ設計 • まとめ

  70. まとめ • 素朴にDBスキーマと合わせるのが必ずしも正解ではない • 型で表現するのとフィールド(素朴なbooleanなど)で表現した方が自然なのかはモ ノによって異なる ◦ 近況ノートでは読めるかどうかは型にすることでアクセス制限が作れた ◦ 限定近況ノートかどうかはフィールドで判断することで素直にアクセス出来た

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