跳到主要內容

Web Day 31 PostgreSQL 上線:連線池與遷移

Web Day 31 PostgreSQL 上線:連線池與遷移

執行需求:需 Docker。Day 30 我們把 FastAPI 裝進容器、用 named volume 保存 SQLite 檔。今天要把 SQLite 換成正式環境等級的 PostgreSQL 17,並用 Docker Compose 同時啟動 API 與資料庫兩個服務。我們會說明連線池(connection pool)為什麼對 Postgres 特別重要、用 SQLAlchemy 2.0 的 create_engine 怎麼設定 pool 參數,以及怎麼在容器啟動時自動跑 Alembic 遷移。沒有 Docker 的讀者可以改裝本機的 PostgreSQL,整份設定檔只需要改一條連線字串就能用。

引言

SQLite 對單機開發非常友善——零設定、一個檔案就能跑、SQLAlchemy 預設就支援。但它本質上是「檔案型資料庫」,不適合多個 process 同時大量寫入,正式環境幾乎一定會遇到「寫入鎖死」、「連線數過高」、「無法水平擴展」三個問題中的至少一個。PostgreSQL 是開源關聯式資料庫裡功能最完整、效能最穩定的選項,從小型 Side Project 到中大型 SaaS 都有人用。今天就是把 Day 6 學過的 SQLModel 與 Day 9 學過的 Alembic,正式接到 PostgreSQL 上。

今天寫完之後,你會拿到一份可以一鍵啟動「API + PostgreSQL + 自動跑遷移」的 docker-compose.yml,並理解 connection pool 的 pool_size / max_overflow / pool_pre_ping 三個關鍵參數怎麼影響正式環境的穩定性。後面 Day 33 的 Caddy 反向代理會繼續疊在這個基礎上。

為什麼要換 PostgreSQL

SQLite 在開發環境沒問題,但它有三個限制讓正式環境不適合用:第一,SQLite 的寫入是「全檔鎖」,多個 process 同時寫入時只有一個能進去,其他全部等待;第二,SQLite 沒有連線數的概念,理論上一個 process 開幾百條連線都行,但實務上整個應用只能有一個 process 在寫;第三,SQLite 沒有內建的角色權限機制,所有人都用同一個權限存取資料庫。當使用者量成長到一定規模,這三個限制會先後爆炸,最後逼著你做一次大規模遷移。

PostgreSQL 解決了這三個問題:用 MVCC(Multi-Version Concurrency Control)讓讀寫不互相阻塞;用 process-per-connection 模型支援數百個並發連線;用 role 與 grant 機制做精細的權限控管。除此之外,PostgreSQL 還有 JSONB 型別、全文檢索、GIS 擴充、CTE 與 window function 等進階功能,是「長大後」最不容易後悔的選擇。對台灣的中小型團隊來說,PostgreSQL 的另一個優點是授權完全開源,不會被商業授權綁住,預算規劃上比較單純。

對這個系列來說,PostgreSQL 還有一個重要好處:等後面 Day 35 的「預約管理系統」要處理「同一時段不能有兩個預約」這種衝突檢查時,PostgreSQL 的 transaction isolation level 與 SELECT FOR UPDATE 機制可以確保兩個 client 同時下單時只有一個會成功,SQLite 雖然也有 BEGIN IMMEDIATE,但語意與效能都比不上 Postgres。今天先把基礎環境裝好,後續章節會把這個特性發揮出來。

PostgreSQL 17 跟前一代 16 比起來,主要強化了邏輯複寫、增強 VACUUM 與 incremental backup 等維運面功能;對應用層開發者來說最明顯的新功能是 JSON_TABLE 與 SQL/JSON 的標準化查詢,這在處理 JSONB 欄位時更直覺。本系列預設使用 PostgreSQL 17,未來若需要降到 16 也能直接相容,只有少數 SQL/JSON 寫法需要調整。

用 Docker Compose 同時啟動 API 與 Postgres

今天把 docker-compose.yml 擴充成兩個服務:api 與 db。db 服務用 postgres:17-alpine 映像,這是 PostgreSQL 17 官方提供的輕量變體,image 約 240 MB,啟動時間 2 到 3 秒。我們用 named volume `pg-data` 保存資料庫檔案,並設定健康檢查,確保 API 等 Postgres 完全就緒之後才啟動。

services:
  db:
    image: postgres:17-alpine
    container_name: booking-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: booking
      POSTGRES_PASSWORD: booking_dev_pw
      POSTGRES_DB: booking
    volumes:
      - pg-data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U booking -d booking"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 15s
    ports:
      - "5432:5432"

  api:
    build:
      context: .
      dockerfile: Dockerfile
    image: booking-api:dev
    container_name: booking-api
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql+psycopg://booking:booking_dev_pw@db:5432/booking
      POOL_SIZE: "5"
      MAX_OVERFLOW: "10"
    ports:
      - "8000:8000"
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2).status == 200 else 1)"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 15s

volumes:
  pg-data:

這份 compose 檔有四個關鍵設計:`depends_on: condition: service_healthy` 確保 API 等 Postgres 健康檢查通過才啟動;`db/init:/docker-entrypoint-initdb.d:ro` 讓首次啟動時自動執行 db/init/ 裡的 SQL(例如建立 extensions);`5432:5432` 把 Postgres 對應到主機,方便本機裝 pgAdmin 或 DBeaver 除錯;POSTGRES_USER 與 POSTGRES_PASSWORD 是 Postgres 首次啟動時的預設帳號,正式環境務必改成由 secrets 管理。

db/init 資料夾裡可以放 .sql 檔,Postgres 首次啟動會依檔名字母順序執行。我們放一個簡單的 extensions 安裝腳本,預先裝好 uuid-ossp 與 citext,這兩個 extension 在後面專案篇會用到。

-- db/init/00-extensions.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "citext";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

連線池的關鍵參數

PostgreSQL 是「每條連線一個 process」的模型,不像 SQLite 可以隨便開連線。在 Python 端用 SQLAlchemy 時,預設會為每次查詢開一條新連線,用完立刻關閉;這樣在並發量小的時候沒問題,但正式環境常常一秒鐘要處理幾十個請求,TCP 三次交握 + TLS 交握的成本會把 API 拖慢。這時候就需要 connection pool。

SQLAlchemy 的 create_engine 提供三個關鍵參數:`pool_size` 是常駐連線數量(預設 5)、`max_overflow` 是尖峰時可以額外開的連線數(預設 10)、`pool_pre_png=True` 會在借出連線前先發一張 SELECT 1 確認連線還活著。對後端 API 來說,pool_size 建議設成 CPU 核心數的 2 倍到 4 倍,max_overflow 設成 pool_size 的 1 倍到 2 倍,避免尖峰時把資料庫打爆。

正式環境的 Postgres 通常還會在前端放 PgBouncer 做 connection multiplexer,但那是後續章節的議題。今天先用 SQLAlchemy 內建的 pool 把單機 API 撐到每秒數百個請求。

from collections.abc import Iterator

from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session, sessionmaker


def make_engine(database_url: str, pool_size: int = 5, max_overflow: int = 10) -> Engine:
    """建立 SQLAlchemy engine,設定連線池並啟用 pool_pre_ping。"""
    engine = create_engine(
        database_url,
        pool_size=pool_size,
        max_overflow=max_overflow,
        pool_pre_ping=True,
        pool_recycle=1800,
        future=True,
    )
    return engine


def make_session_factory(engine: Engine) -> sessionmaker[Session]:
    return sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)


def get_session(factory: sessionmaker[Session]) -> Iterator[Session]:
    """FastAPI 相依性注入用的 generator。"""
    session = factory()
    try:
        yield session
    finally:
        session.close()


if __name__ == "__main__":
    import os
    url = os.environ["DATABASE_URL"]
    engine = make_engine(url, pool_size=int(os.getenv("POOL_SIZE", "5")),
                          max_overflow=int(os.getenv("MAX_OVERFLOW", "10")))
    with engine.connect() as conn:
        result = conn.execute(text("SELECT version()"))
        print(f"Postgres 版本:{result.scalar_one()}")

這份程式把 engine / session factory / dependency 三件事拆開,方便 FastAPI 在 lifespan 啟動時建好一次,shutdown 時再優雅關閉。`pool_recycle=1800` 是讓連線每 30 分鐘自動重來,避免被中介的 load balancer 或防火牆因為閒置太久而踢掉。`pool_pre_ping=True` 雖然會在每次借連線時多打一張 SELECT 1,但能擋下「連線看似活著實際已死」的問題,對正式環境非常值得。

用容器自動跑 Alembic 遷移

Day 9 我們學過 Alembic 用來管理 schema 變更。今天要把它接進 docker-compose,讓容器啟動時自動跑最新遷移。最簡單的做法是用 entrypoint script 取代 Dockerfile 的 CMD,把「等資料庫就緒 → 跑遷移 → 啟動 API」三件事串起來。

entrypoint.sh 放在專案根目錄,跟 Dockerfile 同層。腳本一開始會用 pg_isready 或 Python 連線重試,等到 Postgres 接受連線才繼續。這個等待機制比 depends_on 的 service_healthy 更穩,因為容器啟動的瞬間還是有可能 race condition。

#!/usr/bin/env bash
# entrypoint.sh
set -euo pipefail

echo "[entrypoint] 等待 Postgres..."
python /app/scripts/wait_db.py

echo "[entrypoint] 執行 Alembic 升級..."
alembic upgrade head

echo "[entrypoint] 啟動 API..."
exec uvicorn app.main:app --host 0.0.0.0 --port 8000

這個 entrypoint 把啟動流程拆成三段:等待、遷移、啟動。第一段的 Python 連線重試比 shell 的 pg_isready 更通用,因為我們已經有 psycopg 套件可用。`set -euo pipefail` 是 bash 的安全設定:指令失敗立刻退出、未定義變數立刻失敗、pipeline 任一段失敗都算失敗,避免「看起來成功但其實半路出錯」的窘境。`exec uvicorn ...` 用 exec 取代 entrypoint 行程,這樣 uvicorn 收到的 SIGTERM 才會直接送到應用,docker compose down 時才能優雅關閉。

wait_db.py 是被 entrypoint 呼叫的獨立腳本,重試 60 秒內若 Postgres 仍未就緒就退出失敗:

"""scripts/wait_db.py:等到 Postgres 接受連線才回傳 0。"""
import os
import sys
import time

import psycopg


def main() -> int:
    url = os.environ["DATABASE_URL"]
    deadline = time.monotonic() + 60
    while not (time.monotonic() >= deadline):
        try:
            with psycopg.connect(url, connect_timeout=2) as conn:
                with conn.cursor() as cur:
                    cur.execute("SELECT 1")
            print("[wait_db] Postgres 已就緒")
            return 0
        except psycopg.OperationalError as exc:
            print(f"[wait_db] 還沒好:{exc},5 秒後重試")
            time.sleep(5)
    print("[wait_db] 等不到 Postgres,啟動失敗", file=sys.stderr)
    return 1


if __name__ == "__main__":
    sys.exit(main())

Dockerfile 的最後一行也要改:原本是 `CMD ["uvicorn", ...]`,現在改成呼叫 entrypoint script。

COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh

USER app

EXPOSE 8000

ENTRYPOINT ["/app/entrypoint.sh"]

改用 ENTRYPOINT 之後,docker run 時可以再傳額外參數覆寫 uvicorn 指令(例如開發時加 --reload)。正式環境不需要 --reload,但保留 ENTRYPOINT 結構讓開發與正式用同一份 image。

沒有 Docker 時的替代流程

如果本機沒辦法跑 Docker,可以改裝原生 PostgreSQL。macOS 用 Homebrew:`brew install postgresql@17 && brew services start postgresql@17`;Ubuntu:`sudo apt install postgresql-17`;Windows 用官方 installer。裝完之後建立對應的 role 與 database:

psql -U postgres
postgres=# CREATE USER booking WITH PASSWORD 'booking_dev_pw';
postgres=# CREATE DATABASE booking OWNER booking;
postgres=# \q

然後把 DATABASE_URL 改成 `postgresql+psycopg://booking:booking_dev_pw@127.0.0.1:5432/booking`,就可以在主機直接跑 `uvicorn app.main:app`。這個流程跟容器內的行為完全一致,只是少了隔離層;後面拿到能跑 Docker 的環境時,把 DATABASE_URL 改回 `@db:5432` 就行,psycopg 連線字串的主機名稱只是字串,沒有 magic。

驗證:建立資料表並查版本

第一次啟動 compose 後,可以從容器外驗證三件事:Postgres 是否就緒、Alembic 遷移是否成功、SQLAlchemy 是否能從 connection pool 拿到連線。底下這段 Python 程式包成一個小工具,搭配 docker compose exec api python -m scripts.check_db 就能跑。

"""scripts/check_db.py:用 SQLAlchemy 驗證 Postgres 連線與遷移狀態。"""
import os

from sqlalchemy import inspect, text

from app.db import make_engine


def main() -> None:
    url = os.environ["DATABASE_URL"]
    engine = make_engine(url, pool_size=2, max_overflow=2)

    with engine.connect() as conn:
        version = conn.execute(text("SELECT version()")).scalar_one()
        print(f"Postgres 版本:{version}")

        inspector = inspect(engine)
        schemas = inspector.get_schema_names()
        print(f"已存在的 schema:{schemas}")

        tables = inspector.get_table_names(schema="public")
        print(f"public 下的資料表:{tables}")

        if not tables:
            print("尚未跑遷移,請確認 entrypoint 是否正常")
            return

        for table in tables:
            columns = inspector.get_columns(table)
            print(f"  {table}:{len(columns)} 個欄位")


if __name__ == "__main__":
    main()

這支腳本示範了 SQLAlchemy 的兩個常用工具:`text()` 把 SQL 字串包成可執行物件、`inspect()` 用 reflection 機制查詢 schema 而不用自己寫 SELECT。`pool_size=2` 是因為腳本只會用一條連線,把多餘的常駐連線讓給 API 用。

第二支腳本示範怎麼用 connection pool 的事件監聽器,在借出與歸還連線時記錄耗時。當你懷疑某些 endpoint 變慢是因為「排隊等連線」造成時,這個工具能直接給你證據:

"""scripts/pool_inspect.py:監聽 SQLAlchemy 連線池事件,統計借出耗時。"""
import os
import time
from collections import deque

from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine
from sqlalchemy.events import event

BORROW_TIMES: deque[float] = deque(maxlen=200)


def _on_checkout(dbapi_connection, connection_record, connection_proxy):  # noqa: ANN001
    connection_record.info["checked_out_at"] = time.monotonic()


def _on_checkin(dbapi_connection, connection_record):  # noqa: ANN001
    started = connection_record.info.get("checked_out_at")
    if started is not None:
        BORROW_TIMES.append(time.monotonic() - started)


def attach_listeners(engine: Engine) -> None:
    event.listen(engine, "checkout", _on_checkout)
    event.listen(engine, "checkin", _on_checkin)


def report() -> None:
    if not BORROW_TIMES:
        print("尚未借出任何連線")
        return
    samples = list(BORROW_TIMES)
    avg = sum(samples) / len(samples)
    p95 = sorted(samples)[int(len(samples) * 0.95) - 1]
    print(f"借出連線:{len(samples)} 次,平均 {avg*1000:.1f} ms,p95 {p95*1000:.1f} ms")


if __name__ == "__main__":
    engine = create_engine(os.environ["DATABASE_URL"], pool_size=2, max_overflow=2)
    attach_listeners(engine)

    with engine.connect() as conn:
        conn.execute(text("SELECT 1"))
    report()

SQLAlchemy 的事件系統讓我們不用改業務邏輯就能觀察底層行為。`checkout` 事件在連線被借出時觸發,`checkin` 在歸還時觸發;把耗時推到 deque 是為了只保留最近 200 次樣本,避免長時間跑下來記憶體被吃光。實務上若發現 p95 借出時間超過 50ms,就代表 connection pool 太小、或者資料庫已經過載,這時候可以先調大 pool_size 觀察,再決定是否升級硬體。

第三支腳本是用 Alembic 的 Python API 在 CI 中驗證「所有 model 都已建立遷移」。這能擋下「有人改了 model 但忘了跑 alembic revision」這個常見 bug:

"""scripts/check_migrations.py:確認 SQLModel 定義與 Alembic head 一致。"""
import os
import sys

from alembic.config import Config
from alembic.script import ScriptDirectory

from app.db import make_engine
from app.models import SQLModel  # 從 app.models 引入所有 model


def get_head_revision(database_url: str) -> str | None:
    engine = make_engine(database_url, pool_size=1)
    with engine.connect() as conn:
        row = conn.exec_driver_sql("SELECT version_num FROM alembic_version").first()
    return row[0] if row else None


def get_script_head() -> str:
    cfg = Config(os.environ.get("ALEMBIC_CONFIG", "alembic.ini"))
    script = ScriptDirectory.from_config(cfg)
    return script.get_heads()[0]


if __name__ == "__main__":
    url = os.environ["DATABASE_URL"]
    db_head = get_head_revision(url)
    script_head = get_script_head()
    print(f"資料庫 head:{db_head}")
    print(f"script head:{script_head}")
    if db_head != script_head:
        print("資料庫版本與遷移檔不一致,請跑 alembic upgrade head", file=sys.stderr)
        sys.exit(1)
    print("一致")

這個腳本在 CI pipeline 裡非常有用。當工程師修改 SQLModel 卻忘了跑 `alembic revision --autogenerate`,script head 會領先資料庫版本;這支腳本會抓到不一致並讓 CI 失敗,避免把「未遷移的 schema 變更」帶到正式環境。SQLModel 在 0.0.24 之後內建 `SQLModel.metadata`,可以一次收集所有 model 的 schema 資訊,這也是為什麼 `from app.models import SQLModel` 那行能把所有 model 拉進來。

最後一支是整合測試用的 fixture,把「建立資料表 → 跑遷移 → 結束時清掉」封裝起來,方便 pytest 重複使用:

"""tests/conftest.py:建立與銷毀測試用 Postgres 結構。"""
import os

import pytest
from alembic import command
from alembic.config import Config
from sqlalchemy import create_engine, text


@pytest.fixture(scope="session")
def engine():
    url = os.environ["DATABASE_URL"]
    eng = create_engine(url, pool_size=2, max_overflow=2)
    yield eng
    eng.dispose()


@pytest.fixture(scope="session", autouse=True)
def _migrate(engine):
    cfg = Config(os.environ.get("ALEMBIC_CONFIG", "alembic.ini"))
    cfg.set_main_option("sqlalchemy.url", str(engine.url))
    command.upgrade(cfg, "head")
    yield
    with engine.begin() as conn:
        conn.execute(text("DROP SCHEMA public CASCADE"))
        conn.execute(text("CREATE SCHEMA public"))

這份 fixture 是 Day 17 測試資料管理那一篇的進階版:`_migrate` 用 `autouse=True` 自動跑遷移,並在 session 結束時把整個 public schema 清掉。對正式環境的 Postgres 不能這樣做(會把資料刪光),但測試環境我們用獨立的 database 名稱(例如 booking_test),這樣 `DROP SCHEMA public` 只會清掉測試資料,不會影響開發用的資料庫。

常見錯誤與踩雷

DATABASE_URL 寫成 postgres:// 而不是 postgresql+psycopg://。SQLAlchemy 2.0 對 driver 標註非常嚴格,必須寫 `postgresql+psycopg://` 才能用 psycopg 3(2025 年 7 月主流的 driver)。只寫 `postgresql://` 會被當成 psycopg2,而你的環境裝的是 psycopg 3,會出現 `No module named 'psycopg2'` 的錯誤。

depends_on 只寫 service 啟動,不等健康。`depends_on` 預設只等 container 啟動到 running 狀態,不等服務本身可用。Postgres 容器啟動到「可以接受連線」中間還有約 5 到 10 秒的初始化時間,API 沒處理好就會爆 OperationalError。記得用 `depends_on: db: { condition: service_healthy }` 配上 db 的 healthcheck,這也是為什麼今天的 compose 兩個服務都有 healthcheck。

pool_size 設太大打爆 Postgres。Postgres 預設 max_connections 是 100。如果 API 開 5 個 replica,每個 pool_size=20,總共就會吃 100 條常駐連線,剩 0 條給 admin 與其他服務。Pool size 應該用「預期並發數」除以「每個請求平均使用連線時間」推估,而不是無腦調大。

忘記在容器外暴露 5432 port。如果 db 服務沒有 `ports: 5432:5432`,本機裝的 pgAdmin 就連不進去。要記得正式環境不對外暴露 db port,只在 compose 內部服務互通;只有開發或除錯才需要 ports。

Alembic 跑遷移時沒有 transaction 隔離。多人協作時如果兩個工程師同時下 `alembic upgrade head`,可能會出現「半套遷移」狀態。Alembic 內建 advisory lock 機制會自動避免這個問題,但升級到 Alembic 1.16 之後是用 pg_advisory_xact_lock 實作,需要 db 帳號有 lock 權限。如果看到 `permission denied for function pg_advisory_xact_lock`,請確認 role 是 database owner。

效能與實務提醒

PostgreSQL 的效能調校不是這系列的主軸,但有兩個簡單的習慣值得早點建立:第一,所有時間欄位都存 UTC,不要存當地時間;顯示時再依使用者時區轉換,否則日光節約時間會讓你懷疑人生。Postgres 的 timestamp with time zone 內部一律用 UTC,只在輸出時依 session 的 timezone 轉換,所以只要專案統一用 UTC 存,跨時區的報表對齊就不會出錯。第二,金額欄位用 `numeric(12, 2)` 而非 `float`,避免浮點數誤差;後面專案篇如果要做付費功能,會回頭來用這個觀念。

連線池方面,pool_pre_ping 的成本是每次借連線多打一張 SELECT 1,在高並發 API 下可能佔用 5% 到 10% 的查詢時間。對穩定的內部網路可以關掉以換取效能,但對跨機房或雲端環境建議保持開啟,因為 load balancer 或 NAT 容易在背景默默關閉閒置連線。如果連線死掉的頻率真的很高,可以把 pool_recycle 調小(例如從 1800 降到 600),讓連線主動 recycle 而不是被動等待 ping 才發現問題。

最後,Postgres 容器啟動時間雖然短(2 到 3 秒),但第一次執行 initdb 與 CREATE EXTENSION 可能要 15 秒以上。entrypoint 的 60 秒等待窗口足夠,但 CI 上要記得給 Postgres 容器更多 start_period(建議 30 秒以上),否則 healthcheck 還沒通過就會被 docker compose 判定失敗。另一個 CI 加速技巧是用 Postgres 的 `pg_basebackup` 預先做好的模板備份還原,這在大型遷移測試(例如有幾萬筆種子資料)能把 setup 時間從 30 秒壓到 3 秒內。

資料庫備份與監控也是正式環境必備的功課,但今天先以「把服務跑起來」為主軸。Day 34 會專門談健康檢查與監控、Day 42 會講監控日誌與備份的整合。今天先確認你的 docker compose 可以用 curl 打 /healthz、SQLAlchemy 能從 connection pool 借到連線、Alembic 遷移能順利跑完,這三件事都通過就算完成今天的進度。

小結

今天我們把 SQLite 換成 PostgreSQL 17,用 docker-compose 同時管理 API 與資料庫兩個服務,並把 Alembic 遷移接進 entrypoint。重點觀念包括:為什麼正式環境不用 SQLite、connection pool 的 pool_size / max_overflow / pool_pre_ping 怎麼設定、以及 depends_on + healthcheck 怎麼確保啟動順序。我們也用幾支小腳本示範了事件監聽、Alembic 版本驗證、整合測試 fixture 等延伸工具,這些在正式環境會越來越常用。文末也提供了不安裝 Docker 時的原生 PostgreSQL 替代流程,確保沒有 Docker 的讀者也能跟上進度。明天我們會進入 CI/CD 章節,用 GitHub Actions 自動跑測試與建置 image。

結語

PostgreSQL 是後端系統最常見的資料庫選項,今天把它裝進容器、接好連線池、串好遷移,這個組合可以撐住大多數中型應用的流量。明天,我們會用 GitHub Actions 把測試與 image 建置自動化,讓 push 程式碼時 CI 自動幫我們跑完一輪品質檢查,確保每一次合併到 main 的程式碼都已經通過基本驗證。

延伸資源

  • PostgreSQL 17 官方文件:https://www.postgresql.org/docs/17/
  • SQLAlchemy 2.0 connection pool 指南:https://docs.sqlalchemy.org/en/20/core/pooling.html
  • psycopg 3 文件:https://www.psycopg.org/psycopg3/docs/
  • Alembic 1.16 操作指南:https://alembic.sqlalchemy.org/en/latest/cookbook.html
  • Docker Compose healthcheck 設定說明:https://docs.docker.com/reference/compose-file/services/

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...