記帳網站(三):換上雲端資料庫,正式上線
在自己電腦上,資料存在 SQLite 檔案裡;但 Vercel 主機上的檔案是暫時的。今天把線上網站的資料庫換成 Neon(雲端 Postgres),讓任何人新增的資料都會留下來。
本週結束時你會
今天的時間安排
| 0:00–0:30 | 步驟 ⑤ 驗收【自己做】請 AI 依驗收標準寫測試 |
| 0:30–1:05 | 讓程式支援兩種資料庫【跟著做】 |
| 1:05–1:15 | 休息 |
| 1:15–1:50 | 步驟 ⑥ 上線【跟著做】Vercel 加入 Neon、建立資料表、匯入資料 |
| 1:50–2:20 | 線上驗收、同學互相試用、Demo 發表 |
| 2:20–2:30 | 專案回顧 |
Neon 的建立與連線需要學生自己的 Vercel 帳號操作,教室網路不穩時最容易卡在這段。建議前一週先請學生在家完成 Vercel 帳號註冊與 ledger-web 的第一次部署(第 6 週已做),今天只處理資料庫。
步驟 ⑤ 驗收:自動測試
請先讀 PROJECT.md 與需求說明書的「驗收標準」。 用 pytest 與 Flask 的 test_client 寫 tests/test_app.py: 1. 每個測試使用獨立的暫存資料庫,先建表並匯入 expenses_範例.csv 2. 至少涵蓋:10 月合計 4,898 元與 40 筆、列表新到舊、空月份、錯誤的 month 參數、月統計排序與 44%、與 9 月比較增加 952 元、新增成功、金額 0/-5/abc/1.5/1000001 被擋下且不寫入、日期 2026-02-30、刪除必須經過確認頁 3. 每個測試上方用一行中文說明對應哪一條驗收標準 執行 python -m pytest -q 給我看。有測試失敗時先告訴我原因,不要為了通過而修改 app.py 或預期值。
參考解答共 14 項測試。你的數量可以不同,但上面列的情況都要涵蓋。
讓程式支援兩種資料庫【跟著做】
SQLite 和 Postgres 都說 SQL,但有幾個地方寫法不同:
| 差異 | SQLite | Postgres |
|---|---|---|
| 參數符號 | ? | %s |
| 自動編號 | INTEGER PRIMARY KEY AUTOINCREMENT | INTEGER GENERATED ALWAYS AS IDENTITY |
| 現在時間 | datetime('now', 'localtime') | now() |
| 連線方式 | 一個檔案路徑 | 一串連線字串(DATABASE_URL) |
請先讀 PROJECT.md。我要部署到 Vercel,線上資料庫使用 Neon(Postgres)。請修改程式: 1. 建立 db.py,把所有資料庫連線集中在這裡:環境變數 DATABASE_URL 存在時用 psycopg 連 Postgres,不存在時照舊用 instance/ 裡的 SQLite 2. 程式其他地方的 SQL 一律寫 ?,由 db.py 在 Postgres 時轉成 %s;查詢結果兩種資料庫都回傳 dict 3. 建表 SQL 分成 schema_sqlite.sql 與 schema_postgres.sql,並列出兩者差在哪裡 4. requirements.txt 加入 psycopg[binary] 完成後在本機執行 python -m pytest -q,確認 SQLite 版仍然全部通過。只修改和資料庫有關的部分。
參考解答的 db.py(看看差異是怎麼被集中處理的)
"""資料庫連線:有環境變數 DATABASE_URL 時連到 Postgres(Neon),沒有時使用本機 SQLite。
兩種資料庫的差異都集中在這個檔案:
- 參數符號:SQLite 用 ?,Postgres 用 %s。程式其他地方一律寫 ?,由 run() 轉換。
- 建表語法:分別放在 schema_sqlite.sql 與 schema_postgres.sql。
"""
import os
import pathlib
import sqlite3
from flask import current_app, g
HERE = pathlib.Path(__file__).resolve().parent
def database_url():
return current_app.config.get("DATABASE_URL") or os.environ.get("DATABASE_URL")
def is_postgres():
return bool(database_url())
def get_db():
"""每個請求共用一個連線,查詢結果一律是 dict。"""
if "db" not in g:
if is_postgres():
import psycopg
from psycopg.rows import dict_row
g.db = psycopg.connect(database_url(), row_factory=dict_row)
else:
path = pathlib.Path(current_app.config["SQLITE_PATH"])
path.parent.mkdir(parents=True, exist_ok=True)
g.db = sqlite3.connect(path)
g.db.row_factory = lambda cur, row: {d[0]: v for d, v in zip(cur.description, row)}
return g.db
def close_db(_exc=None):
db = g.pop("db", None)
if db is not None:
db.close()
def run(sql, params=()):
"""執行一段 SQL 並回傳所有結果。SQL 中的參數一律寫 ?。"""
if is_postgres():
sql = sql.replace("?", "%s")
cur = get_db().execute(sql, params)
return cur.fetchall() if cur.description else []
def commit():
get_db().commit()
def init_db(seed=True):
"""建立資料表,並放入示範資料(會清除原有資料)。"""
schema = "schema_postgres.sql" if is_postgres() else "schema_sqlite.sql"
db = get_db()
scripts = [(HERE / schema).read_text(encoding="utf-8")]
if seed:
scripts.append((HERE / "seed.sql").read_text(encoding="utf-8"))
for script in scripts:
if is_postgres():
db.execute(script)
else:
db.executescript(script)
db.commit()驗證紀錄:參考解答的 14 項測試,在 SQLite 與 PostgreSQL 18 上各執行一次,28 項全部通過,兩種資料庫的結果完全相同。
這是本學期「換一套技術、流程不變」的第一次實際體驗。請學生打開 git diff 看這次改了哪些檔案:路由與頁面幾乎沒動,差異都集中在 db.py 與 schema 檔。這也是第 12 週改用 Next.js 的伏筆。
存檔並推上 GitHub:「支援 Postgres」。
步驟 ⑥ 上線:Vercel 加入 Neon【跟著做】
1. 建立資料庫並連到專案
- Vercel 打開 ledger-web 專案 →「Storage」分頁 → 建立資料庫,選擇 Neon(Postgres),方案選免費。
- 連結到 ledger-web 專案。Vercel 會自動新增環境變數
DATABASE_URL(到 Settings → Environment Variables 可以看到名稱,但不要把值複製到任何地方)。
Vercel 的介面名稱可能更新,以實際畫面為準。
2. 在 Neon 建立資料表並匯入示範資料
從 Vercel 的 Storage 頁面點進 Neon 的管理後台,打開 SQL Editor:
請讀 schema_postgres.sql 與 expenses_範例.csv,產生一個 neon_setup.sql:先建立資料表,再用 INSERT 匯入 CSV 的 80 筆資料(欄位名稱依照我的資料表)。 這個檔案我會手動貼到 Neon 的 SQL Editor 執行,所以請確認語法是 Postgres。不要把它加入 Git。
- 把 neon_setup.sql 的內容貼到 SQL Editor,按 Run。
- 在 SQL Editor 執行
SELECT SUM(amount), COUNT(*) FROM expenses WHERE substr(spent_on, 1, 7) = '2026-10';(欄位名稱依你的設計),應該得到 4898 與 40。
為什麼用 SQL Editor:這樣連線字串不必離開 Vercel 和 Neon,不會出現在你的電腦或對話框裡。金鑰越少地方出現,越安全。
3. 重新部署
環境變數是在部署之後才加入的,所以要重新部署一次:Vercel → Deployments → 最新一次部署的「⋯」→ Redeploy。或者推送任何一個新的存檔點,也會觸發重新部署。
線上驗收
| 檢查 | 正確結果 | 代表什麼 |
|---|---|---|
打開 /?month=2026-10 | 4,898 元、40 筆 | 連上了 Neon,資料匯入正確 |
| 新增一筆 → 重新整理 | 資料還在,合計增加 | 寫入的是雲端資料庫,不是暫時檔案 |
| 隔 10 分鐘後再打開 | 剛剛新增的資料還在 | 同上,再確認一次 |
| 手機打開、記一筆 | 表單好按、不跑版 | 真實使用情境 |
常見狀況
網站顯示錯誤,Logs 出現 relation "expenses" does not exist:Neon 上還沒建立資料表,回到 SQL Editor 執行 neon_setup.sql。
網站還在用 SQLite(新增的資料過一陣子消失):加入 Neon 之後沒有重新部署,或程式沒有讀取 DATABASE_URL。
Logs 出現 No module named psycopg:requirements.txt 漏了 psycopg[binary]。
其他錯誤:對 Antigravity 說「準備上線到 Vercel,請做上線前檢查」,並附上 Logs 的錯誤訊息。
Demo 發表與專案回顧
依專案工具包的 Demo 結構發表 3 分鐘:用一個具體的情境開場(「月底只剩 200 元,錢到底花去哪了?」),現場用公開網址操作,最後說一個你抓到的 AI 錯誤。
發表後填寫「⑥ 專案回顧」,最後一題的答案寫進下一個專案的 PROJECT.md 模板。
參考解答在 practice/s3-3-1_記帳網站/參考解答/ledger-web/。本機執行 flask --app app init-db 後即可展示;若要示範 Postgres 版本,設定 DATABASE_URL 指向 Neon 的測試分支,再執行 python -m pytest -q(另設 TEST_DATABASE_URL 可同時測兩種資料庫)。
本週檢核
第 10 週預告(第 9 週期中考):專案二「社團活動報名平台」,第一次處理多張資料表、登入與權限。