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 • GigaViewer はマルチテナント ◦ 1つのアプリケーション、1つのDBで複数サイト運用

    ◦ 👉 高速にサイトを立ち上げられるように • GraphQL 周りもマルチテナント ◦ 1つのスキーマ、1つの API サーバー ◦ 👉 どう工夫して実現しているか紹介 3
  2. 会員情報ページのコンポーネント 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 クエリを発行して...
  3. 会員情報ページのコンポーネント 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 クエリを発行して... ② エラーハンドリングした後...
  4. 会員情報ページのコンポーネント 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 クエリを発行して... ② エラーハンドリングした後... ③ クエリから取得した ユーザ名、所有ポイントを表示
  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> ); } 8 ① GraphQL クエリを発行して... ② エラーハンドリングした後... ③ クエリから取得した ユーザ名、所有ポイントを表示 👉 どうマルチテナント対応させるか検討していく
  6. こういうことをしたい 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 ③ ポイント機能がある時だけ表示する ① クエリは同じものを使い... ② テンプレートも 共通化して...
  7. 工夫: 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 を付与 ② フラグのないサイトでは エラーを返す ③ フラグのあるサイトでは ポイントを返す
  8. エラーを使った実装の問題点 • クライアントでエラーとして扱われる • エラーハンドリングにより、予期せぬ挙動に function UserInfoPage() { const {

    user, error } = useUserQuery(query); if (error) throw error; // 上位コンポーネントで500ページを render return /* ... */; } 13 😫 ページ全体が見えなくなってしまう
  9. 工夫: サーバーから 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
  10. クライアント側はこう書ける 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 の時だけ要素を表示 😄 ポイント欄だけ出し分けできるように
  11. 工夫: @hasFeature で共通化 type UserAccount { name: String! point: Int

    @hasFeature(features: ["Point"]) } 21 Point feature が必須であることを 明示してやる 👉 フレームワーク側で feature が無ければ 自動で null を返してくれるように
  12. 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; } 😕 前: 😄 後:
  13. まとめ • 3つの工夫を紹介した ◦ Feature Toggle 導入 ◦ エラーを返さず NULL

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