Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

今日する話 ● マルチテナント + GraphQL ● GigaViewer はマルチテナント ○ 1つのアプリケーション、1つのDBで複数サイト運用 ○ 👉 高速にサイトを立ち上げられるように ● GraphQL 周りもマルチテナント ○ 1つのスキーマ、1つの API サーバー ○ 👉 どう工夫して実現しているか紹介 3

Slide 4

Slide 4 text

例: 会員情報ページ ● ユーザ名と所有ポイン ト数が表示される 4

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

会員情報ページのコンポーネント function UserInfoPage() { const { user, error } = useUserQuery(query); if (error) throw error; // 上位コンポーネントで500ページを render return (
ユーザー: {user?.name}
所有ポイント: {user?.point}pt
); } 8 ① GraphQL クエリを発行して... ② エラーハンドリングした後... ③ クエリから取得した ユーザ名、所有ポイントを表示 👉 どうマルチテナント対応させるか検討していく

Slide 9

Slide 9 text

マルチテナント特有の要件 ● 20サイト分重複が発生するのは避けたい ○ 似たようで違うものが20個あったら大変 ○ 1つに共通化したい ● ポイント機能の無いサイトも扱えるように ○ そうしたサイトでは「ポイント数」を非表示にした い! 9

Slide 10

Slide 10 text

こういうことをしたい function UserInfoPage() { const { user, error } = useUserQuery(query); if (error) throw error; return (
ユーザー: {user?.name}
{isSupported(user?.point) &&
所有ポイント: {user.point}pt
}
); } 10 ③ ポイント機能がある時だけ表示する ① クエリは同じものを使い... ② テンプレートも 共通化して...

Slide 11

Slide 11 text

11 工夫 ① Feature Toogle を使う

Slide 12

Slide 12 text

工夫: 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 を付与 ② フラグのないサイトでは エラーを返す ③ フラグのあるサイトでは ポイントを返す

Slide 13

Slide 13 text

エラーを使った実装の問題点 ● クライアントでエラーとして扱われる ● エラーハンドリングにより、予期せぬ挙動に function UserInfoPage() { const { user, error } = useUserQuery(query); if (error) throw error; // 上位コンポーネントで500ページを render return /* ... */; } 13 😫 ページ全体が見えなくなってしまう

Slide 14

Slide 14 text

14 工夫 ② サーバーから null を返す

Slide 15

Slide 15 text

工夫: サーバーから 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

Slide 16

Slide 16 text

クライアント側はこう書ける function UserInfoPage() { const { user, error } = useUserQuery(query); if (error) throw error; return (
ユーザー: {user?.name}
{user?.point ??
所有ポイント: {user.point}pt
}
); } 16 null チェックをして、 非 null の時だけ要素を表示 😄 ポイント欄だけ出し分けできるように

Slide 17

Slide 17 text

17 めでたしめでたし

Slide 18

Slide 18 text

18 …

Slide 19

Slide 19 text

19 もう一声欲しくない?

Slide 20

Slide 20 text

同じコードが頻出してイマイチ ● 特に feature の有無のチェック ○ return gql->null unless $ctx->media->has_feature(...) ● プログラマの美徳的に避けたい ● 共通化したい 20

Slide 21

Slide 21 text

工夫: @hasFeature で共通化 type UserAccount { name: String! point: Int @hasFeature(features: ["Point"]) } 21 Point feature が必須であることを 明示してやる 👉 フレームワーク側で feature が無ければ 自動で null を返してくれるように

Slide 22

Slide 22 text

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; } 😕 前: 😄 後:

Slide 23

Slide 23 text

まとめ ● 3つの工夫を紹介した ○ Feature Toggle 導入 ○ エラーを返さず NULL を返す ○ @hasFeature 導入 ● これにより... ○ 1つのスキーマ・コードで複数サイトを運用 ○ 開発速度やメンテナンス性を高く保つ 23