付録H 会話データのスキーマ例(conversations / messages / usage / memories)
📖 このページのゴール:この教材で使うテーブルを一望し、そのままコピーして使える。 ← 目次・はじめにへもどる
このページは、第5章(会話ログ)・第6章(usage)・第9章(要約)・第10章(メモリ)でバラバラに出てきた表(テーブル)を、1ページにまとめた“地図”です。各章でその都度くわしく説明しているので、ここは「全体を見渡す」「そのままコピーして使う」ための索引として使ってください。保存先は前提どおり Supabase(中身は Postgres というデータベース) です。
🗺 全体像
4つの表が、どうつながっているかを先に見ます(矢印は「→ の先を指している」の意味)。
┌─────────────────────────┐
│ auth.users (Supabaseが用意) │ ← ログインした人。ここは自分で作らない
└─────────────┬───────────┘
│ user_id(持ち主)
┌────────────┼───────────────┬───────────────┐
▼ ▼ ▼ ▼
conversations usage memories (どれも user_id で
(会話=箱) (使った量) (覚えた事実) 本人にひも付く)
│ id
│
▼ conversation_id(どの会話か)
messages(発言=中身)
1行ずつ読むと:
auth.users… Supabaseが最初から持っているログイン台帳。自分では作らない。すべての表のuser_idは、ここの人を指す。conversations… 会話1本=1行(お弁当箱)。user_idで持ち主に、idで発言につながる。messages… 発言1つ=1行(中のおかず)。conversation_idで「どの会話のものか」を指す(1対多)。usage… 「誰が・どの日・何回・何トークン」使ったかの台帳(第6章)。会話とは直接つながらず、人と日付でまとまる。memories… 会話をまたいで覚える「事実」(第10章)。会話に関係なく、人ごとにぶら下がる。
🧱 テーブル定義
そのままコピーして、SupabaseのSQLエディタに貼れば作れます。上から順に(conversations → messages → usage → memories)実行してください(messagesは conversations を参照するので順番が大事)。
conversations(会話=箱)
create table conversations (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id),
title text not null default '新しい会話',
summary text not null default '',
created_at timestamptz not null default now()
);
1行ずつ読むと:
id ... default gen_random_uuid()… 会話の背番号。自動で重複しないID(UUID)が振られる。user_id ... references auth.users(id)… 持ち主。ログイン台帳とつなぐ。RLSの判定はここを見る。title ... default '新しい会話'… 一覧に出す名前。まず仮の名前を入れておく。summary ... default ''… これまでの会話の要約(第9章で追加)。最初は空っぽで、会話が伸びると中身が入る。created_at ... default now()… 作った時刻。会話一覧を新しい順に並べるのに使う。
messages(発言=中身)
create table messages (
id uuid primary key default gen_random_uuid(),
conversation_id uuid not null references conversations(id),
user_id uuid not null references auth.users(id),
role text not null, -- 'system' | 'user' | 'assistant'
content text not null,
status text not null default 'done', -- 'done' | 'pending' | 'error'
created_at timestamptz not null default now()
);
1行ずつ読むと:
conversation_id ... references conversations(id)… その発言が どの会話のものかを指す札(1対多のかなめ)。user_id ... references auth.users(id)… 持ち主。発言からも本人をたどれるよう持たせる(RLSが書きやすくなる。後述)。role text not null… 発言の種類。system(最初の設定)/user(あなた)/assistant(AI)のどれか。content text not null… 発言の本文。status text not null default 'done'… 発言の状態(第7章)。ふだんはdone(完了)。返事を生成中ならpending、途中で失敗したらerrorを入れて、順番が崩れた発言を見分けるのに使う。created_at ... default now()… 発言した時刻。この順で並べると会話の順番が正しく戻る。
usage(使った量=台帳)
create table usage (
user_id uuid not null references auth.users(id),
day date not null,
count int not null default 0,
tokens int not null default 0,
primary key (user_id, day)
);
1行ずつ読むと:
user_id ...… 誰の記録か(第6章のreq.userIdが入る)。day date not null… いつの日の記録か(1日ごとに1行)。count int not null default 0… その日の呼び出し回数。最初は0。tokens int not null default 0… その日に使ったトークン量(料金の単位。付録D参照)。primary key (user_id, day)… 「人+日」で1行と決める。同じ人の同じ日は必ず1行にまとまる。
memories(覚えた事実)
create table memories (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id),
fact text not null,
created_at timestamptz not null default now()
);
1行ずつ読むと:
id ... default gen_random_uuid()… 事実1つごとの背番号。あとで「古い事実を消す・上書きする」とき、この背番号で1件をねらえる。user_id ...… 誰のメモリか(持ち主)。fact text not null… 覚える事実そのもの(例「名前は太郎」)。空はダメ。created_at ... default now()… いつ覚えたかの時刻。
🔒 RLS
公開アプリでは「他人の会話・他人のメモリが見えてはいけない」。これを守るのがRLS(Row Level Security=行ごとの見える・見えないルール)です。全部の表でONにし、「自分の行だけ」に絞ります。
-- まず4つの表すべてでRLSをONにする(ONにするまでは全公開なので注意)
alter table conversations enable row level security;
alter table messages enable row level security;
alter table usage enable row level security;
alter table memories enable row level security;
-- どの表も「自分が持ち主の行だけ」さわれる
create policy "own conversations" on conversations
for all using (auth.uid() = user_id);
create policy "own messages" on messages
for all using (auth.uid() = user_id);
create policy "own usage" on usage
for all using (auth.uid() = user_id);
create policy "own memories" on memories
for all using (auth.uid() = user_id);
1行ずつ読むと:
enable row level security… その表で「行ごとの見える・見えないルール」をONにする。4つ全部に必要(1つでも忘れると、その表だけ全公開になる)。for all using (auth.uid() = user_id)… 読み書き全部について、「いま操作している人(auth.uid())=その行の持ち主(user_id)」のときだけ許可する。これで他人の行は丸ごと見えなくなる。messagesもuser_idを持たせているので、第5章のように親の会話をたどらず、auth.uid() = user_idの同じ一文で書ける(ポリシーがシンプルで間違えにくい)。
⚡ インデックス
インデックス(索引)は、本の巻末さくいんと同じ。よく「この条件でしぼり込む」列にあらかじめ張っておくと、表が大きくなっても検索が速いままです。この教材でよく使う絞り込みに合わせて、次を張ります。
-- 「ある会話の発言を、古い順に取る」が速くなる(第5章の再開・第9章の直近N件)
create index messages_conv_time on messages (conversation_id, created_at);
-- 「この人の・この日の使用量」を一発で引ける(第6章の上限チェック)
-- usage は primary key (user_id, day) が索引も兼ねるので、追加は不要
1行ずつ読むと:
messages (conversation_id, created_at)…conversation_idでしぼり、created_atで並べる——といういつもの取り方とぴったり同じ並びの索引。会話を開くたびの検索がぐっと速くなる。usageのコメント …primary key (user_id, day)を作った時点で、その2列の索引も自動でできている。だから「この人の・この日」を引くための追加インデックスは要らない。
💡 「なぜUUIDを主キーにするのか」「どの列に索引を張るべきか」をもっと基礎から知りたい人は、第1弾 Twitterクローンで学ぶWeb開発入門 — はじめに・目次 の 付録G「ID・インデックス」 が詳しいです(考え方はそのまま同じ)。
⚠️ ハマりどころ
- RLSの有効化を忘れる … 表を作っただけで
enable row level securityを実行しないと、その表は全公開(誰でも全行を読める)。4つ全部でONにしたか、作った直後に確認する。 user_idを入れ忘れる … 行に持ち主が入っていないと、RLSのauth.uid() = user_idに合わず、自分のなのに見えない/逆に他人のが見える事故に。insert のたびにuser_idを必ず入れる(DB側でdefault auth.uid()を付けておくと入れ忘れにくい)。created_atで並べ替えない … 発言を取り出すとき順番を指定しないと、会話がちぐはぐに。再開・要約のときは必ずcreated_atの昇順(古い順)で並べる。
📝 ことばメモ
- 主キー(primary key):その表の中で1行をただ1つに決める目印。
conversationsならid、usageなら「user_id+day」の組 - 外部キー(foreign key/references):別の表の行を指し示す札。
messages.conversation_idがconversations.idを指す、など - RLS(Row Level Security):行ごとに見える・見えないを決めるDBのルール。ここでは「自分の行だけ」(
auth.uid() = user_id) - インデックス(索引):よく絞り込む列に張る“さくいん”。表が大きくても検索が速いまま
- status:発言の状態(
done/pending/error)。途中で止まった・失敗した発言を見分ける(第7章)