テーブルは自分のことだけを知っていればいい ― 中間テーブルと責務の分離
Published on 2026年3月15日
DB設計はじめに
個人開発でUserとArtistのテーブル設計をしていたとき、ある違和感に気づきました。
その違和感を起点に、中間テーブルの役割・責務の単一化・DBとアプリケーションの責務の境界線まで、思考が展開していった記録をまとめます。
前提:何を作っているか
このプラットフォームはbeatbox業界向けのサービスです。
beatboxの世界には、バトルイベント・大会・パフォーマンスなど様々な文脈があり、それに関わる人の役割も多様です。
Users
├── Artists (表現者・パフォーマー)
│ ├── solo (個人として活動)
│ ├── team (グループとして活動)
│ └── tag (タッグ=2人組で活動)
├── Organizer (大会・イベントの主催者)
└── 観客 (イベントの参加者・視聴者)
今回の設計で扱うのはArtistsの構成に絞っています。OrganizerやObserverは別タスクで設計する前提です。
beatbox業界におけるArtistの活動形態とは
Artistの活動形態には複数のパターンがあります。
活動形態のパターン
1. solo(個人)
1人で活動するArtist
例:ソロバトル、ソロパフォーマンス
2. tag(タッグ)
2人組で活動するArtist
例:tag team battle
3. team / crew
3人以上のグループで活動するArtist
例:crew battle、グループパフォーマンス
soloは個人、tagは2人組、team/crewはそれ以上のグループという区分です。1人のArtistが複数の形態で活動することもあります(ソロ活動しながらタッグも組む等)。この多様な活動形態をどうテーブル設計に落とし込むかが、今回の設計の核になります。
違和感の発生
最初の設計はシンプルでした。ArtistテーブルにそのままUserのIDを外部キーとして持たせる構成です。
Artists
┌───────────┬─────────┬──────────┐
│ artist_id │ user_id │ genre │
└───────────┴─────────┴──────────┘
↑
「Artistの情報」なのに
「Userへの参照」が混入している
この構成の問題は、Artistテーブルの責務は「Artistとしての属性を持つこと」のはずなのに、user_idという「関係の情報」が紛れ込んでいる点です。「関心の分離」が崩れています。
ここで二つの考え方があります。
- 考え方A: UsersがArtistsを集約しているのだから、含めていい
- 考え方B: それぞれの情報は独立させて、関係は中間テーブルに集約する
どちらが正しいかは、関係の多重度次第です。1:1なら外部キーで十分、N:Mになり得るなら中間テーブルが必要になります。
UserとArtistの関係がどちらかを判断するには、まず「Artistとは何か」を定義する必要があります。
概念の定義から始める
テーブル設計より先に、ドメインを言語化します。
Artistは表現者である。
この定義が、その後の設計判断をすべて導きます。
ソロのArtistも、チームのArtistも、どちらも「表現者」という同じ概念です。ソロかチームかはArtistの内部構造に過ぎません。つまりわざわざ別テーブルに分ける理由がありません。
// 最初の発想(分けすぎ)
SoloArtists テーブル
TeamArtists テーブル
// 定義に基づいた発想
Artists テーブル(ソロもチームも「表現者」として同居)
では、soloとteamの違いはどこで表現するのか。
typeカラムという罠
素朴に考えるとこうなります。
Artists
┌─────────┬──────┬───────┐
│ id │ name │ type │ ← "solo" / "team"
└─────────┴──────┴───────┘
しかしこれはアンチパターンです。
なぜか。エンティティの振る舞いはデータ構造で決まります。その判断をするのはアプリケーションレイヤーの仕事であり、DBの仕事ではありません。
typeカラムを持つ
→ DBが「これはsoloだ / teamだ」という判断を持つ
→ DBの責務は「信頼できる事実の記録」
→ 振る舞いの判断を持たせるのは責務の逸脱
DBが記録すべきは「事実」です。typeという分類は、データの解釈であり判断です。それはアプリケーションが担うべきものでした。
では、soloとteamの違いをどう表現するか。答えはデータの存在そのもので表現することでした。
// DBが記録する事実
artist_team_members にレコードがある → teamとして機能している
artist_team_members にレコードがない → soloとして機能している
// soloかteamかの「判断」はアプリケーションが行う
typeというフラグではなく、データの存在が事実を語る設計です。
UserとArtistの関係をどう持つか
定義が固まったところで、最初の問いに戻ります。UserとArtistの関係は1:1か、それ以上か。
soloの場合
User ──── Artist(自分のソロアカウント)
teamに所属する場合
User ──── Artist(自分のソロ)
User ──── Artist(所属しているチームA)
User ──── Artist(所属しているチームB)
1人のUserが複数のArtistに紐づく可能性があります。1:Nです。
これは憶測ではなく、業界の実態としてsolo・teamの複数パターンが存在するという根拠のある判断です。
1:Nになり得るなら、ArtistsテーブルにFKとしてuser_idを直接持たせる構成は破綻します。
// 破綻する構成
Artists
│ user_id │ ← 1つしか持てない、1:Nに対応不可
// 対応できる構成
user_artists(中間テーブル)
│ user_id │ artist_id │ ← 何行でも持てる
こうしてuser_artists中間テーブルが必要になりました。
中間テーブルとは何か、改めて考える
中間テーブルは単なる「N:Mを解決する技術的手段」ではありません。**「関係そのものをエンティティとして表現する」**という発想です。
「購入」がUserでもProductでもない独立した概念であるように、「UserがArtistを所有している」という関係も独立したエンティティとして切り出せます。
user_artists
┌─────────┬───────────┬─────────────┐
│ user_id │ artist_id │ created_at │
└─────────┴───────────┴─────────────┘
「UserがArtistを所有している事実」を1行として記録する
中間テーブルに何を持たせるかを考えることは、「この関係はどんな意味を持つか」を言語化することでもあります。
チーム構成をどう記録するか
最初、artist_team_membersテーブルのmember_idをUserのIDにしようとしました。しかし先ほどの定義に立ち返ります。
チームもメンバーも「表現者(Artist)」である。
メンバー個人も表現者なので、チームとメンバーの関係は「Artist同士の関係」として表現できます。
artist_team_members
┌───────────┬───────────┬──────────┬───────────┐
│ team_id │ member_id │ status │ joined_at │
└───────────┴───────────┴──────────┴───────────┘
↑ ↑
artists.id artists.id
(チームとして)(メンバーとして)
同じartistsテーブルを2方向から参照する自己参照の中間テーブルです。ドメインの定義が設計に自然に反映されています。
statusは「事実の記録」
チームへの招待・承諾フローにも、typeカラムの教訓が適用できます。
// statusが表す「事実」
pending → 招待した事実(レコードが存在する)
accepted → 承諾した事実
rejected → 拒否した事実
// 「正式メンバーかどうか」の判断はアプリケーションが行う
// DBはあくまで起きた出来事を記録するだけ
statusは振る舞いの判断ではなく、起きた出来事の結果を事実として記録するものです。
Ownerという概念との格闘
「誰がこのArtistを作ったか」をどこで表現するか。チームの発起人は「Owner」として特別な役割を持つはずです。
いくつかのアプローチを検討しました。
案1:is_ownerフラグをuser_artistsに持たせる
user_artists
│ user_id │ artist_id │ is_owner │
→ DBが「これはOwnerだ」という状態を持ちます。typeカラムと同じアンチパターンです。
案2:roleカラムをuser_artistsに持たせる
user_artists
│ user_id │ artist_id │ role │ ← "owner" / "member"
→ DBが振る舞いの判断を持ちます。これもアンチパターンです。
案3:created_byをartistsテーブルに持たせる
artists
│ id │ name │ created_by │ ← 作成者のuser_id
→「誰が作ったか」は作成時に確定する事実です。DBの責務として自然です。
案3が最も原則に忠実です。Ownershipは「所有という状態」ではなく、「作成という事実」として記録します。
将来、所有権の移譲が必要になった場合は別途対応します。
// 将来、所有権の移譲が必要になったとき
artist_ownership_history
┌─────────────┬───────────┬──────────┬──────────┬─────────────┐
│ id │ artist_id │ from_user│ to_user │ created_at │
└─────────────┴───────────┴──────────┴──────────┴─────────────┘
「移譲という出来事」をイベントとして記録する
現在のOwnerはアプリケーションが履歴の最新を見て判断する
これは今すぐ設計する必要はなく、仕様が固まったときに追加すれば十分です。
最終的なテーブル構成
最終的にこの構成になりました。
users
├── id
├── name
├── email
└── created_at
artists
├── id
├── name
├── artist_id(表示用ID)
├── created_by(作成者のuser_id ← 作成の事実)
└── created_at
artist_profiles
├── id
├── artist_id FK
├── bio
├── genre
└── avatar_url
user_artists(中間テーブル)
├── id
├── user_id FK
├── artist_id FK
└── created_at
artist_team_members(自己参照の中間テーブル)
├── id
├── team_id FK → artists.id
├── member_id FK → artists.id
├── status(pending / accepted / rejected)
├── joined_at
└── created_at
各テーブルの責務は明確です。
| テーブル | 責務 |
|---|---|
artists |
表現者としての属性を持つ |
artist_profiles |
Artistの詳細情報を持つ |
user_artists |
UserがArtistを所有している事実を記録する |
artist_team_members |
チームの構成と招待状態を記録する |
artistsはUserのことを知らない。user_artistsはUserとArtistの関係だけを知っている。artist_team_membersはチーム構成だけを知っている。 各テーブルが自分のことだけを知っている状態です。
今やらないことも設計のうち
設計では「何をやるか」だけでなく、「今何をやらないか」を意識的に決めることも重要です。
| 項目 | 今やらない理由 |
|---|---|
| Owner権限の移譲 | 脱退・譲渡の仕様が固まってから |
artist_ownership_history |
権限移譲が実際に必要になったとき |
| 脱退・削除の論理削除設計 | 別タスクで設計する |
ただし「やらない」は「考えていない」ではありません。拡張の方向性が見えている状態で、意識的にシンプルに保つのと、何も考えずにシンプルにするのは全く違います。
この設計から学んだこと
概念の定義が先、テーブル設計は後
「Artistは表現者である」という定義が、その後の設計判断をすべて導きました。定義が曖昧なまま設計を始めると、カラムの配置で迷子になります。設計に詰まったら、まずドメインの言語化に立ち返ることが重要です。
違和感は設計の羅針盤
「ArtistテーブルにUser IDが入っているのがおかしい」という違和感を深掘りしたことで、責務の分離という本質に辿り着きました。違和感の原因を言語化する時間は決して無駄ではありません。
DBは事実を記録し、判断はアプリケーションに委ねる
typeカラム、roleカラム、is_ownerフラグ。これらはすべてDBに振る舞いの判断を持たせてしまうという同じ問題を抱えています。DBが記録するのは「何が起きたか」という事実であり、「それをどう解釈するか」はアプリケーションの仕事です。
問題点が見えている状態で意識的に選択することが設計
「将来N:Mになり得ると知りながら、今はシンプルにFKで済ます」という選択も、理由と移行コストを把握した上でやるなら正しい設計判断です。問題を知らずに組むのと、知った上で選択するのでは全く意味が違います。