第8章 ホームタイムライン — フォロー中の人だけ表示(テーブルをまたぐ)
📖 この章のゴール:「自分がフォローしている人」の公開ツイートだけを、新しい順にならべて表示できるようになる。ここで初めて 2つの表(テーブル)をまたいで データを取り出します。これは Twitter というアプリの「心臓部」です。
📱 まずは画面をイメージしよう
Twitter(ツイッター)を開いたとき、最初に見えるのが「ホームタイムライン」です。
ここには、あなたがフォローしている人の投稿が、新しいものから順に流れてきます。
- フォローしている人がツイートする → あなたのホームに出てくる
- フォローしていない人がツイートする → あなたのホームには出てこない
つまりホームは、「世界中のツイート全部」ではなく、「あなたが選んだ人たちのツイートだけ」を集めた、あなた専用の流れなのです。
第6章で作った「みんなのタイムライン(公開タイムライン)」は、世界中の公開ツイートが全部流れる場所でした。今回のホームタイムラインは、そこから「フォロー中の人だけ」にしぼりこんだもの、とイメージしてください。
🤔 どうやって作るの?
🟢 必要なのは「2つの表」
ここがこの章でいちばん大事なところです。
ホームタイムラインを作るには、2つの表(テーブル)の力を借りる必要があります。
- tweets(ツイート)の表… だれが・いつ・何を投稿したか
- follows(フォロー関係)の表… だれが・だれをフォローしているか
第7章で作った follows の表を思い出してください。これは「Aさんが Bさんをフォローした」というつながり(関係)を記録する表でした。1行が1つのフォロー関係です。
| follower_id(フォローした人) | followee_id(フォローされた人) |
|---|---|
| わたし | たろうさん |
| わたし | はなこさん |
この表を見れば、「わたしがフォローしている人」がわかります。
🟢 手順は「2段がまえ」で考える
いきなり全部やろうとすると頭がこんがらがるので、2つのステップに分けて考えます。
- ステップ①:
followsの表から、「わたし(= follower)がフォローしている followee_id(フォローされた人のID)」を集める - ステップ②:そのIDの人たちが書いた公開ツイートを、
tweetsの表から取り出す
たとえ話でいうと、こんな感じです。
まず手元のノートに「わたしの友だちリスト」を書き出す(ステップ①)。 つぎに、そのリストにのっている人の投稿だけを、掲示板からひろってくる(ステップ②)。
このように「ある表の結果を使って、別の表をしぼりこむ」のが、データベースでいう 結合(join) や サブクエリ の入口です。むずかしそうな言葉ですが、やっていることは「友だちリストを作って、その人の投稿だけ拾う」だけ、と覚えておけば大丈夫です。
🛠 Supabase でこう作る
🟢 まずは「考え方」をSQLで見てみよう(イメージ)
データベースに直接お願いするときの言葉(SQL)で書くと、考え方がはっきりします。これは「こういう発想なんだな」と眺めるためのイメージです。
select * from tweets
where user_id in (select followee_id from follows where follower_id = auth.uid())
and is_public
order by created_at desc;
1行ずつ読むと:
select * from tweets:「tweets の表から、すべての列(情報)をください」とお願いしています。where user_id in (...):「ただし、user_id が この集合(かっこの中身)のどれかに当てはまる ものだけ」という条件です。in (...)は「このリストの中に入っているなら」という意味です。(select followee_id from follows where follower_id = auth.uid()):これが内側のお願い(サブクエリ)です。先に「follower_idが自分(auth.uid()= ログイン中の自分のID)になっている行のfollowee_id」、つまり「フォロー中の人ID一覧」を作っています。and is_public:「さらに、公開されているツイートだけ」という条件を足しています。order by created_at desc:「作られた日時(created_at)の 新しい順(desc= 大きい順 = 最近順)にならべて」という意味です。
ポイントは、内側のサブクエリで「友だちリスト」を先に作り、それを使って外側で tweets をしぼっている、という二段構えになっていることです。
🟢 Supabaseクライアントでの素直な「2段」
実際のアプリ(JavaScript)では、上の考え方をそのまま2ステップに分けて書くのがいちばんわかりやすいです。
// ① 自分がフォローしている人のID一覧(RLSで自分のフォローが取れる)
const { data: f } = await supabase.from('follows').select('followee_id');
const ids = f.map((x) => x.followee_id);
// ② その人たちの公開ツイートを新しい順で
const { data: tweets } = await supabase
.from('tweets')
.select('*')
.in('user_id', ids)
.order('created_at', { ascending: false });
1行ずつ読むと:
const { data: f } = await supabase.from('follows').select('followee_id');:followsの表からfollowee_id(フォローしている相手のID)だけを取り出して、結果をfという名前で受け取ります。第7章で作ったRLS(安全のしくみ)のおかげで、ここでは自分のフォロー行だけが自動的に取れます。const ids = f.map((x) => x.followee_id);:取れたデータfは「行のリスト」なので、mapを使って IDだけの単純なリスト(例:['たろうのID', 'はなこのID'])に作り直します。これがステップ①の「友だちリスト」です。const { data: tweets } = await supabase:ここからステップ②。tweetsの表に取りにいきます。.from('tweets').select('*'):「tweets の表から、すべての情報をください」。.in('user_id', ids):「ただし、user_idが さっき作ったidsのリストの中にある ものだけ」。SQLのin (...)と同じ役わりです。.order('created_at', { ascending: false }):「作られた日時の 新しい順 にならべて」。ascending: falseは「昇順ではない」=「降順(新しい順)」という意味です。
これで tweets の中に、「フォロー中の人の公開ツイート」が新しい順で入ります。
🔧 もっと良い方法もある(先に知っておこう)
上の「2段がまえ」は、しくみがわかりやすく、最初の練習にはぴったりです。ただし、フォローしている人がとても多くなると、ids のリストが何百・何千件にもなり、.in(...) の処理がだんだん重く(遅く)なっていきます。
そこで本格的なアプリでは、こんな工夫をします。
- ビュー(view)を作る:データベース側に「フォロー中の人の公開ツイートを集めた、見るための仮想の表」をあらかじめ用意しておく
- 関数(RPC)を作る:データベース側に「ホームタイムラインを返す処理」をひとまとめにしておき、アプリからは1回呼ぶだけにする
こうすると、毎回2段に分けたり、大量のIDを送ったりせずに、1回のお願いで結果が取れます。
このあたりの「速くするための工夫」は、第9章と第12章でくわしく扱います。今は「2段でも動くけど、いずれもっと良いやり方がある」と知っておくだけで十分です。
⚠️ ハマりどころ
実際にやってみると、つまずきやすいポイントがいくつかあります。
- だれもフォローしていないと、空っぽになる:
followsに1件もなければidsは空のリスト。するとtweetsも0件で、ホームがまっさらになります。バグではありません。「フォローしている人がまだいないだけ」です。新規ユーザー向けに「おすすめユーザー」を出すアプリが多いのは、このためです。 - 自分の投稿も混ぜたいとき:ホームに「自分のツイート」も出したいなら、
idsに自分のID(auth.uid())を足してからtweetsを取りにいきます。逆に「他人の投稿だけ」でよければ、何もしなくてOKです。 - N+1問題(エヌプラスワン問題):ツイートを20件取ったあと、「だれが書いたか」を表示するために1件ずつ著者のプロフィールを取りにいくと、お願いの回数が「1(ツイート)+20(プロフィール)」のように増えてしまいます。これを N+1問題といい、表示がガクッと遅くなる原因になります。プロフィールもまとめて1回で取るのがコツです(必要なIDを集めて、
.in('id', ids)で一気に取る、という同じ発想が使えます)。 inに大量のIDは重い:先ほども触れたとおり、フォロー数がふくらむと.in(...)は遅くなります。小規模なうちは問題ありませんが、「将来きつくなる場所」として頭の片すみに置いておきましょう(第12章)。
🤖 AIに頼むなら
自分でゼロから書くのが大変なときは、AI(Claude などのアシスタント)に頼むのも良い方法です。ただし「何がほしいか」をはっきり伝えるのがコツです。
🗣 プロンプト例
「Supabase で、ログイン中のユーザーのホームタイムラインを作りたい。自分がフォローしている人の公開ツイートだけを新しい順で取得して。
follows(follower_id, followee_id)からtweets(user_id, body, created_at, is_public)を 結合(join) かinでつなげて。各ツイートの著者プロフィールはまとめて取得して、N+1問題を避けて。JavaScriptのSupabaseクライアントのコードでお願い。」
AIが書いてくれたコードは、そのまま信じずに自分でチェックしましょう。次の3つを確認すれば安心です。
- ✅ フォロー中の人だけになっている?(フォローしていない人の投稿が混じっていないか)
- ✅ 公開ツイートだけになっている?(
is_publicの条件が入っているか) - ✅ 新しい順にならんでいる?(
created_atの降順になっているか)
📝 ことばメモ
- 結合(join/ジョイン):2つ以上の表を、共通の手がかり(今回は「人のID」)でつなげて、まとめて使うこと。「友だちリスト」と「投稿一覧」を突き合わせるイメージ。
- サブクエリ:あるお願い(クエリ)の中に入れる、別の小さなお願い。先に内側で「フォロー中の人ID一覧」を作るのに使った。
- in(イン):「このリストの中にあるものだけ」という条件のしぼりかた。
user_id in (...)で「このIDのどれかに当てはまる行」を選ぶ。 - ビュー(view):データベース側にあらかじめ用意しておく「見るための仮想の表」。よく使う取り出し方を保存しておけて、毎回書かずにすむ。
- N+1問題:一覧を1回取ったあと、その中身ごとに追加で1件ずつ取りにいってしまい、お願いの回数が増えて遅くなる問題。まとめて取れば防げる。
➡️ 次の章へ
ホームタイムラインで、「2つの表をまたいでデータを取る」感覚をつかめました。ただし、フォローが増えると遅くなる、という宿題も見えてきましたね。
次の第9章では、その「速さの工夫」に踏みこみます。同じ結果を何度も取り直さずにすませる キャッシュ という考え方と、その落とし穴を学びましょう。