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/
留言
張貼留言