EDUCARE / 技術資料・社外秘

Educare のデータベース構成

全データストアの現物調査。数値は 2026-09-14 に本番 Cloudflare を実測した値です。

2026-09-14 / 宛先: キヨさん / 作成: 敏チーム

結論

1. 全体像

会員サイト LINE 公式アカウント 受験ツール 管理画面 Cloudflare Worker(Hono) educare-poc / educare-members / educare-mail / spi-questions ほか D1(SQLite)— 人・権利・出来事 educare-poc120 表 educare-poc-staging122 表 educare-crawler-ops29 表 educare-profile-store15 表 educare-cbt-events(匿名)9 表 educare-content-ledger(7) educare-article-pipeline(5)/ recruit-serving(5) R2 — コンテンツ(不変リリース) educare-question-bank educare-kakomon-bank(過去問) educare-ronbun-bank(論文) educare-cbt-raw-events(生ログ) educare-recruit-bodies / -releases educare-profile-sources / -archive educare-poc-images ほか(計 13 バケット)

図1: 関係データは D1、コンテンツ本体は R2。出題は DB でなく R2——2026-07-22 に D1 spi-questions-pool を削除し、current.json でバージョンを解決する不変リリース方式へ移行済み。

DB を使っていない領域: 過去問抽出・SPI バンク生成・論文バンク・WEB テキスト工房は JSON ファイル+ディレクトリ構造で管理(台帳部分のみ educare-content-ledger へ移行中)。求人クローラーだけは一次データがローカル SQLite(recruitment_v6.db・584MB・20表)で、公開分のみ D1 へ派生させる構成。

2. 会員DB educare-poc の地層

migration 001〜045 OSS「LINE Harness」由来 046〜079 Educare 第1波 080〜122 Educare 第2波 友だち・タグ・シナリオ配信 一斉配信・フォーム・流入計測 予約・リッチメニュー・スタッフ AI 添削の脳・セミナー 自治体アラート・受験 統合ユーザー基盤・Web 認証 配信台帳・VOC・問題集・プラン AI 模擬面接・教材と課金 学習ログ(出来事表) 本番 120 テーブル / 317 インデックス migration ファイル 126 本(2026-09-14 実測)

図2: LINE 色の強いテーブルと Educare 固有のテーブルが同じ DB に同居しているのは、この出自による。

ドメイン主要テーブル
アイデンティティusers(ハブ)/ user_identities(provider×uid で UNIQUE。line/email/google/spi_session)/ user_profiles / user_consents / friends
Web 認証user_sessions / email_login_tokens / web_login_codes / oauth_login_states / login_events / anon_sessions
LINE 配信(OSS)line_accounts / tags / friend_tags / scenarios / scenario_steps / scenario_step_deliveries / broadcasts / auto_replies / messages_log / chats / rich_menu_*
流入計測entry_routes / tracked_links / link_clicks / ref_tracking / conversion_points / conversion_events / ad_platforms / retro_attribution
受験・演習spi_attempts(1受験=1行・answer_records は JSON)/ answer_events(1問1行・両建て)/ mondaishu_sessions / mondaishu_session_items
AI 模擬面接interview_sessions / interview_events / interview_costs / interview_access / interview_access_attempts
教材・課金courses / lessons / course_revisions / offers / offer_items / learning_paths / learning_path_items / enrollments / user_plans / purchase_orders / stripe_events / stripe_member_events
学習ログlesson_progress(状態)/ learning_events(出来事)/ learning_daily(日次集計)
メール・問合せmail_outbox / mail_send_attempts / mail_webhook_events / mail_audit_events / support_threads / support_messages
運用voc_feedback / seminars / seminar_registrations / alert_subscriptions / municipality_events / system_flags / doc_intel

3. 設計原則

3.1 受験データは匿名のまま、別 D1 に隔離する

D1 educare-poc(実名・PII あり) users 統合ハブ friendsLINE 友だち user_identitiesline / email / google user_profiles志望自治体・職種・年度 user_consents同意の追記ログ D1 は跨ぎ JOIN 不可 D1 educare-cbt-events(匿名) events item_responses test_sessions user_id を持たせない =個人に戻せない構造 同意時のみ session_id↔user_id を記録し Worker が 2 つの D1 を順に引く

図3: D1 が DB を跨いで JOIN できないことを、そのまま個人情報の堀として使っている。

3.2 「内容・まとまり・売り物・権利」を別エンティティにする

内容 courses / lessons course_revisions まとまり learning_paths 順序付き多対多 売り物 offers / offer_items 価格・Stripe はここだけ 権利 enrollments / user_plans 単品購入 / 読み放題 順序を持って束ねる 決済成功 → 対象ぶん権利を冪等付与 offer_items で対象を指す 権利が閲覧可否を決める

図4: 2026-09-10 決裁。元は courses が価格と Stripe キーを直接持っており、セット売り・期間限定価格・同一教材の2価格が表せなかった。購入者ゼロのいま剥がす判断。

3.3 そのほか

状態と出来事を分ける状態=lesson_progress(1人×1単元=1行)/出来事=learning_events・answer_events(追記専用)。集計は日次の learning_daily を読む。Open edX・Moodle と同型。
追加のみ(additive-only)scripts/check-migrations.ts が CI で DROP/RENAME/ALTER COLUMN TYPE/DEFAULT 無し NOT NULL/ADD UNIQUE を機械遮断(041 以降)。down は無い。巻き戻しは D1 time-travel(DB 全体の時点復旧)。

4. スキーマの規約

論点実態
主キー例外なく TEXT の UUID(crypto.randomUUID())。126 本に AUTOINCREMENT はゼロ。
時刻🔴 2 系統が混在。065 以前=TEXT の JST 文字列(一部 UTC が混入)/067 以降=INTEGER unix ミリ秒。混在は実際にバグを生み、084/085 で文字列を剥がす回避が入っている。
外部キーREFERENCES 宣言は 116 か所。新しい表ほど薄い——問題集(090)・プラン(094)・学習ログ(118) は user_id を素の TEXT で保持し FK 無し。整合はアプリ層任せ。
UNIQUESQLite の制約上すべて CREATE UNIQUE INDEX で代替。状態つき一意性には部分 UNIQUE インデックス(UNIQUE … WHERE)を使う型が定着。
JSON 列TEXT に JSON を詰める列が多数。json_valid() CHECK は 3 ファイル(029/040/064)のみ、大半は無検査。
論理削除deleted_at は予約系 2 本のみ。ほかは is_active フラグ。
ORM不使用。素の prepare().bind()。原子性が要る所は db.batch()(D1 に BEGIN/COMMIT が無いため)。共通クエリは packages/db/src/(55ファイル)、ルート固有は routes 直書きの二層。
SQL 組立文字列連結は検出されず。値・JSON パスとも bind() 徹底、動的カラム名は固定リテラルのホワイトリスト。TypeScript の型は手書き(スキーマとの整合を機械検査する仕組みは無し)。

5. マイグレーション運用

for migration in 108_materials 109_purchase_orders 110_stripe_member_events; do
  pnpm exec wrangler d1 execute educare-poc-staging --remote \
    --config apps/worker/wrangler.members-staging.toml \
    --file "packages/db/migrations/$migration.sql" || exit 1
done
教材ブランチpackages/db/migrations126 本(〜122) AI 先生ブランチ088_training_events他ブランチには無い 論文ブランチ089_ronbun_correction他ブランチには無い mail-workerapps/mail-worker/migrations独自に 0001 から採番 wrangler d1 execute --file を手で当てる(追跡なし) D1 educare-poc(本番) 120 テーブル どのブランチのディレクトリを見ても、本番の全体像にならない 本番の d1_migrations は実測 0 行=wrangler の追跡機構は未使用

図5: 番号の重複 6 組(009・018・037・038・065 が各2本、041 は3本)、欠番 3(054・088・089)。ただし 088/089 は欠番ではなく別ブランチに実在する。

過去の事故の記録も残っている: 037_scenario_delivery_mode.sql は中身が SELECT 1; だけの空ファイル(先に手で本番へ当てた後 CI が再実行して duplicate column で落ち、「適用済みと記録するだけ」に書き換えられた)。096 のコメントにも「本番には別ブランチで 090_test_accounts.sql として当て済み。再適用するな」とある。

6. 本番と staging の差分

本番 educare-poc 120 表 staging 122 表 10 本番だけ 110 共通 12 staging だけ 本番だけ: mail_* 5 / support_* 4 / answer_events staging は本番の上位互換ではなく、両方向にずれている staging だけ: offers / purchase_orders / learning_* ほか

図6: メール系は別 Worker educare-mail が同じ D1 に対して持つ第2のマイグレーションディレクトリ由来。staging だけの 12 表は本番リリース前の教材・課金・学習ログ一式。

本番 Worker のコード反映は 3 段ゲート(prod_deploy.py execute → sudo promote → verify)で守られ、素の wrangler deploy は hook で遮断される。D1 マイグレーションの適用はこのゲートの対象外。

7. 構造的に弱いところ(7件)

高本番 D1 のバックアップが約2か月停止

仕組みは完成(全D1を d1 export → gzip → age 暗号化 → オフサイト同期)。launchd 定義が .disabled のままリンクされ自動ロード対象外。launchctl list にヒット無し、ログの最終成功は 2026-07-22 17:10。

拡張子を直して再ロードで復帰する見込み。time-travel の保持期間を超えると戻せない。

高本番にあるのに定義 SQL がリポジトリに無い表がある

answer_events は本番稼働中(Worker が実際に INSERT/SELECT)だが CREATE TABLE が 126 本のどこにも無い。全ワークツリー横断検索で見つかったのは 2026-07-10 の本番ダンプの中だけ。設計ドキュメントから直接 d1 execute --command で当てたものと推定。

現行スキーマから CREATE TABLE IF NOT EXISTS を起こして migration 化すれば、再構築で失われない。

高スキーマの正本が1か所に無い

本番スキーマの全体像=「主 migration 126 本 + mail-worker の第2 migration + 他ブランチ限定の migration + 手当ての DDL」の総和(図5)。

本番から wrangler d1 export --no-data でスキーマを吐き、生成物としてリポジトリに定期更新する。

中本番 D1 への直接 SQL を止める仕組みが無い

Worker デプロイには 3 段ゲートと遮断 hook があるが、hook 17 本を全数検査して d1 execute に反応するものはゼロ。本番 DB は AI を含め誰でも直接書き換えられる。

本番 D1 名を含む d1 execute を検知し、--file かつ migrations 配下のみ通す fail-closed の門。

中staging が本番の上位互換でない

本番にあって staging に無い表が 10 本(図6)。メール/問い合わせ系は staging で検証できない。

mail-worker の migration を staging にも当て、本番 ⊆ staging を回復。

中時刻の型が2系統混在

TEXT の JST 文字列と INTEGER unix ミリ秒が同居。additive-only 方針のため一括変換不可。

新規は INTEGER で統一(すでにその方向)。既存 TEXT 列は跨いで比較する箇所にだけ変換を挟む。

低既存のデータモデル資料が古い

packages/db/schema.sql は 62 表のみで認証・教材・面接が丸ごと欠落。自動生成の DATA_MODEL.draft.md は 2026-07-01・65 番までで自称「未検証」。現時点で信頼できるのは教材限定の MATERIALS_DATA_MODEL_DESIGN_20260910.md のみ。

8. ご意見をいただきたい点

  1. migration を追跡ありの方式に戻すか。d1 migrations apply に寄せ直すには番号重複の解消と既適用分のバックフィルが要る。コストを払う価値があるか。
  2. 連番をタイムスタンプ採番に変えるか。衝突は「並行ブランチが連番を取り合う」構造が原因。20260914_1203_offers.sql 形式なら衝突は消えるが、適用順序の意味づけが変わる。
  3. answer_events のように手当てで入った表は他にもありそうか。手元の本番ダンプが 2026-07-10 のものだけで、以降の手当て DDL は追跡できていない。
  4. 本番 D1 への直接 SQL に門を置いてよいか。置くとしたら、どこまで通すか。