第7章 フォローする — 人と人を結ぶ(中間テーブル)
📖 この章のゴール:「AさんがBさんをフォローする」という、人と人のつながりをデータベースに保存できるようになります。そのとき出てくる「多対多(たたいた)」という関係と、それを表す「中間テーブル」という考え方を、たとえ話でつかみます。
📱 Twitterではこう見える
Twitterには「フォロー」というしくみがあります。気になる人を見つけたら、その人のプロフィールにある「フォロー」ボタンを押す。すると、その人のつぶやきが自分のタイムラインに流れてくるようになります。
ここで大事なポイントが2つあります。
-
フォローには「向き」がある(片方向) AさんがBさんをフォローしても、BさんがAさんをフォローしているとはかぎりません。「自分が一方的に応援している」という関係もふつうにあります。お互いがフォローし合っていたら「相互フォロー」と呼びますが、それはたまたま両方向のフォローがそろっただけです。
-
ボタンを押すと「関係」が1つ増える フォローボタンを押すたびに、「誰が誰をフォローした」という記録が1つ、世界のどこかに足されていきます。アンフォロー(フォローをやめる)すると、その記録が1つ消えます。
この章では、この「フォローした・されたの記録」をデータベースにどう残すかを考えます。
🤔 「AさんがBさんをフォロー」をどう保存する?(🟢 基礎)
第2章(02-what-is-a-user)で、ユーザーは1人1行の「名簿」として表に入っていることを学びました。では、フォローの情報はどこに書けばいいでしょう?
❌ ユーザーの行に「フォロー一覧」を書くのはムリ
最初に思いつくのは「ユーザーの行に、フォローしている人の名前を全部書く」という方法かもしれません。でもこれはうまくいきません。
なぜなら、フォローする相手は何人にでも増えるからです。0人かもしれないし、1000人かもしれない。表の1つのマス(列)に、何人ぶん入るか分からないものを詰め込むのは無理があります。Excelで「友だち欄」に名前を100人ぶん書こうとして列がパンクする、あの感じです。
⭕ フォロー専用の表をつくる
そこで発想を変えます。「フォロー関係」だけを記録する、専用の表を新しくつくるのです。この表では、
1行 = 1つのフォロー関係
というルールにします。AさんがBさんをフォローしたら1行。AさんがCさんをフォローしたら、もう1行。こうすれば、何人フォローしても「行を足すだけ」で済みます。
この「2つの表のあいだをつなぐためだけの表」を、中間テーブル(ちゅうかんテーブル)と呼びます。学校の「出席名簿」とは別に、「誰がどの委員会に入ったか」を1行ずつ書いた紙を用意するイメージです。
「多対多」って何?
ユーザーとフォローの関係は、こう言えます。
- 1人のユーザーは、たくさんの人をフォローできる
- 1人のユーザーは、たくさんの人からフォローされる
両側が「たくさん」になっています。これを 多対多(たたいた) の関係と呼びます。多対多は表1つでは表せないので、あいだに中間テーブルをはさんで表すのが定石です。
「向き」を名前で区別する
フォローには向きがあるので、2人の役割に名前をつけます。
- follower(フォロワー)…フォローする人(する側)
- followee(フォロイー)…フォローされる人(される側)
「フォローする人 → フォローされる人」という矢印で覚えると混乱しにくいです。
🛠 Supabaseでこう作る(🟢 基礎)
それでは、フォロー関係をしまう中間テーブル follows(フォローズ)をつくっていきます。Supabaseの「SQL Editor」に貼り付けて実行してください。
① follows テーブルをつくる
create table follows (
follower_id uuid not null references auth.users (id) default auth.uid(),
followee_id uuid not null references auth.users (id),
created_at timestamptz not null default now(),
primary key (follower_id, followee_id)
);
1行ずつ読むと:
create table follows (… 「follows という名前の表をつくります」という宣言。中身をここから書きます。follower_id uuid not null references auth.users (id) default auth.uid(),… フォローする人のID。uuidは長い識別番号の型。references auth.users (id)で「これはログインユーザー一覧(auth.users)の id とつながっている」と約束します(これを外部キーといいます)。default auth.uid()は「書かなければログイン中の自分のIDを自動で入れる」という意味。followee_id uuid not null references auth.users (id),… フォローされる人のID。これも auth.users とつながった外部キー。not nullなので空っぽは禁止です。created_at timestamptz not null default now(),… フォローした日時。now()でその瞬間の時刻が自動で入ります。primary key (follower_id, followee_id)… この表の「主キー(=その行を見分ける目印)」を、follower_id と followee_id の2つの組み合わせにします。);… 表の定義はここまで、という閉じカッコ。
ここで一番大事なのが最後の 複合主キー(ふくごうしゅキー) です。主キーは「同じものは2つ存在できない」というルールを表します。主キーを (follower_id, followee_id) の組にすると、「AさんがBさんをフォロー」という同じ組み合わせは1回しか入れられないようになります。つまり、うっかり同じ人を二重にフォローしてしまう事故を、データベースが自動で防いでくれるのです。
② RLS(鍵)をかける
第4章(04-data-isolation)で学んだ RLS(行ごとの鍵)を、この表にもかけます。
alter table follows enable row level security;
create policy "フォロー関係は誰でも読める"
on follows for select using ( true );
create policy "自分のフォローを作る"
on follows for insert with check ( auth.uid() = follower_id );
create policy "自分のフォローを消す"
on follows for delete using ( auth.uid() = follower_id );
1行ずつ読むと:
alter table follows enable row level security;… follows 表の「行ごとの鍵」をオンにします。これを忘れると鍵がかからず素通しになるので注意。create policy "フォロー関係は誰でも読める" on follows for select using ( true );… 読む(select)ときのルール。using ( true )は「いつでもOK」。誰が誰をフォローしているかは公開情報なので、みんなが見られるようにします。create policy "自分のフォローを作る" on follows for insert with check ( auth.uid() = follower_id );… 作る(insert)ときのルール。auth.uid() = follower_id、つまり「フォローする人=ログイン中の自分」のときだけ追加を許可します。他人になりすまして勝手にフォローを作れないようにする鍵です。create policy "自分のフォローを消す" on follows for delete using ( auth.uid() = follower_id );… 消す(delete)ときのルール。自分が follower の行、つまり「自分がしたフォロー」だけアンフォローできます。他人のフォローは消せません。
③ アプリからフォロー・アンフォローする
ボタンを押したときに動かす JavaScript はとても短く済みます。相手のuserId には、フォローしたい人のIDが入っているとします。
// フォローする
await supabase.from('follows').insert({ followee_id: 相手のuserId });
// アンフォローする
await supabase.from('follows').delete().eq('followee_id', 相手のuserId);
1行ずつ読むと:
await supabase.from('follows').insert({ followee_id: 相手のuserId });… follows 表に1行追加します。渡すのはfollowee_id(される人)だけ。する人follower_idは①でdefault auth.uid()にしたので、ログイン中の自分が自動で入ります。だから書かなくてよいのです。await supabase.from('follows').delete().eq('followee_id', 相手のuserId);… follows 表から行を消します。.eq('followee_id', 相手のuserId)で「followee_id がこの人の行」を狙い撃ち。さらに RLS が「自分が follower の行だけ」に絞るので、ちゃんと自分のフォローだけが消えます。
⚠️ ハマりどころ
-
自分で自分をフォローできてしまう 上の定義のままだと、follower も followee も自分、という行が作れてしまいます。防ぎたいときは、テーブルに
check (follower_id <> followee_id)というルール(<>は「等しくない」の意味)を足すと、自分自身へのフォローをデータベースが拒否します。 -
二重フォローしてしまう これは①の 複合主キー
(follower_id, followee_id)がすでに防いでくれています。もし複合主キーを忘れると、同じフォローが何行も増えてしまうので、主キーの設定は必ず入れましょう。 -
follower と followee の向きを混同する 「する人」と「される人」を逆に入れると、フォローの向きが反対になってしまいます。「follower(する人)→ followee(される人)」の矢印を、コードを書くたびに心の中で唱えてください。
-
フォロワー数を毎回 count で数えると、大きくなったとき重い 「この人のフォロワーは何人?」を知りたくて、毎回 follows 表を全部数える(count する)方法は、利用者が少ないうちは平気です。でも何百万行にもなると、毎回かぞえるのは時間がかかって重くなります。この対策は第12章(スケール=大規模化)であらためて扱います。今は「数えすぎ注意」とだけ覚えておけば十分です。
🤖 AIに頼むなら(Vibe codingのコツ)
中間テーブルと RLS は、言葉でしっかり伝えれば AI がきれいに書いてくれます。「向き」と「複合主キー」を必ず指定するのがコツです。
🗣 プロンプト例 「Supabase で、フォロー関係をしまう中間テーブル
followsを作って。列は follower_id(フォローする人), followee_id(フォローされる人), created_at。follower_id は default auth.uid()。複合主キーは (follower_id, followee_id) にして二重フォローを防いで。RLS も付けて、読むのは誰でも可、作る・消すは自分が follower のときだけ。自分自身はフォローできないようにして。」
チェックリスト(AIの答えを確認するポイント):
- 向きは合っている? follower=する人 / followee=される人 になっているか
- 複合主キー
(follower_id, followee_id)で二重フォローを防げているか - RLS はオンになっていて、insert / delete が「自分が follower のときだけ」か
- 自分自身フォロー禁止(
check (follower_id <> followee_id))を入れたか - フォローのとき followee_id だけ渡せば動くか(follower_id は自動)
📝 ことばメモ
- 多対多(たたいた)…両側が「たくさん」でつながる関係。1人がたくさんフォローし、たくさんからフォローされる、のような形。
- 中間テーブル…多対多の関係を記録するためにあいだにはさむ専用の表。1行=1つの関係。
- follower / followee…follower はフォローする人、followee はフォローされる人。向きを表す名前。
- 複合主キー…複数の列の組み合わせを「その行の目印」にした主キー。同じ組み合わせが二重に入るのを防ぐ。
- 外部キー(references)…「この列の値は、あちらの表の id とつながっている」と約束するしくみ。バラバラな表どうしを安全に結びつける。
➡️ 次の章へ
これで「誰が誰をフォローしているか」をデータベースに残せるようになりました。次の第8章では、このフォロー情報を使って 「フォローした人だけのつぶやきが流れるタイムライン(ホームタイムライン)」 を作ります。第6章(06-public-timeline)の「全員のタイムライン」と、follows の関係を組み合わせるのがポイントです。
➡️ 第8章 ホームタイムライン(08-home-timeline)へ