学習日報アプリのDB設計を晒す:技術×ジャンル×ステータスで学習を記録する
⚙️ この記事は AI(ChatGPT)による自動生成および編集支援のもとで作成されています。
内容はできる限り正確に記述していますが、参考情報としてご覧ください。
使用ツール:ChatGPT(OpenAI / GPT-5 モデル)
個人開発で 学習日報アプリ を作る中で、最初にめちゃくちゃ悩んだのが DB 設計 でした。
- 「メンバー(ユーザー)」
- 「会社(テナント候補)」
- 「技術(PHP / Laravel / React など)」
- 「学習ジャンル」
- 「学習ステータス(未着手 / 進行中 / 完了)」
- 「日報(いつ・何を・どれくらい勉強したか)」
こういった情報を あとから集計しやすい形 で持ちたい。
この記事では、実際に学習日報アプリで採用したテーブル構成と、そのとき考えていたことをまとめます。
全体像:どんなテーブルを作ったか
ざっくり ER 図にすると、こんな関係になります(テキスト版)。
companies ---< members ---< study_reports ---< study_report_technologies >--- technologies --- technology_genres
↑
roles (多対多でもOK)
登場する主なテーブルは以下です。
-
companies… 会社 / 組織(将来的なテナント候補) -
members… ユーザー(研修生 / 講師など) -
roles… 権限(trainee / teacher / admin) -
technologies… 技術(PHP / Laravel / React など) -
technology_genres… 技術ジャンル(言語 / フレームワーク / DB / インフラ…) -
study_reports… 日報(いつ・何を・どれくらい勉強したか) -
study_report_technologies… 日報と技術の中間テーブル -
study_statuses… (任意)学習ステータスのマスタ
以降、1つずつ見ていきます。
1. メンバーと会社とロール
companies
CREATE TABLE companies (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
-- 将来的にサブドメインを切るなら
subdomain VARCHAR(64) UNIQUE NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
deleted_at TIMESTAMP NULL
) ENGINE=InnoDB;
将来的に SaaS 化 / マルチテナント を見据えているので、
「会社」という概念を最初から分けておきました。
members
CREATE TABLE members (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) NOT NULL UNIQUE,
company_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
email_hash CHAR(64) NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
deleted_at TIMESTAMP NULL,
UNIQUE KEY members_email_company_unique (company_id, email_hash, deleted_at),
CONSTRAINT members_company_id_foreign
FOREIGN KEY (company_id) REFERENCES companies(id)
) ENGINE=InnoDB;
ポイントは、
-
uuid… フロントには UUID を渡して、ID を隠す -
email_hash… 物理メールアドレスをそのままユニークキーに使わない -
company_id + email_hash + deleted_atをユニーク制約にして、
「同じ会社内でメールが被らない」 & SoftDelete 後に再登録可能、の両立を狙っています。
roles(ロール)
シンプルに trainee / teacher / admin などのロールを定義。
CREATE TABLE roles (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(64) NOT NULL UNIQUE,
display_name VARCHAR(64) NOT NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);
メンバーとの関係は、システム次第で:
- 単一ロール →
members.role_id - 複数ロール → 中間テーブル
member_roleを作る
今回は 「講師でもあり研修生でもある」 みたいなケースも想定したので中間テーブルにしています。
2. 技術マスタとジャンル
学習日報アプリの肝は「何を勉強したか」です。
ここを マスタテーブルに切り出す ことで、あとから
- 技術別学習時間
- ジャンル別(言語 / FW / インフラ)集計
などのグラフが作りやすくなります。
technology_genres(技術ジャンル)
CREATE TABLE technology_genres (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(64) NOT NULL UNIQUE,
display_name VARCHAR(64) NOT NULL,
sort_order INT UNSIGNED NOT NULL DEFAULT 100,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);
例:
-
backend,frontend,infra,database,language… など
technologies(技術)
CREATE TABLE technologies (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
genre_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(64) NOT NULL,
display_name VARCHAR(64) NOT NULL,
sort_order INT UNSIGNED NOT NULL DEFAULT 100,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
UNIQUE KEY technologies_name_unique (name),
CONSTRAINT technologies_genre_id_foreign
FOREIGN KEY (genre_id) REFERENCES technology_genres(id)
) ENGINE=InnoDB;
例:
-
php,laravel,react,nextjs,mysql,aws…
3. 日報テーブル(study_reports)
学習日報そのものを表すテーブルです。
CREATE TABLE study_reports (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
member_id BIGINT UNSIGNED NOT NULL,
studied_on DATE NOT NULL, -- 学習日
title VARCHAR(255) NOT NULL,
body TEXT NULL,
total_minutes INT UNSIGNED NOT NULL, -- 1日の合計学習時間(分)
status VARCHAR(32) NOT NULL DEFAULT 'submitted', -- draft / submitted など
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
deleted_at TIMESTAMP NULL,
CONSTRAINT study_reports_member_id_foreign
FOREIGN KEY (member_id) REFERENCES members(id),
UNIQUE KEY study_reports_member_date_unique (member_id, studied_on, deleted_at)
) ENGINE=InnoDB;
ここでこだわったのは、
-
member_id + studied_onにユニーク制約を張ることで
「1 日 1 日報」ルール を DB レベルで保証すること -
total_minutesをカラムに持っておくかどうか
total_minutes は 集計時に中間テーブルから SUM しても求まる のですが、
- フロントで「今日 120 分勉強しました」と入力させる
- 中間テーブルはより詳細な「技術ごとの配分」として使う
という役割分担にしたかったので、あえてカラムに持ちました。
4. 日報と技術の中間テーブル
日報 1 件に対して複数の技術を紐づけるために、中間テーブルを作ります。
CREATE TABLE study_report_technologies (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
study_report_id BIGINT UNSIGNED NOT NULL,
technology_id BIGINT UNSIGNED NOT NULL,
minutes INT UNSIGNED NOT NULL, -- その技術に使った学習時間(分)
status VARCHAR(32) NOT NULL, -- 未着手 / 進行中 / 完了 など
memo VARCHAR(255) NULL, -- 任意のメモ
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
CONSTRAINT srt_report_id_foreign
FOREIGN KEY (study_report_id) REFERENCES study_reports(id),
CONSTRAINT srt_technology_id_foreign
FOREIGN KEY (technology_id) REFERENCES technologies(id),
INDEX srt_report_id_index (study_report_id),
INDEX srt_technology_id_index (technology_id)
) ENGINE=InnoDB;
このテーブルを作ることで、例えばこんな集計が簡単になります。
- 直近 7 日間の 技術別学習時間
- 1 か月単位の ジャンル別学習割合
- 学習ステータスごとの件数(
status = 'in_progress'の数など)
**「グラフ API を作りやすい設計」**にしておくと、後続のフロント実装がかなり楽になります。
5. 学習ステータスはどう持つか問題
「未着手 / 進行中 / 完了」をどう表現するかは悩みポイントでした。
選択肢はざっくり 3 つです。
-
VARCHARカラムに文字列で持つ(今回採用) -
TINYINTなどで数値コードを持つ(0,1,2) - 別テーブル
study_statusesを作って FK で紐づける
今回は「ステータスの種類が少なく、頻繁に変わらない」ことと、
レスポンスをわかりやすくしたかったので、文字列形式にしました。
status VARCHAR(32) NOT NULL -- 'not_started' / 'in_progress' / 'completed'
もし今後「ユーザーごとにステータスマスタをカスタムしたい」などの要件が出てきたら、
study_statuses テーブルを切るのが良さそうです。
6. マルチテナント(tenant_id)をどう考えたか
学習日報アプリは、将来的には 複数の会社が使う SaaS をイメージしています。
この場合、「どこに tenant_id(もしくは company_id)を入れるのか?」という問題が出てきます。
基本の方針は、
-
「人」に紐づくもの …
members.company_idを辿ればわかる
→study_reports/study_report_technologiesには明示的なtenant_idを入れなくてもよい -
会社ごとに設定が変わるマスタ
→ 例:technologiesをテナントごとにカスタムしたいならtechnologies.company_idを持たせる
という感じで、
「そのデータがどこまで共有されるか」
を軸に、company_id を持たせるかどうかを分けると整理しやすかったです。
7. 失敗しかけたポイント
実際に作りながら「あ、これやめといてよかった」と思ったのは次のあたりです。
- 日報テーブルに
technology_idを直接持たせようとした
→ 「1 日に 3 つ技術を勉強した」みたいなケースを表現できず撃沈 - 「学習時間は全部 text で free 記入にしよう」と一瞬考えた
→ 集計できなくて後悔しそうだったので数値(分)で持つ方向に統一 - メールアドレスをそのままユニークキーにしようとした
→ 削除 / 復活 / 会社跨ぎを考えるとemail_hash+company_idに切り出した方が安心
まとめ
学習日報アプリの DB 設計では、
-
companies/members/rolesで「誰がどの会社でどんな立場か」を表現 -
technologies/technology_genresで「何を学習したか」をマスタ化 -
study_reports/study_report_technologiesで「いつ・何を・どれくらい学んだか」を記録 - 将来のマルチテナントやグラフ表示を見据えて、中間テーブルと集計しやすいカラム構成にする
という方針に落ち着きました。
次の記事では、この DB を元に
- 学習時間グラフ API
- Next.js + Recharts でのフロント実装
あたりも書いていこうと思います 📈
Discussion