STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

一括登録機能実装における設計と実装

はじめに

STORES 予約 で Web エンジニアをしている osd です。

今回は STORES 予約 で店舗一括登録を実装した際の設計、実装についてお話しします。

前提

なぜ必要とされているのか

STORES 予約 では、事業者が店舗を追加する毎に開設手続きを行う必要があります。具体的にはその際に

  • 店舗情報の登録
  • 予約での店舗情報(業種/id 設定など)

の操作を店舗数分行う必要があります。大規模な事業者になると 3 桁店舗数の作成が必要になり、開設工数の増加やミスが発生しやすい状況になります。

また、店舗ごとに予約ページ設定やスタッフ登録などの設定が必要になるため、店舗登録以外の一括設定機能も要望されており、その前段として店舗一括登録機能が必要とされていました。

予約のストア構造

現在 STORES 予約 では事業者 / 店舗が作成でき、事業者には複数の店舗が紐づく構造になっています。

事業者 / 店舗情報は 組織管理基盤システム(組織管理基盤システム についての記事)上で管理されており、STORES 予約 では 組織管理基盤システム 上での変更を subscribe してキャッシュテーブルに反映することで予約独自の組織情報と統合して組織情報を管理しています。

erDiagram
    "組織管理基盤:ORGANIZATION" {
        int id PK
        string name
    }
    "組織管理基盤:STORE" {
        int id PK
        string name
        string location
        int organization_id FK
    }

    "組織管理基盤:ORGANIZATION" ||--o{ "組織管理基盤:STORE" : ""

    "予約:ORGANIZATION" {
        int id PK
        string name
    }
    "予約:STORE" {
        int id PK
        string name
        string location
        int organization_id FK
    }
    "予約:ORGANIZATION_CACHE" {
        int id PK
        int organization_id FK
    }
    "予約:STORE_CACHE" {
        int id PK
        int store_id FK
    }

    "予約:ORGANIZATION" ||--o{ "予約:STORE" : ""
    "予約:ORGANIZATION" ||--|| "予約:ORGANIZATION_CACHE" : ""
    "予約:STORE" ||--|| "予約:STORE_CACHE" : ""

設計

機能として用意しなかった理由

店舗一括登録機能を実装する上で、

  • 利用者側から一括登録の操作ができる UI を提供する
  • 依頼があった際は都度アドホックに店舗登録作業をエンジニアが行う

という 2 つの選択肢がありました。

初期段階では、前者の UI を提供することで利用者が自身で店舗登録を行える仕様を検討していましたが、以下の理由からアドホックに倒すことを選択しました。

機能追加時の改修コスト

一括登録機能はすでに UI として提供されている機能のインタフェースの形を変えた機能となるので、新たな機能や field を対象機能に追加した際にはメンテナンス対象となるコードが増えることになります。利用者がいれば価値のあるコードですが今後のコストとのトレードオフを考える必要がありました。

データ構造の複雑化

ネストしたデータ構造を取り込む場合にはデータ登録の仕様が複雑になりオペレーションコストが高くなる傾向があります。 例として、複数スタッフ登録を持つ予約ページを表現した簡易的なデータ構造を以下に示します。

{
  "reservation_page": {
    "name": "平日予約受付",
    "price": 8000,
    "menu": [
      {
        "id": 1,
        "name": "カット",
        "price": 3000
      },
      {
        "id": 2,
        "name": "カラー",
        "price": 5000
      },
      {
        "id": 3,
        "name": "パーマ",
        "price": 6000
      }
    ],
    "option": [
      {
        "id": 1,
        "name": "トリートメント",
        "price": 2000
      },
      {
        "id": 2,
        "name": "ヘッドスパ",
        "price": 3000
      }
    ],
    "staffs": [
      {
        "name": "staff1",
        "acceptable_menu": [
          {
            "id": 1,
            "acceptable_nomination": true
          },
          {
            "id": 2,
            "acceptable_nomination": true
          }
        ],
        "acceptable_option": [
          {
            "id": 1
          }
        ]
      },
      {
        "name": "staff2",
        "acceptable_menu": [
          {
            "id": 1,
            "acceptable_nomination": true
          },
          {
            "id": 2,
            "acceptable_nomination": false
          }
        ],
        "acceptable_option_id": [
          {
            "id": 1
          }
        ]
      }
    ]
  }
}

頭が痛くなりますね…。実際に存在するデータ構造はさらに複雑で外部システムと連携している部分もあります。 そして、ユーザーの操作性のため csv に落とし込むとさらに冗長な形式となり、構造の理解を強制してしまいます。

これら 2 つの理由から、

  • メンテナンスコストを抑えることができる
  • 作成 / 変更されるデータ構造を理解している前提でデータ登録ができる

というメリットを取り、必要な際にエンジニアがアドホックに対応する形を取ることにしました。

オペレーションの設計

アドホックに対応する際には、業務ロジックの設計が重要になります。 今回の店舗一括登録機能では、以下のようなオペレーションを設計しました。

sequenceDiagram
    actor エンジニア
    actor カスタマーサポート
    actor 利用者

    カスタマーサポート ->> エンジニア: 一括編集 依頼
    エンジニア ->> エンジニア: 依頼内容の一括編集の可否を検討【*1】
    alt 不可能な場合
        エンジニア ->> カスタマーサポート: 対応不可能 連絡
    else 可能な場合
        エンジニア ->> エンジニア: 変更に必要なcsvのフォーマットを準備
        エンジニア ->> カスタマーサポート: csvフォーマットの連携
        カスタマーサポート ->> 利用者: csvフォーマットを埋めてもらうよう依頼【*2】
        loop データ登録完了まで
          利用者 ->> カスタマーサポート: データ提供
          カスタマーサポート ->> エンジニア: データ提供
          エンジニア ->> エンジニア: データインポート
          alt インポート成功
            エンジニア ->> カスタマーサポート:完了連絡
            カスタマーサポート ->> 利用者:完了連絡
          else インポート失敗
            エンジニア ->> カスタマーサポート: 修正依頼内容の連携
            カスタマーサポート ->> 利用者: エラー内容の連絡
          end
        end
    end

*1: 依頼内容の一括編集の可否を検討

アドホックな対応の利点として、依頼内容の可否検討の段階を設けています。 対応可能な工数であれば一括登録機能の項目を拡充したりなど要望ベースで柔軟に対応できます。

*2: csv フォーマットの連携

こちらから提供するフォーマットに沿ってデータを提供してもらうことで、エンジニアがデータの整形などの作業をする必要がなくなります。データの整形は工数の問題もありますが、利用者から提供されたデータに手を加えず一次情報として扱うことでデータの信頼性を保つことができます。

実装

組織管理基盤 との連携

前述した組織管理基盤が組織情報を管理している以上、店舗一括登録を行う際には予約のシステム内で完結せず 組織管理基盤 との連携が必要になります。

そこで、店舗一括登録機能では以下のようなシーケンスで処理を行っています。

sequenceDiagram
box 予約
  participant DB
  participant Sidekiq
end
participant 組織管理基盤

loop each_row
  activate Sidekiq
  Sidekiq ->> Sidekiq:【*1】
  Sidekiq ->> DB: Store.find_by(public_id:)
  DB -->> Sidekiq: store
  alt store exists【*2】
    Sidekiq ->> Sidekiq: 行エラー
  else
    Sidekiq ->> 組織管理基盤: createShop
    組織管理基盤 -->> Sidekiq: response
    Sidekiq ->> 組織管理基盤: assignServiceToOrganization
    組織管理基盤 -->> Sidekiq: response
    alt success
      Sidekiq ->> DB: insert store
      DB -->> Sidekiq: response
    else
      Sidekiq ->> 組織管理基盤: deleteShop【*3】
      組織管理基盤 -->> Sidekiq: response
      Sidekiq ->> Sidekiq: 行エラー
    end
  end
  deactivate Sidekiq
end

処理として考慮したポイントは主に 3 点です。

*1: 外部システムへの負荷軽減

組織管理基盤に対して店舗作成のリクエストを送る際に、一定のフロー制御をかけています。

組織管理基盤においても店舗作成時に複数の外部システムとの連携が発生するため余裕を持ってリクエストを送ることで、外部システムに負荷をかけないようにしています。

*2: 再実行性の確保

エンジニア作業での店舗一括登録を行う際にはデータの確認は行うものの、想定外のエラーが発生することが想定されます。その際に登録済の店舗があっても対象の登録をスキップすることで再実行時に一時情報となる登録元 csv を編集することなく処理の再実行が可能になります。

*3: ロールバック処理

想定外のエラーが発生したり、外部システム上でのエラーが発生する可能性があるため予約内での処理は transaction で rollback されますが、組織管理基盤に対してデータを操作するリクエストを実行している場合には対象処理のロールバックと見做せる deleteShop のリクエストを送ることで外部システム上でのデータの整合性を保っています。

さいごに

今回は STORES 予約 で店舗一括登録を実装した際の設計、実装についてお話しました。業務ロジック設計やアドホックな対応を選択する際の判断基準の一例として参考にしていただければ幸いです。