第8章 ホームタイムライン — フォロー中の人だけ表示(テーブルをまたぐ)

📖 この章のゴール:「自分がフォローしている人」の公開ツイートだけを、新しい順にならべて表示できるようになる。ここで初めて 2つの表(テーブル)をまたいで データを取り出します。これは Twitter というアプリの「心臓部」です。

← 目次・はじめにへもどる


📱 まずは画面をイメージしよう

Twitter(ツイッター)を開いたとき、最初に見えるのが「ホームタイムライン」です。

ここには、あなたがフォローしている人の投稿が、新しいものから順に流れてきます。

  • フォローしている人がツイートする → あなたのホームに出てくる
  • フォローしていない人がツイートする → あなたのホームには出てこない

つまりホームは、「世界中のツイート全部」ではなく、「あなたが選んだ人たちのツイートだけ」を集めた、あなた専用の流れなのです。

第6章で作った「みんなのタイムライン(公開タイムライン)」は、世界中の公開ツイートが全部流れる場所でした。今回のホームタイムラインは、そこから「フォロー中の人だけ」にしぼりこんだもの、とイメージしてください。


🤔 どうやって作るの?

🟢 必要なのは「2つの表」

ここがこの章でいちばん大事なところです。

ホームタイムラインを作るには、2つの表(テーブル)の力を借りる必要があります。

  • tweets(ツイート)の表… だれが・いつ・何を投稿したか
  • follows(フォロー関係)の表… だれが・だれをフォローしているか

第7章で作った follows の表を思い出してください。これは「Aさんが Bさんをフォローした」というつながり(関係)を記録する表でした。1行が1つのフォロー関係です。

follower_id(フォローした人) followee_id(フォローされた人)
わたし たろうさん
わたし はなこさん

この表を見れば、「わたしがフォローしている人」がわかります。

🟢 手順は「2段がまえ」で考える

いきなり全部やろうとすると頭がこんがらがるので、2つのステップに分けて考えます。

  1. ステップ①follows の表から、「わたし(= follower)がフォローしている followee_id(フォローされた人のID)」を集める
  2. ステップ②:その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章では、その「速さの工夫」に踏みこみます。同じ結果を何度も取り直さずにすませる キャッシュ という考え方と、その落とし穴を学びましょう。

➡️ 第9章 キャッシュ — 速さの工夫と落とし穴へ

← 目次・はじめにへもどる