py程式設計(三)課程網站
第 8 週 · 單元 3-1 · 專案一

記帳網站(三):換上雲端資料庫,正式上線

在自己電腦上,資料存在 SQLite 檔案裡;但 Vercel 主機上的檔案是暫時的。今天把線上網站的資料庫換成 Neon(雲端 Postgres),讓任何人新增的資料都會留下來。

W8記帳網站Vercel+Neon

本週結束時你會

今天的時間安排

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 週已做),今天只處理資料庫。

步驟 ⑤ 驗收:自動測試

提示詞 1 Antigravity
請先讀 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,但有幾個地方寫法不同:

差異SQLitePostgres
參數符號?%s
自動編號INTEGER PRIMARY KEY AUTOINCREMENTINTEGER GENERATED ALWAYS AS IDENTITY
現在時間datetime('now', 'localtime')now()
連線方式一個檔案路徑一串連線字串(DATABASE_URL)
提示詞 2 Antigravity
請先讀 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(看看差異是怎麼被集中處理的)
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. 建立資料庫並連到專案

  1. Vercel 打開 ledger-web 專案 →「Storage」分頁 → 建立資料庫,選擇 Neon(Postgres),方案選免費。
  2. 連結到 ledger-web 專案。Vercel 會自動新增環境變數 DATABASE_URL(到 Settings → Environment Variables 可以看到名稱,但不要把值複製到任何地方)。

Vercel 的介面名稱可能更新,以實際畫面為準。

2. 在 Neon 建立資料表並匯入示範資料

從 Vercel 的 Storage 頁面點進 Neon 的管理後台,打開 SQL Editor:

提示詞 3 Antigravity
請讀 schema_postgres.sql 與 expenses_範例.csv,產生一個 neon_setup.sql:先建立資料表,再用 INSERT 匯入 CSV 的 80 筆資料(欄位名稱依照我的資料表)。
這個檔案我會手動貼到 Neon 的 SQL Editor 執行,所以請確認語法是 Postgres。不要把它加入 Git。
  1. 把 neon_setup.sql 的內容貼到 SQL Editor,按 Run。
  2. 在 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-104,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 週期中考):專案二「社團活動報名平台」,第一次處理多張資料表、登入與權限。