Web Day 5 請求驗證與相依性注入
執行需求:CPU 可跑。昨天的書本 API 雖然能跑,但只是「能驗證」而已。今天要把請求驗證做到生產等級:用 Pydantic v2 的自訂驗證器處理跨欄位約束、用 Annotated 與 Depends 把常用邏輯抽出來做成「相依性」,並用 yield 處理資料庫連線、token 驗證這類「用完要清掉」的資源。學完這篇,你會理解為什麼 FastAPI 工程師常說「Depends 是這個框架最被低估的功能」。
引言
寫過幾支 CRUD API 之後,你會發現很多邏輯是重複的:分頁參數(skip、limit)、排序參數(sort_by、order)、認證(從標頭拿 token、解碼、查使用者)、資料庫連線(開 connection、用完關掉)。如果每個端點都自己寫一份,很快就會變成「二十個端點各有自己的分頁寫法」這種維運噩夢。FastAPI 的相依性注入(Dependency Injection,DI)就是為了解決這個問題而設計的:把邏輯包成可重複使用的「相依性」,由框架自動注入到需要的端點。
同時,僅靠 Pydantic 的欄位層級驗證還不夠。實務上常見「兩個欄位要互相約束」的需求,例如「結束時間必須晚於開始時間」、「密碼與確認密碼要一致」、「建立訂單時數量必須大於零且不能超過庫存」。這類跨欄位邏輯必須寫在 Pydantic 模型裡(這樣驗證錯誤才會統一回 422),而不是在端點函式裡手寫。今天會帶你用 Pydantic v2 的 model_validator 處理這類情境。
這篇文章會做四件事:第一,介紹 FastAPI 的請求驗證全貌(路徑、查詢、標頭、Cookie、Body);第二,示範 Pydantic v2 的自訂驗證器;第三,深入相依性注入的設計哲學與 Depends 寫法;第四,建立一個「分頁 + 排序 + 過濾 + 共用驗證」的書本 API,把今天所有東西串起來。
FastAPI 的請求驗證全貌
FastAPI 把 HTTP 請求的不同部位對應到 Python 函式的參數,這個機制在 Day 4 已經介紹過。今天把整個家族一次攤開來看:
| 來源 | 注入方式 | 常用場景 |
|---|---|---|
| 路徑參數 | item_id: int |
資源 ID、必填 |
| 查詢參數 | q: Annotated[str | None, Query(...)] = None |
篩選、分頁、排序 |
| 標頭 | x_token: Annotated[str, Header()] |
認證 token、API key |
| Cookie | session: Annotated[str, Cookie()] |
瀏覽器 session |
| 請求主體 | payload: ItemCreate |
建立、整批更新 |
| 表單 | Form() |
檔案上傳、傳統表單 |
| 檔案 | File() |
上傳檔案(Day 14) |
所有這些注入都支援相同的驗證機制(Query、Header、Cookie、Body 都有 min_length、ge、pattern 等參數)。實務上建議統一用 Annotated[type, Validation()] 的寫法,不要把預設值與驗證擠在同一個參數裡,這樣規格書最清楚。
標頭驗證的小細節:HTTP 標頭的大小寫不敏感(X-Token 等於 x-token),但 FastAPI 預設會把 Python 函式參數名稱 x_token 自動轉成 x-token 來比對。如果你不想用這種轉換,可以用 Header(alias="X-API-Key") 明確指定。
Pydantic v2 自訂驗證器
Pydantic 提供兩種驗證器:欄位層級(field validator)與模型層級(model validator)。欄位層級驗證單一欄位的內容,模型層級驗證跨欄位的關係:
# Pydantic v2 的驗證器範例
from datetime import date
from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
class Reservation(BaseModel):
"""預約資料模型:跨欄位驗證(結束日期必須晚於開始日期)"""
customer_name: str = Field(min_length=1, max_length=80)
customer_email: EmailStr
start_date: date
end_date: date
guests: int = Field(ge=1, le=20)
@field_validator("customer_name")
@classmethod
def name_must_not_be_blank(cls, v: str) -> str:
# 欄位層級:去除頭尾空白,空白字串視為無效
v = v.strip()
if not v:
raise ValueError("姓名不可為空白")
return v
@model_validator(mode="after")
def check_dates(self):
# 模型層級:檢查 end_date 必須晚於 start_date
if self.end_date <= self.start_date:
raise ValueError("結束日期必須晚於開始日期")
return self
# 合法
r = Reservation(
customer_name="王小明",
customer_email="wang@example.com",
start_date=date(2025, 8, 1),
end_date=date(2025, 8, 3),
guests=2,
)
print(f"{r.customer_name}:{r.start_date} 到 {r.end_date},{r.guests} 位")
# 輸出:王小明:2025-08-01 到 2025-08-03,2 位
# 跨欄位錯誤
try:
Reservation(
customer_name="王小明",
customer_email="wang@example.com",
start_date=date(2025, 8, 3),
end_date=date(2025, 8, 1), # 早於 start_date
guests=2,
)
except Exception as e:
print("驗證失敗:", type(e).__name__)
# 輸出:驗證失敗: ValidationError
幾個關鍵寫法:@field_validator 必須加 @classmethod,這是 Pydantic v2 的新規範;@model_validator(mode="after") 在所有欄位驗證完成後執行,適合做跨欄位檢查;mode="before" 則會在欄位驗證之前執行,適合做「把字串轉成日期」這種預處理。實務上 mode="after" 是最常用的模式。
另一個常見需求是「資料清洗」。例如使用者輸入的電話號碼可能有空格、破折號、國際碼,可以用 field_validator 在驗證階段就清洗乾淨:
import re
from pydantic import BaseModel, Field
class OrderCreate(BaseModel):
customer_name: str = Field(min_length=1, max_length=80)
phone: str = Field(min_length=8, max_length=20)
@field_validator("phone")
@classmethod
def normalize_phone(cls, v: str) -> str:
# 去掉所有非數字字元;保留國際碼前綴(+)
cleaned = re.sub(r"[^\d+]", "", v)
if not cleaned:
raise ValueError("電話號碼不可為空")
return cleaned
# 輸入:手機格式不一
o = OrderCreate(customer_name="測試", phone="0912-345-678")
print(f"清洗後:{o.phone}")
# 輸出:清洗後:0912345678
把清洗邏輯寫在模型裡有兩個好處:第一,所有端點收到的資料都會被同樣清洗,避免某個端點忘了清洗導致資料不一致;第二,清洗後的資料會反映在 model_dump() 的輸出,方便日誌與除錯。
相依性注入:把常用邏輯抽成可重用元件
相依性注入(Dependency Injection,DI)聽起來很抽象,其實概念很簡單:把「端點函式需要用到的資源或邏輯」抽出來,讓 FastAPI 在呼叫端點之前先幫你準備好。準備的方式是定義一個函式(稱為相依性),用 Depends() 註記在端點參數上。
先看一個簡單的例子:
# 相依性注入的最小範例
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def common_params(
q: Annotated[str | None, "查詢字串"] = None,
skip: Annotated[int, "略過筆數"] = 0,
limit: Annotated[int, "取幾筆"] = 10,
):
# 這是「相依性函式」:回傳一個 dict
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items")
def list_items(params: Annotated[dict, Depends(common_params)]):
# Depends 會自動呼叫 common_params 並把結果注入這裡
return params
# 呼叫:GET /items?q=hello&skip=0&limit=10
# FastAPI 會自動呼叫 common_params(q="hello", skip=0, limit=10)
# 把結果 {"q": "hello", "skip": 0, "limit": 10} 注入 list_items
這個例子看起來簡單到沒什麼,但「把所有端點都要用的參數」抽成相依性,正是大型後端專案的基礎。實務上你可以把分頁、排序、過濾、共用驗證、認證、資料庫連線都做成相依性,讓端點函式專注在「拿到資料後要做什麼」。
FastAPI 0.95+ 推薦用 Annotated[type, Depends(fn)] 寫相依性,比 name: type = Depends(fn) 更清楚型別意圖。本系列從這篇開始所有相依性都用 Annotated 寫法。
用 yield 處理資源生命週期
某些資源「用完要清掉」:資料庫連線(要 close)、檔案(要關閉)、交易(要 commit 或 rollback)、token 驗證(要記錄)。FastAPI 的相依性支援 yield 語法,把「建立」與「清理」寫在同一個函式裡:
# 用 yield 寫資料庫連線相依性(Day 6 會接 SQLite,這裡先用假連線示範語法)
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
class FakeDB:
"""假資料庫連線:只是為了示範 yield 的生命週期"""
def __init__(self):
print("開啟連線") # 模擬開啟資源
def query(self, sql: str) -> list[dict]:
return [{"id": 1, "name": "fake"}]
def close(self):
print("關閉連線") # 模擬釋放資源
def get_db():
# yield 之前的程式碼在端點「之前」執行
db = FakeDB()
try:
yield db # 把資源交給端點
finally:
# yield 之後的程式碼在端點「之後」執行(即使端點出錯也會跑)
db.close()
@app.get("/items")
def list_items(db: Annotated[FakeDB, Depends(get_db)]):
return db.query("SELECT * FROM items")
# 呼叫:GET /items
# 印出順序:開啟連線 -> ... -> 關閉連線
關鍵設計:yield 把資源交給端點,端點函式執行完(不論成功或失敗)後,框架會自動執行 yield 後面的清理區塊。這保證資源一定會被釋放,避免連線洩漏(connection leak)。如果你的端點函式拋出例外,finally 還是會跑——這對資料庫交易特別重要(先 rollback 再 close)。
另一個常見的 yield 應用是「依賴鏈(dependency chain)」:一個相依性可以呼叫另一個相依性,FastAPI 會依照呼叫順序建立、反向拆除:
# 依賴鏈範例
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def query_token(token: Annotated[str | None, "查詢參數 token"] = None):
return token or "anonymous"
def require_user(token: Annotated[str, Depends(query_token)]):
# 這個相依性依賴另一個
if token == "anonymous":
raise ValueError("需要登入")
return {"user": f"user-{token}"}
@app.get("/profile")
def profile(user: Annotated[dict, Depends(require_user)]):
return user
FastAPI 在處理 /profile 時會自動先呼叫 query_token,把結果注入 require_user,再把 require_user 的結果注入 profile。這個鏈可以無限延伸,Day 12 介紹 JWT 認證時你會看到一個完整的應用範例。
完整實作:分頁 + 排序 + 過濾 + 共用驗證
把今天所有東西一次做完。我們擴充昨天的書本 API,加入:
- 分頁與排序做成共用相依性
- 作者與年份過濾
- 書本建立時的跨欄位驗證(年份範圍)
- 用
yield模擬資料庫連線
建立 src/book_api/main.py:
# src/book_api/main.py
# 書本管理 API(含分頁、排序、過濾、相依性注入)
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Query
from pydantic import BaseModel, ConfigDict, Field, model_validator
app = FastAPI(
title="書本管理 API",
description="Day 5:分頁、排序、過濾、相依性注入",
version="0.2.0",
)
# ---------- 資料模型 ----------
class BookBase(BaseModel):
title: str = Field(min_length=1, max_length=200)
author: str = Field(min_length=1, max_length=80)
year: int = Field(ge=0, le=2100)
class BookCreate(BookBase):
@model_validator(mode="after")
def check_year_reasonable(self):
# 跨欄位:年份不能超過「現在 + 1」(不允許預言書)
import datetime
next_year = datetime.date.today().year + 1
if self.year > next_year:
raise ValueError(f"年份不可超過 {next_year}")
return self
class Book(BookBase):
id: int
model_config = ConfigDict(from_attributes=True)
# ---------- 共用相依性 ----------
class Pagination:
"""分頁參數的容器"""
def __init__(
self,
skip: Annotated[int, Query(ge=0, description="略過筆數")] = 0,
limit: Annotated[int, Query(ge=1, le=100, description="取幾筆")] = 20,
):
self.skip = skip
self.limit = limit
class Sorting:
"""排序參數的容器"""
def __init__(
self,
sort_by: Annotated[str, Query(pattern="^(id|title|author|year)$")] = "id",
order: Annotated[str, Query(pattern="^(asc|desc)$")] = "asc",
):
self.sort_by = sort_by
self.order = order
# 給 endpoint 用的型別別名
PaginationDep = Annotated[Pagination, Depends()]
SortingDep = Annotated[Sorting, Depends()]
# ---------- 假資料庫(用 yield 模擬連線) ----------
class FakeDB:
def __init__(self):
self._books: dict[int, Book] = {
1: Book(id=1, title="Python 程式設計實戰", author="Hao", year=2024),
2: Book(id=2, title="FastAPI 入門", author="Wei", year=2025),
3: Book(id=3, title="資料庫設計", author="Hao", year=2023),
4: Book(id=4, title="Clean Code", author="Robert", year=2008),
}
def list(self) -> list[Book]:
return list(self._books.values())
def get(self, book_id: int) -> Book | None:
return self._books.get(book_id)
def add(self, book: Book) -> Book:
self._books[book.id] = book
return book
def close(self):
pass
def get_db():
db = FakeDB()
try:
yield db
finally:
db.close()
DBDep = Annotated[FakeDB, Depends(get_db)]
# ---------- 端點 ----------
@app.get("/books", response_model=list[Book], summary="列出書本")
def list_books(
db: DBDep,
pagination: PaginationDep,
sorting: SortingDep,
author: Annotated[str | None, Query(description="依作者過濾")] = None,
year: Annotated[int | None, Query(ge=0, le=2100)] = None,
):
items = db.list()
if author is not None:
items = [b for b in items if b.author == author]
if year is not None:
items = [b for b in items if b.year == year]
items.sort(key=lambda b: getattr(b, sorting.sort_by), reverse=(sorting.order == "desc"))
return items[pagination.skip : pagination.skip + pagination.limit]
@app.get("/books/{book_id}", response_model=Book)
def get_book(db: DBDep, book_id: int):
book = db.get(book_id)
if book is None:
raise HTTPException(status_code=404, detail="book not found")
return book
@app.post("/books", response_model=Book, status_code=201)
def create_book(db: DBDep, payload: BookCreate):
next_id = max(db._books.keys(), default=0) + 1
new = Book(id=next_id, **payload.model_dump())
return db.add(new)
# 啟動:uvicorn book_api.main:app --reload --port 8000
這個版本做了幾個重要的設計選擇:
Pagination與Sorting用 class 封裝參數,端點函式拿到的是物件,可以直接存取pagination.skip。- 用型別別名
PaginationDep、SortingDep、DBDep簡化簽章,避免每個端點都寫Annotated[X, Depends()]。 BookCreate的model_validator在年份超過「明年」時拒絕(避免有人上傳「2026 年的書」這種測試資料)。get_db用yield寫連線生命週期,端點用完自動呼叫close()。
用 httpx 測試分頁與排序:
# tests/test_pagination.py
import httpx
BASE = "http://127.0.0.1:8000"
def main():
with httpx.Client(base_url=BASE, timeout=5.0) as client:
# 1. 分頁:取第 2 頁(skip=2, limit=2)
r = client.get("/books", params={"skip": 2, "limit": 2})
print("GET /books?skip=2&limit=2 ->", r.status_code, r.json())
# 輸出(範例):[{...id 3}, {...id 4}]
# 2. 排序:依年份降序
r = client.get("/books", params={"sort_by": "year", "order": "desc"})
print("GET /books?sort_by=year&order=desc ->", r.status_code, [b["year"] for b in r.json()])
# 輸出(範例):[2025, 2024, 2023, 2008]
# 3. 過濾:作者是 Hao
r = client.get("/books", params={"author": "Hao"})
print("GET /books?author=Hao ->", r.status_code, r.json())
# 輸出(節錄):[{...id 1, author 'Hao'}, {...id 3, author 'Hao'}]
# 4. 錯誤:排序欄位非法
r = client.get("/books", params={"sort_by": "evil"})
print("GET /books?sort_by=evil ->", r.status_code, r.json())
# 輸出(節錄):422 - pattern 不符合
# 5. 錯誤:建立超未來的書
r = client.post(
"/books",
json={"title": "未來的書", "author": "Hao", "year": 9999},
)
print("POST /books (year=9999) ->", r.status_code, r.json())
# 輸出(節錄):422 - 年份範圍錯誤
main()
這個迴圈把分頁、排序、過濾、錯誤驗證一次展示。實務上你會把這些呼叫包成 pytest 測試(Day 16 會示範 TestClient)。今天重點在於「驗證邏輯寫對地方」——所有 422 都來自 Pydantic 或 FastAPI 的自動驗證,沒有任何一行手寫的 if 判斷。
常見錯誤與踩雷
第一個常見的踩雷是「忘了寫 @classmethod」。Pydantic v2 的 field_validator 必須搭配 @classmethod,否則會在執行時報錯(TypeError)。這是從 Pydantic v1 升級上來的人最常見的錯誤,看到驗證器沒生效就檢查這裡。
第二個是「Depends 用 class 而不用 function」。FastAPI 允許 Depends(SomeClass),這會把 SomeClass() 當作相依性。這對「共享狀態」很方便,但要注意:每個請求都會建立新的 instance(FastAPI 的相依性預設是 per-request),如果你的 class 裡有昂貴的初始化,要改成模組層級的 singleton(Day 22 會討論快取)。
第三個是「yield 後的例外被吞掉」。如果在 yield 之後的清理區塊(finally 內)發生例外,那個例外會覆蓋原本的端點例外,導致除錯困難。實務上 close() 等清理動作應該設計成「不會失敗」,必要時把例外記錄到日誌就好,不要讓它傳出去。
效能與實務提醒
相依性注入的代價:每個請求都會額外呼叫相依性函式。在「Pagination()」這種輕量物件上成本幾乎為零;但如果相依性要做資料庫連線、外部 API 呼叫、讀取設定檔,就要考慮共用。FastAPI 提供 Depends(get_settings, use_cache=True) 機制(0.117 之後更明確),可以把結果快取在同一次請求裡;模組層層級的 singleton(直接在模組裡 settings = load_settings())也是常見做法。
Pydantic 驗證效能很好(v2 用 Rust 核心),但巢狀過深的模型(例如 5 層巢狀)在大量請求下會成為瓶頸。如果你發現 API 在驗證階段花太多時間,可以:用 model_construct() 跳過驗證(只在信任內部資料時用)、把大型模型拆成幾個小模型、或考慮 Pydantic 的 TypeAdapter 做更精細的控制。本系列規模不大,通常不會碰到這個瓶頸。
排序與過濾目前是在 Python 裡做的(db.list() 後用 list comprehension 過濾)。Day 6 換成 SQLite 之後,這些動作會下沉到資料庫層,可以用 WHERE 與 ORDER BY 子句做,效能會比記憶體內過濾好很多。今天先用記憶體版專注在「驗證與 DI 的觀念」,效能留給後面。
小結
今天把 FastAPI 的兩個進階主題一次講完:請求驗證的進階用法(Pydantic v2 的 field_validator、model_validator、自訂清洗邏輯)與相依性注入(Depends、Annotated、yield 處理資源生命週期、依賴鏈)。我們用一個擴充版的書本 API 把這些東西組起來:分頁與排序抽成共用相依性、建立時的跨欄位驗證、yield 模擬資料庫連線。這套結構在後續每天的範例都會沿用,是整個系列最常被用到的子集合之一。
進階主題:全域相依性與例外處理
前面介紹的 Depends 是「端點等級」的相依性,但有些邏輯是「全應用等級」的:例如「每個請求都要記錄開始時間以便計算耗時」、「每個請求都要驗證 API key」、「每個請求發生例外都要回傳統一格式」。這時用 FastAPI 的「全域相依性」與「例外處理器」就對了。
# global_deps.py
# 全域相依性 + 例外處理器範例
from datetime import datetime, timezone
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Request
app = FastAPI(title="全域相依性示範")
# 全域相依性:計算每個請求的耗時
async def log_request_time(request: Request):
request.state.start_time = datetime.now(timezone.utc)
# 全域相依性:簡單的 API key 驗證
async def verify_api_key(request: Request):
api_key = request.headers.get("X-API-Key")
if api_key != "demo-key":
raise HTTPException(status_code=401, detail="invalid API key")
# 把全域相依性掛到 app 上
app.include_router(
# 為了簡化,直接在 FastAPI 實例上掛 middleware
# 實際專案會用 app.middleware("http") 或 router
)
# 例外處理器:把所有 HTTPException 統一格式
from fastapi.responses import JSONResponse
@app.exception_handler(HTTPException)
async def custom_http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.status_code,
"message": exc.detail,
"path": str(request.url.path),
}
},
)
@app.get("/hello")
async def hello():
return {"message": "Hello, FastAPI!"}
這段範例展示三個進階機制。第一,request.state.start_time 是 FastAPI 提供的「每個請求私有空間」,可以在全域相依性裡塞值,在端點函式裡讀出來——這對「計算耗時」、「記錄使用者資訊」非常有用。第二,@app.exception_handler 讓你把某一類例外統一包裝成想要的回應格式,這對「錯誤回應結構」的一致性非常關鍵。第三個 verify_api_key 雖然簡單,但展示了「全域驗證」的設計——後續 Day 12 介紹 JWT 認證時,會用同樣的機制檢查 token。
第二個進階主題:用「依賴覆寫(dependency override)」做測試時的取代。這個機制讓你在測試時把某個相依性換成假的,例如把「連到正式資料庫」換成「連到測試資料庫」、把「送 email」換成「記錄到記憶體」。
# test_overrides.py
# 用 FastAPI 的 dependency_overrides 做測試時的取代
import os
from typing import Generator
import httpx
from fastapi.testclient import TestClient
from main import app, get_session
# 1. 寫一個假的 get_session
def fake_get_session() -> Generator[dict, None, None]:
"""測試用的假 session:直接回傳假資料"""
yield {
"items": [
{"id": 1, "title": "測試書", "author": "Test", "year": 2024}
]
}
# 2. 註冊 override
app.dependency_overrides[get_session] = fake_get_session
# 3. 用 TestClient 測試
client = TestClient(app)
r = client.get("/books")
print(r.status_code, r.json())
# 輸出(範例):200 [{'id': 1, 'title': '測試書', 'author': 'Test', 'year': 2024}]
dependency_overrides 是 FastAPI 對「測試替身(test double)」的內建支援。它的好處是你不用為了測試去改任何端點函式,只要在測試開頭 app.dependency_overrides[xxx] = fake_xxx 就能把相依性換成假貨。這在 Day 16、Day 17 介紹 pytest 與 fixture 時會大量使用,今天先記得這個機制的存在。
第三個進階主題:用 Depends(use_cache=False) 強制每次呼叫。預設情況下,同一個端點的同一個相依性只會被執行一次(結果會被重用)。但在某些情境你需要「每次都重新執行」,例如「每個請求都讀取最新的設定檔」:
# non_cached_dep.py
# 強制每次都重新執行的相依性
from typing import Annotated
import os
import time
from fastapi import Depends, FastAPI
app = FastAPI()
def get_config(force_reload: Annotated[bool, "強制重新讀取"] = False):
"""讀取設定檔;force_reload=True 時重新讀一次"""
if force_reload:
time.sleep(0.1) # 模擬讀檔耗時
return {"loaded_at": time.time(), "env": os.getenv("ENV", "dev")}
# 一般呼叫:會被快取(同一請求內只執行一次)
@app.get("/normal")
def normal(config: Annotated[dict, Depends(get_config)]):
return config
# 強制重新讀取:每次都執行
@app.get("/fresh")
def fresh(config: Annotated[dict, Depends(get_config, use_cache=False)]):
# FastAPI 0.116+ 支援 use_cache=False
return config
這個 use_cache=False 在 FastAPI 0.115+ 開始正式支援(本系列固定在 0.116,完整支援)。它的語意是「這個相依性在同一次請求內不要重用快取,每次呼叫都重新執行」。這個設計在「讀取設定檔、檢查時間戳記、生成 UUID」這類「不該被快取」的情境特別有用。
到這裡我們已經把請求驗證與相依性注入的所有進階主題走完。今天學到的東西涵蓋了:欄位驗證、跨欄位驗證、自訂清洗、共用相依性、yield 資源管理、依賴鏈、全域相依性、例外處理、測試覆寫、use_cache 控制。這是 FastAPI 框架裡最精華的部分之一,後續的認證、測試、資料庫章節都會建立在這個基礎上。
結語
今天把驗證與 DI 都講完了,但資料還存在記憶體裡。明天,我們要進入「資料持久化」的主題:SQLModel 與 SQLite。你會學到 ORM 的核心觀念、用 SQLModel 定義資料模型、用 SQLite 做本機儲存、以及怎麼把今天的 FakeDB 換成真正的資料庫。一旦資料可以持久化,整個 API 才真的開始像「系統」,而不只是「腳本」。
延伸資源
- FastAPI 相依性教學(2025):
https://fastapi.tiangolo.com/tutorial/dependencies/ - Pydantic v2 Validators(2025):
https://docs.pydantic.dev/latest/concepts/validators/ - PEP 593 - Annotated(2019):
https://peps.python.org/pep-0593/ - FastAPI 進階 DI:全域相依性(2025):
https://fastapi.tiangolo.com/tutorial/dependencies/global-dependencies/ - Real Python:FastAPI Dependencies(2025):
https://realpython.com/fastapi-dependencies-injection/ - Python Type Hints(PEP 484,2014):
https://peps.python.org/pep-0484/
留言
張貼留言