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. GraphQL

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

    View full-size slide

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

        ・技術戦略、アーキテクト、認証基盤
    ・Hobby Guitarist (2020/06~)
    ・Student @ Univ. Tsukuba (2019/04~)

        ・情報科学類
    自己紹介

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  5. これから GraphQL API を作るときの大失敗を避ける
    GraphQL はプロトコルなので後方互換性が必要。ミスっても無邪気にリファクタしにくい。

    クライアント/サーバー双方の都合を理解して設計するための最低限を知る
    本セッションの目標
    話さないこと
    “ GraphQL そのものの紹„
    “ クライアント/サーバーの設計や実装詳r
    “ モデリング一般の話題

    View full-size slide

  6. Apollo Client を前提に話します
    圧倒的にシェアが大きいし、iOS / Android / Web 共通

    ただし、一部 Relay や urql にも触れる
    npmtrends.com

    View full-size slide

  7. どこまで厳密にやるかは要件次第。落とし所は必要

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

    View full-size slide

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

    View full-size slide

  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回リクエストを送らないと

    会社名に辿り着けない

    View full-size slide

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

    View full-size slide

  11. リソースをグラフ構造にする MUST
    Q: Company が不要な場合に、無駄な計算コストが発生しないか?
    A: フィールド単位のリゾルバによって回避できる
    const =
    return
    {

    User: {

    ( ) {

    companyRepository. (parent.companyId);

    },

    },

    };
    resolvers
    company
    find
    parent
    query {

    ( : ) {

    }

    }
    user id
    id

    name

    "yukukotani"
    リゾルバ定義 オペレーション
    company フィールドを含んでいないので

    リゾルバは実行されない

    View full-size slide

  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`

    View full-size slide

  13. グローバルにユニークなIDを振る MAY
    Apollo Client ではクエリ単位で再フェッチをするため、

    無理に準拠する必要はない
    Relay ではフラグメント単位の再フェッチなどのために必須となっている。

    Relay 以外でも `node` クエリで任意のリソースを取得できるのは開発時に役立つ

    View full-size slide

  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}` を

    キーとして参照グラフを形成

    View full-size slide

  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 の実体は残る
    ミューテーションの結果が

    自動で反映される

    View full-size slide

  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

    クエリ単位で再フェッチ
    ミューテーションと同様に

    クエリ結果は自動反映

    View full-size slide

  17. コラム: 各クライアントのキャッシュ機構
    Relayのキャッシュ機構
    Apollo のように正規化されたグラフキャッシュを持つ。

    加えて、`node` クエリによって任意のリソース単位の再フェッチが可能
    urqlのキャッシュ機構
    デフォルトでは、クエリ(と与えた変数)をキーとした、

    1階層のシンプルな Key-Value キャッシュを持つ。

    `id` は見ずに、ミューテーションによって更新された型を Value に含むキャッシュを全て破棄。

    `@urql/exchange-graphcache` を導入することで Apollo に似たグラフキャッシュを導入できる

    View full-size slide

  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 以外に変更すると壊れる

    View full-size slide

  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!

    単にフィールドを追加するだけで

    追加のリソースを返せる

    View full-size slide

  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 がキャッシュに反映される

    View full-size slide

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

    View full-size slide

  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 にしておくと

    後からエラーを追加できる

    View full-size slide

  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 的にキャッチ。

    サーバー側でエラーの種類が増えても壊れなくなる

    View full-size slide

  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 インターフェースを使う

    View full-size slide

  25. スキーマにエラーを含める SHOULD
    Q: トップレベルのエラーはいつ使う?
    A: 個別にハンドルできないネットワークエラー等や

    ユーザーに見せない開発者向けのエラーで使う。

    スキーマを汚さずに済む

    View full-size slide

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

    !

    !

    {

    :
    :
    :
    }
    User
    ID
    Int
    Boolean
    id
    age
    isAdult
    const =
    return >=
    {

    User: {

    ( ) {

    parent.age ;

    },

    },

    };
    resolvers
    20
    isAdult parent
    従来のRESTではオーバーフェッチによる無駄な計算コストを無視できなかったが、

    GraphQLの部分フェッチにより可能になった。

    クライアントはさらにJSON色付けに専念するようになる
    `isAdult`が参照されたときのみ計算。

    この程度の計算コストなら問題ないが、

    DBアクセスが入ると無視できなくなる

    View full-size slide

  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

    View full-size slide

  28. カーソル・ページネーションではConnectionを使う SHOULD
    戻り値を後から Connection にすると破壊的変更になってしまう。

    リストを返す場合は、後々ページネーションが必要にならないか

    予めよく検討する必要がある

    View full-size slide

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

    View full-size slide

  30. まとめ
    ・スキーマを に育てていくために、初期設計が大事

    ・クライアント/サーバー双方の視点で設計しないとすぐに破綻する

    ・世に溢れるプラクティスの背景を理解して、取捨選択していく
    連続的

    View full-size slide

  31. 参考文献
    ・Production Ready GraphQL Book

    ・Production Ready GraphQL Blog

    ・Yelp Schema Design Guidelines

    ・Shopify GraphQL Design Tutorial

    ・GraphQLスキーマ設計ガイド 第2版

    ・GraphQL Client Architecture Recommendation 社外版






    全て必読レベルです

    View full-size slide

  32. スーパー採用中
    Ubieでは以下の職種を募集中です!

    採用サイトをご覧いただくか、懇親会でお声がけください!楽しいよ!
    ・QA エンジニア

    ・ソフトウェアエンジニア(デリバリー&サクセス)

    ・基盤開発エンジニア

    ・SRE

    ・プロダクト開発エンジニア
    https://recruit.ubie.life/engineer

    View full-size slide

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

    View full-size slide