iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Vibe Coding

《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》系列 第 15 篇

【Day 15|海面之下】資料庫與 Schema:從 data.ts 到真正的資料表

  • 分享至 

  • xImage
  •  

第二幕結束時,專案中的畫面已經長得稍微像樣了。但如果今天出了一張新專輯,我想把它加進網站,要做的事情是:打開 data.ts,照格式多寫一筆資料,commit、push,然後重新部署。

而且目前的實作,是先把整份 data.ts 交給瀏覽器,再由瀏覽器做搜尋和篩選。25 張沒什麼,但如果變成 2500 張,每個人都得先拿到一大堆根本沒看到的資料。

到目前為止,我們做的都是海面上看得到的東西。從今天開始,要潛到海面之下:讓資料離開程式碼,住進真正的資料庫。


一、為什麼不能全部塞在前端?

先想清楚,資料一直寫在 data.ts 裡,到底會出什麼問題:

問題 例子
改資料就要改程式 新增一張專輯,要 commit、push、重新部署
資料全部送到瀏覽器 現在 25 張還好,2500 張呢?每個人都要先下載全部
使用者產生的資料沒地方放 收藏現在只能存在瀏覽器;之後使用者回報的資料錯誤,要存去哪?
沒辦法控制誰看得到什麼 Day 6 說過:送到使用者手上的東西,沒有秘密

前兩個是「麻煩」,後兩個是「做不到」。收藏要跟著帳號走、回報要讓管理員看得到——這些都需要一個不在任何人瀏覽器裡、大家都連得到的地方。

那個地方,就是資料庫。


二、選哪一種資料庫?

2.0 我曾走過的路

我在另一個專案裡換過三次資料庫:NoSQL 格式自由、上手快,但資料之間的關係一多就開始吃力;SQLite 整個資料庫就是一個檔案,本機很方便,但放進 Vercel 這類沒有持久磁碟的環境,寫進去的內容不能當成永久資料——我需要一個獨立於網站程式之外的資料庫;最後選了 PostgreSQL,關聯式,而且是一個獨立存在、有網址可以連過去的資料庫。

回頭看之前畫的關係圖:藝人有很多專輯,專輯有很多版本,版本有很多內容物。這張圖本身,就是選關聯式資料庫的理由。

2.1 資料庫平台怎麼選?

選定資料庫類型之後,還要決定「誰來幫你管」。常見的幾個平台:

平台 資料庫類型 除了資料庫還提供什麼 適合
Supabase PostgreSQL(關聯式) 登入、檔案儲存、權限規則 需要關聯式資料,又想要登入一起處理
Neon PostgreSQL(關聯式) 主要就是資料庫 只需要資料庫,登入另外處理
PlanetScale MySQL(關聯式) 主要就是資料庫 已經熟悉 MySQL 的團隊
Firebase Firestore(文件式) 登入、檔案儲存、推播 資料關係簡單、偏即時同步的 App
MongoDB Atlas MongoDB(文件式) 主要就是資料庫 資料格式變化大、關係少

MySQL 跟 PostgreSQL 一樣是關聯式資料庫,都用資料表和 SQL,差別在細節功能和生態系。
另外,AWS、Google Cloud 這類大型雲端平台也有自己的資料庫服務(例如 AWS 的 RDS),能選的資料庫種類多、控制權更大,但要自己設定的東西也比較多。平台有非常多種可以選擇,這邊舉的只是冰山一角。

我認為這個專案可以採用關聯式資料庫,而且後面又要用到登入和權限規則,所以我選 Supabase。

那 Prisma 呢? 它能讓你用 TypeScript 的寫法操作資料庫,還會自動產生型別,我在另一個專案用過,很好用。(它是一種ORM,有興趣可以上網查一下)但這次後面要接 Supabase 的登入和權限規則,今天先不做這個,把這條線走完再說。


三、從型別到資料表

Day 9 我們替資料定了型別。現在要把這些型別,變成資料庫裡的資料表。

3.1 型別和資料表,不一定一對一

先這樣理解會比較直覺:

TypeScript 裡的概念 資料庫裡常見的做法
一種主要的東西,例如 Album 通常會有一張對應的資料表
一個簡單的屬性,例如 title 通常會有一個對應的欄位
很多筆資料 很多列
巢狀的關係,例如專輯裡的版本 常拆成另一張表,用外鍵連起來

程式裡的型別和資料庫的表,不一定是一對一的。型別描述的是「程式希望拿到什麼形狀」;schema 描述的是「資料怎麼存比較合理」。

3.2 巢狀的資料,為什麼要拆成另一張表?

真正需要想一下的,是巢狀的資料。在之前設計的型別裡,版本是放在專輯裡面的一個陣列:

type Album = {  id: string;  title: string;  versions: AlbumVersion[];   // 版本放在專輯裡面};

PostgreSQL 其實可以把一整包 JSON 塞進某個欄位。但版本不是一段單純的附加文字:它自己還有名稱、格式、內容物、成員款,而且之後可能需要獨立搜尋或修改。所以這裡更適合把它拆成另一張表,每一列記著「我屬於哪一張專輯」:

albums(專輯)
┌─────────────────────┬──────────────┐
│ id                  │ title        │
├─────────────────────┼──────────────┤
│ aespa-armageddon    │ Armageddon   │
└─────────────────────┴──────────────┘
          ↑
          │ 屬於誰
          │
album_versions(版本)
┌──────────────┬─────────────────────┬──────────────┐
│ id           │ album_id            │ name         │
├──────────────┼─────────────────────┼──────────────┤
│ superbeing   │ aespa-armageddon    │ Superbeing   │
│ zine         │ aespa-armageddon    │ Zine         │
└──────────────┴─────────────────────┴──────────────┘

這裡出現兩個常聽到的名詞:

  • 主鍵:每一列的身分證,不能重複。例如專輯的 id
  • 外鍵:「我屬於誰」。例如版本表裡的 album_id,指向它所屬的那張專輯

還記得 Day 9 說的嗎:替每個欄位問一句「它屬於誰」。 那個問題的答案,到了資料庫裡,就變成了外鍵。

3.3 請 AI 提出 schema

跟之前一樣,先把結構談清楚,再讓 AI 寫程式。 以下是我給 Codex 的 prompt:

請根據 data.ts 裡的型別,設計對應的 PostgreSQL 資料表,準備放在 Supabase 上。

  • 列出每一張資料表、欄位、型別、主鍵和外鍵
  • 說明每張表之間的關係
  • 不需要 tags 欄位

先不要建立任何東西,等我確認。

我確認過後的資料表關係大致上是這樣:

artist_memberships(成員關係)
 member_id   group_id
   │           │
   └─────┬─────┘
         ▼  兩個欄位都指向藝人
artists(藝人)
   │ 一個藝人,有很多張專輯
   ▼
albums(專輯)
   │ 一張專輯,有很多個版本
   ▼
album_versions(版本)
   │ 一個版本,有很多項內容物;也可能有很多種成員款
   ├───────────────────────────┐
   ▼                           ▼
version_inclusions(內容物)  version_designs(成員款)

多出來的 artist_memberships 是一張「關係表」:每一列記一組「誰是哪個團的成員」,例如 member_id 是 Jimin、group_id 是 BTS,兩個欄位都指回 artists。有了它,搜尋成員的名字,也找得到團體的專輯。(這是中間我告訴 AI 我希望有這個功能之後他做的修改。此外,資料表的設計方法並不唯一,要考量的點很多。)


四、建表與匯入

資料表的樣子確定了,接下來要真的把表建出來、把資料搬進去。

4.1 Migration:資料表的版本紀錄

資料表可以在 Supabase 後台用滑鼠點出來,但這樣做,專案裡通常只看得到「現在長什麼樣」,看不到「怎麼一步步變成這樣」:換一台電腦、開一個測試環境,就很難照樣重現。

比較好的做法,是把「建表」、「加欄位」這些動作 寫成一個一個的檔案 ,按順序執行。這些檔案就叫 migration,可以把它想成資料表結構的版本紀錄:Git 是記錄程式碼怎麼改變,migration 記錄 schema (資料庫結構)怎麼改變。已經套用到雲端的,就不要回頭改,而是再新增下一份。

要注意的是,migration 管的是結構,不是內容。之後新增、修改專輯資料,走後台或管理介面,不是再寫一份 migration。

4.2 匯入的方式,看習慣選

把表和資料送進 Supabase,常見的有三種方式:

方式 怎麼做 適合
SQL Editor 在 Supabase 後台貼上 SQL,按執行 資料不多,想親眼看每一步
Supabase CLI(我用的) migration 和初始資料都放在專案裡,用指令推上雲端 想留下紀錄,之後能在別的環境重建
寫程式匯入 寫一支程式,透過 API 一筆一筆寫入 資料要先從別的地方轉換;需要另一把高權限的金鑰(第五節會提到),只能在自己電腦上跑

沒有哪一種一定比較好。我選 CLI,因為這樣 migration 會留在專案裡、跟著 Git 一起被記錄。

4.3 我的流程

這次我自己只做了兩件事:在 Supabase 建立專案,以及在終端機登入 Supabase CLI ,其他的都基本上交給 AI 處理:

4.3.1 建立、連接專案(我處理)

https://ithelp.ithome.com.tw/upload/images/20260929/20178017s3CL8gprXs.png
【圖1|建立 Supabase 專案】 (要設定資料庫密碼,後面會用到)

以下是我在登入、連結專案所用的幾個終端機指令(須先安裝 supabase CLI,不知道怎麼安裝也可以請 AI 幫你安裝):
supabase login:登入 Supabase (會開瀏覽器,去得驗證碼,複製回終端機)
supabase init:初始化專案
supabase link --project-ref <你的-project-ref> :連接專案(會需要輸入當初創建專案所設的密碼)。
另外,<你的-project-ref> 就是你在專案首頁,網址的最後那串東西。

https://ithelp.ithome.com.tw/upload/images/20260929/20178017kxgS1K8EWQ.png
【圖2|透過 Supabase CLI 連接資料庫】

不過要記得,CLI 連上之後,能做的事很多——改結構、刪資料表,甚至清空整個雲端資料庫都可以。Git 的紀錄還有機會救回來,資料庫的資料刪了就沒了。所以我會這樣分工:

誰 做什麼
AI 可以做 寫 migration 和 seed、預演
要先讓我看過 SQL 真正推上雲端的資料庫
一律要我同意 刪除資料表、清空或重設資料庫(例如 supabase db reset --linked)

4.3.2 建表、上鎖、匯入( AI 處理)

這一步我其實沒特別寫什麼長的 prompt。因為在我確認 schema 之後,AI 有詢問我是否可以往下做,每做完一步就停下來問我要不要繼續;像 supabase db push 這種會動到雲端資料庫的指令,工具也會先跳出來,要我按同意才會執行。

如果你的工具不會停下來問,就把這幾條寫進 prompt:

  • 推上雲端之前先預演,讓我看過 SQL
  • 資料表要開啟 RLS,只允許讀取
  • 不要為了讓匯入成功而關閉權限保護
  • 不要執行任何刪除、清空或重設資料庫的指令
  • 完成後回報每張表的筆數

4.3.3 總結流程

  1. 連結專案、寫 migration:AI 把 CLI 連到我的 Supabase 專案,依照確認過的設計寫出 migration。如果過程中要輸入資料庫密碼,自己在終端機輸入,不要貼進對話框**。**
  2. 產生初始資料:AI 寫了一支小程式,從 data.ts 產生匯入用的 SQL,而不是手寫幾百行。這種「放入第一批資料」的動作叫做 seed,像播種一樣;它只用在新環境的第一次,之後不會每次重跑。
  3. 先預演,再推上去:先用 supabase db push --dry-run 看會執行哪些 migration,確認過再 supabase db push 建表,最後 supabase db push --include-seed 把資料一起推上去。
  4. 對數量:用 supabase inspect db table-stats --linked 看每張表的筆數,跟原本的資料對一下。

4.4 成果

搬完之後,AI 告知每張表的筆數:(擔心出錯的話可以從後台確認,但理論上機率很低)

artists albums album_versions version_designs version_inclusions artist_memberships
124 25 72 131 242 105

https://ithelp.ithome.com.tw/upload/images/20260929/20178017dPbvWVURb7.png
【圖3|匯入成果】(進入 supabase 網頁版後台,從左側可以選 Table Editor,可查看目前有的資料)


五、交出鑰匙:網址和公開金鑰

資料庫建好了,接下來要讓網站可以讀得到,現在是第一次需要金鑰的時候。

5.1 兩類金鑰,差別很大

Supabase 有兩類金鑰:

金鑰 能做什麼 可以放在前端嗎?
Publishable key(公開金鑰) 能做的事,由剛剛設的權限規則決定 可以,它本來就設計成放在前端
Secret key(管理者金鑰) 權限很高,通常不受權限規則限制 絕對不行,只能放在可信任的伺服器環境

如果看到比較舊的教學,這兩把金鑰可能叫做 anon 和 service_role。以你後台實際看到的名稱為準。

這篇目前只需要公開金鑰。 管理者金鑰是給「寫程式匯入」或之後的管理功能用的,今天我沒有用,就先不放。

這裡有一個 Next.js 特有、很容易出事的細節:

環境變數的名稱,如果以 NEXT_PUBLIC_ 開頭,就會被送到瀏覽器。

公開金鑰可以用 NEXT_PUBLIC_ 開頭,管理者金鑰絕對不行 ——加了,就等於把「可以讀寫所有資料」的權限公開給全世界。AI 有時候為了「讓程式能動」,會把所有金鑰都加上 NEXT_PUBLIC_,這一條等一下會寫進 prompt。(怕的話也可以提前跟 AI 說)

5.2 金鑰放在哪裡

之前提過,可以用 .gitignore 決定哪些檔案不要進 commit。今天就來實際用一次。

金鑰放在專案裡的 .env.local 檔案,而不是直接寫在程式碼裡:

NEXT_PUBLIC_SUPABASE_URL=專案網址
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=公開金鑰

進入 supabase 專案後,在頂部有一個綠色「connect」按鈕,點進去之後,選擇 next.js,可以看得到這兩個參數

https://ithelp.ithome.com.tw/upload/images/20260929/20178017CmGW90yMLe.png
【圖4|參數位置】 (也可以直接點 Copy Prompt 交給 AI,讓 AI 處理。 但通常在讓 AI 處理之前,我會先確認他具體改什麼、為什麼改這個、是否會有動到其他東西。)

存好之後,可以跑一次 git status,理論上 .env.local 不應該出現在清單裡。如果不小心 commit 甚至 push 了,光是把檔案刪掉並不夠——它已經留在 Git 的紀錄裡了,要去 Supabase 重新產生 一組新的金鑰,讓舊的失效。

5.3 .gitignore 擋得住 Git,擋不住 AI

這裡有一件很容易誤會的事:

.gitignore 只代表 Git 不會 commit,不代表 AI 工具看不到。

Codex、Claude Code 這類 AI 工具,本來就能讀取專案裡的檔案。你沒有把金鑰貼進對話框,不代表它讀不到 .env.local。所以我會額外注意:

  • 不把金鑰貼進 prompt
  • 不請 AI 讀取或印出 .env.local,只告訴它「變數名稱是什麼」,或是需要的時候請他用變數名稱跟我溝通,而不是請他直接編輯環境變數檔案。
  • 如果工具有忽略檔案或限制權限的設定,把 .env.local 排除

之前有說「送到別人手上的東西,沒有秘密」,這次的「別人」,也包括 AI。

https://ithelp.ithome.com.tw/upload/images/20260929/20178017ymaVjfKy9a.png
【圖5|.gitignore 內容】


六、頁面改成查資料庫

變數設定好了,最後一步是讓頁面改成去資料庫拿資料。

6.1 改頁面

這一步同樣很短:我填好 .env.local、跟 AI 說一聲,它就先用公開金鑰測試讀取,再把首頁、列表、詳情頁的資料來源,一頁一頁換成 Supabase,最後自動跑了型別檢查和 build 測試。收藏頁裡用來顯示專輯資訊的資料,也改成從 Supabase 來;但目前「收藏了哪些」本身還是存在瀏覽器,等之後有了登入會搬。

改完之後,data.ts 並沒有消失:它還留著型別和標籤,也是之後重新產生 seed 的來源。只是專輯資料本身,已經不從這裡來了。

6.2 改一個名字,看網站會不會跟著變

最後,來做今天最有感的測試:不改任何程式,直接改資料庫裡的資料。

  1. 開著開發模式(npm run dev)
  2. 在 Supabase 後台的 Table Editor,找一張專輯,把 title 暫時改掉
  3. 重新整理網站,看列表和詳情頁

選 title 是因為它只是一般的文字欄位:網址和其他資料表用的是 id,改名字不會影響到別的東西。id 不要動。

https://ithelp.ithome.com.tw/upload/images/20260929/20178017iLF4ZkpAel.png
【圖6|更改某一張專輯名稱,重整頁面】 (沒有重啟開發伺服器)

名字變了——改一筆資料,終於不再是改一行程式碼了。 測完記得改回來。

部署之後再做一次同樣的測試,詳情頁不一定會馬上跟著變,這跟部署的設定有關。這個問題,等到部署那天再回來看。

最後順手確認兩件事:

  • git status 裡看不到 .env.local
  • 程式碼裡的 NEXT_PUBLIC_,只有網址和公開金鑰

換個領域:Day 9 的資料模型,變成資料表

Day 9 我們替別的產品也畫過資料模型。搬進資料庫,可能會長成這樣(取決於功能的需求而有變化):

記帳

accounts(帳戶)
   │ 一個帳戶,有很多筆交易
   ▼
transactions(交易)── category_id ──→ categories(分類)

點餐

menu_items(餐點)
   │ 一個餐點,有很多種規格
   ▼
item_options(規格:大小、甜度、冰塊)

不管是什麼產品,拆表的方式都一樣:替每個欄位問「它屬於誰」,屬於別人的,就用外鍵指過去。這部分,我通常會先詢問 AI,詢問他的建議,在詢問過程中也會詳細的描述我需要的功能可能有哪些,因為不同的功能,對於資料庫的設計可能會有不同。


結語與明日預告

今天做的事,說穿了就是一件:讓資料離開程式碼。 在那之前,資料是網站的一部分,改資料等於改程式。現在,資料住在海面之下,網站只是在需要的時候去把它撈上來。


不過,你可能注意到一件事。今天從頭到尾,我們好像一支 API 都沒寫,網站就拿到資料了。但其實,程式碼裡的 Supabase 套件,背後做的事情就是帶著網址和公開金鑰,向 Supabase 發出請求、拿回結果。

我們早就在用 API 了。

那 API 到底是什麼?常聽到的 CRUD、RESTful 又是什麼意思?明天,我們先不寫程式,用 Postman 親手打幾次 API,把這些概念弄清楚。

我們明天見。


上一篇
【Day 14|三艘小艇】同一份 Skill、三個 AI,會走出三種什麼設計?
下一篇
【Day 16|信號繩】CRUD 與 RESTful API:程式之間怎麼約定彼此說什麼
系列文
《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言