第3章 ツイートしてみる — はじめてのデータ保存

📖 この章のゴール:データを「保存する(書く)」と「取り出す(読む)」という、Webアプリの基本動作を、実際にやってみる。 ← 目次・はじめにへもどる


📱 Twitterではこう見える

文章を書いて「ポスト」を押すと、自分の投稿が一覧の一番上に増えます。 いちど投稿すれば、アプリを閉じても、別の端末で開いても、ちゃんと残っています。

やっていることは、たった2つだけ。

  • 保存する(投稿=データを書き込む)
  • 取り出す(一覧=データを読み込む)

この章では、この2つを実際に作ります。


🤔 「投稿する」って、何をしているの?(🟢 基礎)

第1章で、データは 倉庫(データベース) に保存される、と説明しました。投稿とは——

倉庫の「表」に、1行 書き足すこと。

「表」は テーブル と呼びます。イメージは、第1章でも出てきた 超巨大な表計算(Excel)の1シートです。

  • 列(カラム) … 「項目」。あらかじめ決めておく(例:本文、時刻)
  • 行(レコード) … 1件のデータ。1行=1つのツイート

今回作る tweets テーブルは、こんな形にします。

列(カラム) 入るもの
id 1件ごとの通し番号(自動) 1, 2, 3
body ツイート本文 はじめてのツイート!
created_at 投稿した時刻(自動) 2026-06-26 12:00

📓 たとえ話:ノートに書き足す ノートに1行ずつメモを足していくのと同じです。id はページ番号、body は内容、created_at は書いた時刻。投稿するたびに、新しい1行が増えていきます。

🔖 いまは「誰の投稿か」を入れていないことに注目してください。持ち主(user_id)は、次の第4章で足します。この章では、まず “保存して読み出す” だけに集中します。


🛠 Supabaseでこう作る(🟢 基礎)

① テーブルを作る

Supabaseの管理画面(SQL Editor)で、次のSQLを実行します。

create table tweets (
  id         bigint generated always as identity primary key,  -- 1件ごとの通し番号(自動)
  body       text not null,                                    -- ツイート本文(空はダメ)
  created_at timestamptz not null default now()                -- 投稿した時刻(自動)
);

1行ずつ読むと:

  • create table tweets ( … 「tweets という名前の表を作る」という宣言。
  • id ... generated always as identity primary keyidDBが自動で振る通し番号primary key(主キー)は「行を区別する、重複しない目印」。
  • body text not null … 本文を入れる列。text は文字、not null は「必ず何か入れる(空はダメ)」。
  • created_at ... default now() … 投稿時刻。default now() は「書いた瞬間の時刻を自動で入れる」。

② 投稿する(1行 追加 = insert)

ブラウザ側から、こうお願いします。

// 「この文章を保存して」とお願いする
const { data, error } = await supabase
  .from('tweets')
  .insert({ body: 'はじめてのツイート!' })
  .select();

1行ずつ読むと:

  • supabase.from('tweets') … 「tweets 表に用がある」と伝える。
  • .insert({ body: '...' })body を入れた 1行を追加して、とお願い。
  • .select() … 追加した結果(idcreated_at 付き)を返してもらう
  • const { data, error } … 結果は data(成功した中身)か error(失敗の理由)で受け取る。

💡 idcreated_at書いていないのに入ります。①で「自動」と決めたので、DBが勝手に付けてくれるのです。

③ 一覧を取り出す(読む = select、新しい順)

// 「投稿の一覧を、新しい順でちょうだい」
const { data, error } = await supabase
  .from('tweets')
  .select('*')
  .order('created_at', { ascending: false });

1行ずつ読むと:

  • .select('*') … 「全部の列をちょうだい」(* は「全部」)。
  • .order('created_at', { ascending: false }) … 「created_at新しい順に並べて」(ascending: false = 新しいものが上)。

あとは、受け取った data(ツイートの配列)を画面に並べるだけで、タイムラインの原型ができます。


⚠️ ハマりどころ

  • error を見ないinsertselect失敗することがあります(ネット切れ・設定ミスなど)。if (error) { ... } で必ず確認を。
  • 「保存したはずなのに表示されない」 … よくある原因は、第4章で出てくる RLS(鍵)の設定や、別アカウントでの確認。いまは1人で試している前提でOK、整理は第4章で。
  • 本文をそのまま画面に貼る … 利用者が書いた文字をHTMLへ生のまま出すと、後述の XSS(第10章)の入口に。ふつうは画面ライブラリが自動で安全化してくれますが、「人の入力は要注意」と頭の隅に。
  • いまは “持ち主” がない … だから今は全員の投稿が混ざり、誰でも消せる状態。これは欠陥ではなく、第4章でわざと直すための出発点です。

🤖 AIに頼むなら(Vibe codingのコツ)

🗣 プロンプト例: 「Supabaseで tweets テーブル(idbodycreated_at)を作って。投稿する insert と、新しい順の一覧 select の最小コードを、error 処理つきで書いて。いまは user_id や RLS はまだ入れないで(次の段階でやる)」

出てきたコードのチェックポイント:

  1. insert のあとに .select()結果を確認している?
  2. errorちゃんと見ている?
  3. 一覧が order(...)新しい順になっている?

📝 ことばメモ

  • テーブル(表):データをためる表。列(カラム)行(レコード)でできている
  • カラム(列)idbodycreated_at などの「項目」
  • レコード(行):1件のデータ=1つのツイート
  • insert / select追加する / 取り出す
  • 主キー(primary key):行を区別する、重複しない番号(id

➡️ 次の章へ

保存と表示ができました。でも、いまの tweets「誰の投稿か」が無く、全員のものが丸見え・誰でも消せる状態です。次の 第4章 で、投稿に 持ち主(user_id を付け、RLS(鍵) をかけて、「自分のものは自分だけ」を実現します。ここが教材の背骨です。

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