#846 DB設計

DB設計レビューダッシュボード

既存スキーマとの整合性、要件充足、レビュー指摘を一覧で確認

全体像

users (varchar(36) PK) ├── user_memberships ... 主サブスク契約(既存) │ └── product_id → products ├── product_subscriptions ... 副サブスク契約(新規) │ ├── user_membership_id → user_memberships │ ├── product_id → products │ ├── studio_id → studios │ └── product_subscription_deliveries(新規) │ ├── studio_id → studios │ └── delivered_by → users └── orders → order_items, purchase_histories(既存) products (bigint PK) ├── type: 'SUBSCRIPTION' | 'TICKET' | 'RECURRING_GOODS'(新規追加) ├── subscription_attrs ... 主サブスク属性(既存) ├── ticket_attrs ... チケット属性(既存) ├── recurring_goods_attrs ... 副サブスク属性(新規) └── products_studios ... 店舗との紐付け(既存)

product_subscriptions 新規

ユーザーごとの副サブスク契約状態を管理する。既存の user_memberships(主サブスク)と対になるテーブル。

カラム制約説明
idbigintPK, AUTO_INCREMENT
user_idvarchar(36)NOT NULL, FK→users(id) CASCADE既存 users.id に合わせ varchar(36)
user_membership_idbigintNOT NULL, FK→user_memberships(id) CASCADE主サブスクとの紐付け(連動解約用)
product_idbigintNOT NULL, FK→products(id)
studio_idbigintFK→studios(id)要検討: 権利タイプ商品では不要の可能性。nullable にすべきか
payjp_subscription_idvarchar(255)NOT NULL, UNIQUEPayJP の定期課金ID
statusvarchar(30)NOT NULLPENDING/ACTIVE/SCHEDULED_CANCEL/CANCELED/PAUSED
term_fromdateNOT NULL要検討: 既存 user_memberships は datetime
term_todateNOT NULL同上
change_atdateNULLABLE同上。解約予定日
canceled_attimestampNULLABLEレビューで追加提案。実際に CANCELED になった日時
created_attimestampDEFAULT CURRENT_TIMESTAMP
updated_attimestampDEFAULT CURRENT_TIMESTAMP ON UPDATE

インデックス

種別カラム備考
単体user_id
単体user_membership_id
単体status
単体change_at
UNIQUEpayjp_subscription_idWebhook解決の一意性担保
複合(user_id, status, term_to)レビューで追加。有効サブスク検索用

product_subscription_deliveries 新規

月次の商品受け取り状況を管理する。物品タイプの商品のみレコードが作られる(権利タイプは0件)。

カラム制約説明
idbigintPK, AUTO_INCREMENT
product_subscription_idbigintNOT NULL, FK→product_subscriptions(id) CASCADE
delivery_monthdateNOT NULL月初固定 (YYYY-MM-01)。元設計は varchar(YYYY-MM) → date に変更
studio_idbigintNOT NULL, FK→studios(id)受け取り店舗
is_deliveredbooleanDEFAULT FALSEdelivery_status ENUM 化を検討中
delivered_attimestampNULLABLE
delivered_byvarchar(36)NULLABLE, FK→users(id) SET NULL元設計は bigint → varchar(36) に修正
notetextNULLABLE備考
created_attimestampDEFAULT CURRENT_TIMESTAMP
updated_attimestampDEFAULT CURRENT_TIMESTAMP ON UPDATE

インデックス

種別カラム備考
単体product_subscription_id
単体delivery_month
単体studio_id
単体is_delivered
UNIQUE(product_subscription_id, delivery_month)1サブスク×1ヶ月で1レコード
複合(studio_id, delivery_month, is_delivered)レビューで追加。店舗の月次一覧用

参照する既存テーブル 既存

users

idvarchar(36)(UUID)。新規テーブルの user_id, delivered_by はこれに合わせる。

user_memberships

主サブスクの契約状態を管理。idbigint。product_subscriptions.user_membership_id から参照。プラン変更時は既存レコードを更新する設計(新レコード作成ではない)のため、FK が古い値を指し続ける問題は起きない。

products

type カラムに 'RECURRING_GOODS' を追加する方針。attr_id で商品種別ごとの属性テーブルを参照する構造のため、recurring_goods_attrs テーブルも新規作成が必要。

studios

idbigint。product_subscriptions.studio_id と deliveries.studio_id から参照。

要件 vs テーブル設計

OK
主サブスク + 副サブスクの同時保有

user_memberships(主)と product_subscriptions(副)が別テーブルなので独立管理可能。user_membership_id FK で紐付け。

OK
副サブスクだけの解約が可能

product_subscriptions.status を単独で SCHEDULED_CANCEL にできる。users.withdrawal_date には触れない設計。

OK
主サブスク解約時の連動解約

user_membership_id で紐づく product_subscriptions を検索し、一括で SCHEDULED_CANCEL にする。

OK
決済は主サブスクと別トランザクション(毎月20日)

payjp_subscription_id を独立して持つため、PayJP 上で別 subscription として管理される。UNIQUE 制約で Webhook の振り分けも安全。

OK
店舗オリジナル商品の対応

既存の products + products_studios(多対多)で店舗ごとの商品紐付けが可能。新規テーブルの追加は不要。

OK
月次の受け取り管理

product_subscription_deliveries で月ごとの受取状況を記録。ユニーク制約 (subscription_id, delivery_month) で重複防止。

OK
支払い方法は主サブスクと同じカード

product_subscriptions に user_payment_method_id を持たない設計。主サブスクの支払い方法を共用。テーブル設計上は不要なカラムを入れていないので正しい。

!
権利タイプ商品(ピラティスマシン等)で delivery が不要

deliveries テーブルにレコードが0件になるケースが正当。テーブル設計自体は問題ないが、fulfillment_type(PICKUP_GOODS / IN_STORE_EQUIPMENT_RIGHT 等)を商品マスタ側(products or recurring_goods_attrs)に持たせて、購入時の delivery 作成を分岐する必要がある。

!
初回課金の満額 / 日割り設定

product_subscriptions テーブルには該当カラムがない。これは商品マスタ側(recurring_goods_attrs)に first_billing_type 等として持たせるべき。product_subscriptions 自体の設計は問題なし。

!
未受取分の翌月繰越

delivery_month のユニーク制約があるため、1サブスク×1ヶ月で1レコード。繰越を表現するなら delivery_status の ENUM 化(PENDING/DELIVERED/SKIPPED/CARRIED_OVER 等)が必要。ルール自体は未確定。

OK
副サブスクのみの状態はなし

user_membership_id が NOT NULL FK なので、主サブスクなしでは作成不可。CASCADE で主サブスク削除時に副サブスクも削除される。

chaoさんレビューへの対応方針

bc-master-dev 経由で届いたレビュー全14項目を、対応方針ごとに分類。

取り込む 6件

1-1. delivered_by の型を varchar(36) に

取込
users.id は varchar(36)(UUID) なのに delivered_by が bigint では FK が張れない。
varchar(36) に修正。更新済み ER 図に反映済み。

1-4. delivery_month を date 型に

取込
varchar(YYYY-MM) は範囲検索で不利。バリデーションも追加実装が必要。
date 型(月初固定 YYYY-MM-01)に変更。表示時にフォーマット。

3. payjp_subscription_id に UNIQUE 制約

取込
Webhook 解決時の一意性が担保されない。
UNIQUE 制約を追加。

3. 複合インデックス 2件追加

取込
(user_id, status, term_to) と (studio_id, delivery_month, is_delivered) を推奨。
両方追加。クエリパターンに合致。

2-1. canceled_at カラム追加

取込
status=CANCELED と時点の整合を取りやすい。
nullable timestamp として追加。

4-1. 解約時に users.withdrawal_date を触らない

取込
副サブスク解約で users.withdrawal_* を触ると主サブスクの退会処理と衝突する。
副サブスクの解約は product_subscriptions.status のみ変更。users テーブルは一切触らない。

対応不要 5件

1-3. user_membership_id の nullable 化

不要
プラン変更時に新レコードが作られると ID が古くなる。
既存コードを確認済み。プラン変更時は既存レコードを更新する設計(新レコード作成ではない)のため、user_membership_id が古い値を指し続ける問題は発生しない。NOT NULL のまま。

1-5. term_to の nullable 化

不要
無期限継続の商品では NULL にしたい。
現時点の要件では主サブスクと同じ課金ルール(Webhook で毎月延長)を適用するため NOT NULL で統一。

2-1. scope / subscription_kind / payjp_customer_id / next_billing_at

不要
将来の統合や運用便利のための冗長カラム。
テーブル分離で主/副は自明。PayJP 情報は API 経由で取得可能。冗長カラムは追加しない。

1-6. ステータス遷移ルールの明文化

不要
主サブスクと遷移ルールが異なると混乱する。
主サブスクと同じルールを適用すると確定済み。設計書(#576 コメント)に明記済み。

4-2. BI 売上分類の混在

不要
products.type が同じ SUBSCRIPTION だと主/副の売上が混在。subscription_category カラム追加を提案。
products.type に RECURRING_GOODS を追加する方針で対応。新カラムではなく既存 type の値を拡張。BI は type で分類可能。

検討中 3件

2-3. delivered_product_name_snapshot

検討中
商品名変更後も当時の引渡し内容を追跡可能にする。
採用の方向で検討中。varchar で snapshot を持たせる想定。

2-3. delivery_status の ENUM 化

検討中
is_delivered boolean → PENDING/DELIVERED/SKIPPED 等の ENUM に。
未受取の繰越ルール(Q12)の回答次第で決まる。繰越ありなら ENUM 化が必要。

2-3. delivered_by_type

検討中
スタッフ操作と顧客操作を区別。
現時点はスタッフのみだが、将来ユーザー側で受取確認の可能性を考慮。追加コスト低いため前向き。

既存スキーマとの型整合性

新規テーブルの設計を既存テーブル(develop ブランチ)と照合した結果、判断が必要な点が4つあります。

1. term_from / term_to / change_at の型: date vs datetime

既存 user_memberships
term_from datetime
term_to datetime
change_at datetime
新規 product_subscriptions
term_from date
term_to date
change_at date

プロジェクト内で型が混在すると、サービス層で dayjs の扱いがぶれる。既存に合わせて datetime にするか、date で良い明確な理由があるか判断が必要。副サブスクは時刻精度が不要(日単位で十分)なら date でも合理的だが、コードの一貫性を優先するなら datetime。

2. status の型: varchar vs ENUM

既存 user_memberships
status varchar(30)
元の設計書
status ENUM(...)

bc-master-dev の更新済み ER 図では varchar に修正済み。既存に合わせて varchar(30) で統一が妥当。ENUM にするとステータス追加時にマイグレーションが必要になるデメリットもある。

3. studio_id の NOT NULL 制約

既存 user_memberships
studio_id bigint, NULLABLE
後から追加されたカラム
新規 product_subscriptions
studio_id bigint, NOT NULL
現在の設計

権利タイプ商品(ピラティスマシン等)では受取店舗の概念がないため、studio_id に値を入れる意味がない。「契約した店舗」として必ず入れるか、nullable にして権利タイプでは NULL を許容するか。既存 user_memberships も nullable なので、揃えて nullable にしておく方が自然。

4. fulfillment_type の置き場所

案A: 商品マスタ側
products または recurring_goods_attrs に
fulfillment_type を追加
商品の性質として固定
案B: 契約側
product_subscriptions に
fulfillment_type を追加
契約ごとに変わりうる場合

「プロテインは物品」「ピラティスマシンは権利」という性質は商品ごとに固定なので、商品マスタ側(案A)が妥当。recurring_goods_attrs に fulfillment_type カラムを持たせ、購入時に delivery レコード作成を分岐する。