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

マルチテナントで GraphQL を使う際の工夫

mizdra
September 08, 2022

マルチテナントで GraphQL を使う際の工夫

2022/09/07 の Hatena Engineer Seminar #21 で発表した資料です。

動画もあります: https://youtu.be/N6iUlz4buTc?t=1636

この発表とは別に、30 分のクロストーク枠で、@hasFeature 実装の経緯の詳細や、他に検討したことについて話していますので、ぜひそちらもご覧ください: https://youtu.be/N6iUlz4buTc?t=3326

mizdra

September 08, 2022
Tweet

More Decks by mizdra

Other Decks in Programming

Transcript

  1. マルチテナントで GraphQL を使う際の 工夫 id:mizdra 2022/09/07 Hatena Engineer Seminar #21

    1
  2. 自己紹介 • id:mizdra • マンガメディア開発チーム • GigaViewer 作ってます 2

  3. 今日する話 • マルチテナント + GraphQL • GigaViewer はマルチテナント ◦ 1つのアプリケーション、1つのDBで複数サイト運用

    ◦ 👉 高速にサイトを立ち上げられるように • GraphQL 周りもマルチテナント ◦ 1つのスキーマ、1つの API サーバー ◦ 👉 どう工夫して実現しているか紹介 3
  4. 例: 会員情報ページ • ユーザ名と所有ポイン ト数が表示される 4

  5. 会員情報ページのコンポーネント function UserInfoPage() { const { user, error } =

    useUserQuery(query); if (error) throw error; // 上位コンポーネントで500ページを render return ( <div> <div>ユーザー: {user?.name}</div> <div>所有ポイント: {user?.point}pt</div> </div> ); } 5 ① GraphQL クエリを発行して...
  6. 会員情報ページのコンポーネント function UserInfoPage() { const { user, error } =

    useUserQuery(query); if (error) throw error; // 上位コンポーネントで500ページを render return ( <div> <div>ユーザー: {user?.name}</div> <div>所有ポイント: {user?.point}pt</div> </div> ); } 6 ① GraphQL クエリを発行して... ② エラーハンドリングした後...
  7. 会員情報ページのコンポーネント function UserInfoPage() { const { user, error } =

    useUserQuery(query); if (error) throw error; // 上位コンポーネントで500ページを render return ( <div> <div>ユーザー: {user?.name}</div> <div>所有ポイント: {user?.point}pt</div> </div> ); } 7 ① GraphQL クエリを発行して... ② エラーハンドリングした後... ③ クエリから取得した ユーザ名、所有ポイントを表示
  8. 会員情報ページのコンポーネント function UserInfoPage() { const { user, error } =

    useUserQuery(query); if (error) throw error; // 上位コンポーネントで500ページを render return ( <div> <div>ユーザー: {user?.name}</div> <div>所有ポイント: {user?.point}pt</div> </div> ); } 8 ① GraphQL クエリを発行して... ② エラーハンドリングした後... ③ クエリから取得した ユーザ名、所有ポイントを表示 👉 どうマルチテナント対応させるか検討していく
  9. マルチテナント特有の要件 • 20サイト分重複が発生するのは避けたい ◦ 似たようで違うものが20個あったら大変 ◦ 1つに共通化したい • ポイント機能の無いサイトも扱えるように ◦

    そうしたサイトでは「ポイント数」を非表示にした い! 9
  10. こういうことをしたい function UserInfoPage() { const { user, error } =

    useUserQuery(query); if (error) throw error; return ( <div> <div>ユーザー: {user?.name}</div> {isSupported(user?.point) && <div>所有ポイント: {user.point}pt</div>} </div> ); } 10 ③ ポイント機能がある時だけ表示する ① クエリは同じものを使い... ② テンプレートも 共通化して...
  11. 11 工夫 ① Feature Toogle を使う

  12. 工夫: Feature Toogle を使う 12 # GraphQL Resolver の挙動を出し分ける sub

    point { my ($user_account_record, $ctx) = @_; return gql->error('Point not supported') unless $ctx->media->has_feature('Point'); return $user_account_record->point; } Point, Rental Point, Comment Subscription サイト1 サイト2 サイト3 ① 対応サイトに Point Feature を付与 ② フラグのないサイトでは エラーを返す ③ フラグのあるサイトでは ポイントを返す
  13. エラーを使った実装の問題点 • クライアントでエラーとして扱われる • エラーハンドリングにより、予期せぬ挙動に function UserInfoPage() { const {

    user, error } = useUserQuery(query); if (error) throw error; // 上位コンポーネントで500ページを render return /* ... */; } 13 😫 ページ全体が見えなくなってしまう
  14. 14 工夫 ② サーバーから null を返す

  15. 工夫: サーバーから null を返す • エラーではなく null を返すように ◦ クライアントでエラーとして扱われなくなる

    • field のスキーマは nullable にする sub point { my ($user_account_record, $ctx) = @_; return gql->null unless $ctx->media->has_feature('Point'); return $user_account_record->point; } 15
  16. クライアント側はこう書ける function UserInfoPage() { const { user, error } =

    useUserQuery(query); if (error) throw error; return ( <div> <div>ユーザー: {user?.name}</div> {user?.point ?? <div>所有ポイント: {user.point}pt</div>} </div> ); } 16 null チェックをして、 非 null の時だけ要素を表示 😄 ポイント欄だけ出し分けできるように
  17. 17 めでたしめでたし

  18. 18 …

  19. 19 もう一声欲しくない?

  20. 同じコードが頻出してイマイチ • 特に feature の有無のチェック ◦ return gql->null unless $ctx->media->has_feature(...)

    • プログラマの美徳的に避けたい • 共通化したい 20
  21. 工夫: @hasFeature で共通化 type UserAccount { name: String! point: Int

    @hasFeature(features: ["Point"]) } 21 Point feature が必須であることを 明示してやる 👉 フレームワーク側で feature が無ければ 自動で null を返してくれるように
  22. Resolver 側はスッキリ sub point { my ($user_account_record, $ctx) = @_;

    return $user_account_record->point; } 22 sub point { my ($user_account_record, $ctx) = @_; return gql->null unless $context->media->has_feature('Point'); return $user_account_record->point; } 😕 前: 😄 後:
  23. まとめ • 3つの工夫を紹介した ◦ Feature Toggle 導入 ◦ エラーを返さず NULL

    を返す ◦ @hasFeature 導入 • これにより... ◦ 1つのスキーマ・コードで複数サイトを運用 ◦ 開発速度やメンテナンス性を高く保つ 23