$30 off During Our Annual Pro Sale. View Details »

マルチテナントで 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  5. 会員情報ページのコンポーネント
    function UserInfoPage() {
    const { user, error } = useUserQuery(query);
    if (error) throw error; // 上位コンポーネントで500ページを render
    return (

    ユーザー: {user?.name}
    所有ポイント: {user?.point}pt

    );
    }
    5
    ① GraphQL クエリを発行して...

    View Slide

  6. 会員情報ページのコンポーネント
    function UserInfoPage() {
    const { user, error } = useUserQuery(query);
    if (error) throw error; // 上位コンポーネントで500ページを render
    return (

    ユーザー: {user?.name}
    所有ポイント: {user?.point}pt

    );
    }
    6
    ① GraphQL クエリを発行して...
    ② エラーハンドリングした後...

    View Slide

  7. 会員情報ページのコンポーネント
    function UserInfoPage() {
    const { user, error } = useUserQuery(query);
    if (error) throw error; // 上位コンポーネントで500ページを render
    return (

    ユーザー: {user?.name}
    所有ポイント: {user?.point}pt

    );
    }
    7
    ① GraphQL クエリを発行して...
    ② エラーハンドリングした後...
    ③ クエリから取得した
    ユーザ名、所有ポイントを表示

    View Slide

  8. 会員情報ページのコンポーネント
    function UserInfoPage() {
    const { user, error } = useUserQuery(query);
    if (error) throw error; // 上位コンポーネントで500ページを render
    return (

    ユーザー: {user?.name}
    所有ポイント: {user?.point}pt

    );
    }
    8
    ① GraphQL クエリを発行して...
    ② エラーハンドリングした後...
    ③ クエリから取得した
    ユーザ名、所有ポイントを表示
    👉 どうマルチテナント対応させるか検討していく

    View Slide

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

    View Slide

  10. こういうことをしたい
    function UserInfoPage() {
    const { user, error } = useUserQuery(query);
    if (error) throw error;
    return (

    ユーザー: {user?.name}
    {isSupported(user?.point)
    && 所有ポイント: {user.point}pt}

    );
    }
    10
    ③ ポイント機能がある時だけ表示する
    ① クエリは同じものを使い...
    ② テンプレートも
    共通化して...

    View Slide

  11. 11
    工夫 ①
    Feature Toogle を使う

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  16. クライアント側はこう書ける
    function UserInfoPage() {
    const { user, error } = useUserQuery(query);
    if (error) throw error;
    return (

    ユーザー: {user?.name}
    {user?.point ?? 所有ポイント: {user.point}pt}

    );
    }
    16
    null チェックをして、
    非 null の時だけ要素を表示
    😄 ポイント欄だけ出し分けできるように

    View Slide

  17. 17
    めでたしめでたし

    View Slide

  18. 18

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide