Python?FastAPI連接操作MySQL數(shù)據(jù)庫指南
寫 FastAPI 后端時(shí),數(shù)據(jù)庫連接不是只寫一行連接串就結(jié)束了。
一個(gè)能長期維護(hù)的 MySQL 接入方式,通常要同時(shí)解決這些問題:
- Python 用什么驅(qū)動(dòng)真正連上 MySQL
- ORM 用什么方式把 Python 類映射成數(shù)據(jù)庫表
- 每個(gè) HTTP 請(qǐng)求如何拿到獨(dú)立的數(shù)據(jù)庫會(huì)話
- 寫操作如何提交事務(wù),失敗時(shí)如何回滾
- 項(xiàng)目變大后,數(shù)據(jù)庫連接、模型、路由、業(yè)務(wù)邏輯分別放在哪里
先看完整流程。

這張圖可以拆成兩條主線:
應(yīng)用啟動(dòng)
-> 讀取 DATABASE_URL
-> 創(chuàng)建 Engine
-> 創(chuàng)建 SessionLocal
-> 可選:創(chuàng)建表、寫入初始化數(shù)據(jù)
接口請(qǐng)求
-> FastAPI Router 接收請(qǐng)求
-> Depends 注入數(shù)據(jù)庫 Session
-> Service 處理業(yè)務(wù)規(guī)則
-> Repository / ORM 執(zhí)行數(shù)據(jù)庫讀寫
-> commit 或 rollback
-> 請(qǐng)求結(jié)束后關(guān)閉 Session
FastAPI 不直接負(fù)責(zé)連接 MySQL。它負(fù)責(zé) HTTP、參數(shù)校驗(yàn)、依賴注入和響應(yīng)返回。
真正跟 MySQL 打交道的是:
FastAPI
-> SQLAlchemy
-> PyMySQL
-> MySQL
一、先把它們之間的配合講清楚
如果只記一個(gè)關(guān)系,就記這條鏈路:
用戶發(fā)起 HTTP 請(qǐng)求
-> FastAPI 找到對(duì)應(yīng)路由函數(shù)
-> Depends 調(diào)用 get_db 創(chuàng)建一個(gè) Session
-> 路由函數(shù)把 Session 交給 Service
-> Service 處理業(yè)務(wù)規(guī)則和事務(wù)
-> Repository 使用 Session 寫 SQLAlchemy 查詢
-> SQLAlchemy 把 ORM 查詢翻譯成 SQL
-> PyMySQL 把 SQL 發(fā)給 MySQL
-> MySQL 執(zhí)行 SQL 并返回結(jié)果
-> SQLAlchemy 把結(jié)果轉(zhuǎn)換成 Python 對(duì)象
-> FastAPI 把響應(yīng)對(duì)象轉(zhuǎn)換成 JSON 返回給前端
這套關(guān)系里,每一層都有明確分工。
FastAPI
-> 管 HTTP,不管數(shù)據(jù)庫底層連接
Depends / get_db
-> 管每個(gè)請(qǐng)求的數(shù)據(jù)庫 Session 生命周期
SQLAlchemy Engine
-> 管數(shù)據(jù)庫入口和連接池
SQLAlchemy Session
-> 管一次業(yè)務(wù)操作里的查詢、寫入、提交、回滾
SQLAlchemy ORM Model
-> 管 Python 類和數(shù)據(jù)庫表之間的映射
PyMySQL
-> 管 Python 進(jìn)程和 MySQL 服務(wù)之間的底層通信
MySQL
-> 真正存儲(chǔ)和查詢數(shù)據(jù)
二、需要哪些第三方依賴
一個(gè)同步版 FastAPI + MySQL 項(xiàng)目,最少需要這些依賴:
uv add "fastapi[standard]" sqlalchemy pymysql pydantic-settings
如果使用 pip:
pip install "fastapi[standard]" sqlalchemy pymysql pydantic-settings
這些包分別負(fù)責(zé)不同層級(jí)的事情。
1. FastAPI
FastAPI 是 Web 框架,負(fù)責(zé):
- 定義 HTTP 接口
- 解析路徑參數(shù)、查詢參數(shù)、請(qǐng)求體
- 使用 Pydantic 做數(shù)據(jù)校驗(yàn)
- 通過
Depends注入依賴 - 自動(dòng)生成 OpenAPI 文檔
這里有兩個(gè)詞先解釋清楚。
路由 指的是“某個(gè) HTTP 請(qǐng)求應(yīng)該交給哪個(gè)函數(shù)處理”。例如 GET /users/1 交給 get_user 函數(shù)處理。
依賴注入 指的是“路由函數(shù)需要什么對(duì)象,由 FastAPI 在調(diào)用函數(shù)前幫你準(zhǔn)備好”。數(shù)據(jù)庫 Session 就很適合用依賴注入,因?yàn)槊總€(gè)請(qǐng)求都需要一個(gè)獨(dú)立 Session,請(qǐng)求結(jié)束后還必須關(guān)閉。
數(shù)據(jù)庫連接進(jìn)入 FastAPI 的方式,通常不是在路由函數(shù)里手動(dòng)創(chuàng)建連接,而是通過依賴注入:
from typing import Annotated from fastapi import Depends from sqlalchemy.orm import Session # SessionDep 是一個(gè)類型別名: # 它告訴 FastAPI,只要路由參數(shù)標(biāo)注為 SessionDep, # 就先執(zhí)行 get_db(),把得到的數(shù)據(jù)庫 Session 傳進(jìn)來。 SessionDep = Annotated[Session, Depends(get_db)]
這樣路由只聲明“我需要一個(gè)數(shù)據(jù)庫 Session”,至于這個(gè) Session 怎么創(chuàng)建、怎么關(guān)閉,交給統(tǒng)一的依賴函數(shù)處理。
2. SQLAlchemy
SQLAlchemy 是數(shù)據(jù)庫工具和 ORM。
它負(fù)責(zé):
- 創(chuàng)建數(shù)據(jù)庫連接入口
Engine - 管理連接池
- 創(chuàng)建請(qǐng)求級(jí)
Session - 定義 ORM 模型
- 生成 SQL
- 執(zhí)行增刪改查
- 管理事務(wù)提交和回滾
ORM 是 Object Relational Mapping 的縮寫,中文常叫“對(duì)象關(guān)系映射”。
它解決的是這件事:
- 在 Python 代碼里面操作對(duì)象
- 在數(shù)據(jù)庫里面是操作表和行
- ORM 負(fù)責(zé)在兩者之間做轉(zhuǎn)換
例如 Python 里有一個(gè) User 類,數(shù)據(jù)庫里有一張 users 表。ORM 會(huì)把 User.username 對(duì)應(yīng)到 users.username 字段。這樣你可以寫 Python 對(duì)象和查詢表達(dá)式,而不是在業(yè)務(wù)里到處拼 SQL 字符串。
使用 SQLAlchemy 后,業(yè)務(wù)代碼通常不直接拼 SQL 字符串,而是操作 Python 類:
# select(User) 表示查詢 users 表對(duì)應(yīng)的 User 模型 # where(...) 表示追加查詢條件 # scalar(...) 表示只取一條 ORM 對(duì)象結(jié)果 user = db.scalar(select(User).where(User.username == username))
這行代碼最后會(huì)被 SQLAlchemy 轉(zhuǎn)成 SQL,交給底層數(shù)據(jù)庫驅(qū)動(dòng)執(zhí)行。
3. PyMySQL
PyMySQL 是 MySQL 的 Python 驅(qū)動(dòng)。
它是真正負(fù)責(zé)和 MySQL 服務(wù)通信的底層庫。SQLAlchemy 自己不是 MySQL 驅(qū)動(dòng),它需要通過 DBAPI 驅(qū)動(dòng)連接具體數(shù)據(jù)庫。
驅(qū)動(dòng) 可以理解成數(shù)據(jù)庫的“適配器”。不同數(shù)據(jù)庫說話方式不一樣,MySQL、PostgreSQL、SQLite 都有自己的協(xié)議和細(xì)節(jié)。SQLAlchemy 負(fù)責(zé)生成 SQL 和管理 ORM,但真正打開網(wǎng)絡(luò)連接、登錄 MySQL、發(fā)送 SQL、讀取結(jié)果,需要驅(qū)動(dòng)來做。
DBAPI 是 Python 里數(shù)據(jù)庫驅(qū)動(dòng)遵循的一套接口規(guī)范。PyMySQL 實(shí)現(xiàn)了這套規(guī)范,所以 SQLAlchemy 可以通過它連接 MySQL。
連接 MySQL 時(shí),SQLAlchemy URL 一般寫成:
mysql+pymysql://用戶名:密碼@主機(jī):端口/數(shù)據(jù)庫名?charset=utf8mb4
這里的 pymysql 就是在告訴 SQLAlchemy:
- 數(shù)據(jù)庫類型是 MySQL
- 底層驅(qū)動(dòng)使用 PyMySQL
如果 MySQL 賬號(hào)使用了需要 RSA 的認(rèn)證方式,可以安裝:
uv add "pymysql[rsa]"
或者:
pip install "PyMySQL[rsa]"
普通本地開發(fā)大多數(shù)時(shí)候只需要 pymysql。
4. pydantic-settings
pydantic-settings 用來讀取環(huán)境變量和 .env 配置。
數(shù)據(jù)庫連接串、是否打印 SQL、是否自動(dòng)建表,這些都不應(yīng)該寫死在代碼里,而應(yīng)該放到環(huán)境變量中。
典型 .env:
DATABASE_URL="mysql+pymysql://root:password@127.0.0.1:3306/todo_api?charset=utf8mb4" DATABASE_ECHO=false DATABASE_AUTO_CREATE=false
讀取配置:
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
# 告訴 pydantic-settings 從 .env 文件讀取配置
# env_file_encoding 用來避免中文或特殊字符出現(xiàn)編碼問題
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
)
# 必填配置:沒有 DATABASE_URL 時(shí),應(yīng)用應(yīng)該直接啟動(dòng)失敗
database_url: str
# 是否打印 SQL。調(diào)試時(shí)可以改成 true,正常開發(fā)一般保持 false
database_echo: bool = False
# 是否在啟動(dòng)時(shí)自動(dòng)建表。本地 demo 可以 true,正式項(xiàng)目一般 false
database_auto_create: bool = False
# 創(chuàng)建全局配置對(duì)象。其他模塊通過 settings.database_url 讀取配置
settings = Settings()
5. Alembic
Alembic 不是連接 MySQL 的必需依賴,但正式項(xiàng)目通常需要它。
它負(fù)責(zé)數(shù)據(jù)庫遷移:
uv add alembic
為什么需要遷移工具?
因?yàn)轫?xiàng)目上線后,表結(jié)構(gòu)變化不能靠 Base.metadata.create_all() 隨手創(chuàng)建。正式環(huán)境需要可追蹤、可回滾、可審查的遷移文件。
可以先記住這個(gè)區(qū)分:
本地學(xué)習(xí) / 早期 demo
-> 可以用 create_all()
團(tuán)隊(duì)項(xiàng)目 / 正式環(huán)境
-> 應(yīng)該用 Alembic 管理遷移
三、連接串怎么寫
MySQL 連接串的完整格式:
mysql+pymysql://用戶名:密碼@主機(jī):端口/數(shù)據(jù)庫名?charset=utf8mb4
示例:
DATABASE_URL="mysql+pymysql://root:password@127.0.0.1:3306/todo_api?charset=utf8mb4"
拆開看:
mysql+pymysql://root:password@127.0.0.1:3306/todo_api?charset=utf8mb4
| | | | | |
用戶名 密碼 主機(jī) 端口 數(shù)據(jù)庫名 字符集
幾個(gè)點(diǎn)要特別注意。
第一,推薦顯式寫 mysql+pymysql://,不要只寫 mysql://。
mysql+pymysql:// 的意思更明確:MySQL 方言加 PyMySQL 驅(qū)動(dòng)。團(tuán)隊(duì)項(xiàng)目里不要依賴隱式默認(rèn)值,否則換環(huán)境時(shí)容易出現(xiàn)“本機(jī)能跑,別人機(jī)器不能跑”的問題。
第二,字符集推薦 utf8mb4。
utf8mb4 能完整支持 Unicode,包括 emoji。新項(xiàng)目沒有必要再使用 MySQL 早期的 utf8。
第三,密碼里如果有特殊字符,需要 URL 編碼。
例如密碼里有 @、#、/、:,連接串可能被解析錯(cuò)。更穩(wěn)的做法是密碼避免使用這些字符,或者在生成連接串時(shí)進(jìn)行 URL 編碼。
四、創(chuàng)建 Engine
Engine 是 SQLAlchemy 的數(shù)據(jù)庫入口。
可以把它理解成:Engine = 數(shù)據(jù)庫連接入口 + 連接池
這里的 連接池 指的是一組可復(fù)用的數(shù)據(jù)庫連接。
如果每次請(qǐng)求都重新連接 MySQL,再斷開連接,成本會(huì)很高。連接池會(huì)提前維護(hù)一些連接,請(qǐng)求來了就借一條,用完再還回去。這樣后端服務(wù)可以更穩(wěn)定地處理大量請(qǐng)求。
Engine 本身不是某一次查詢,也不是某一個(gè)請(qǐng)求的連接。它更像應(yīng)用級(jí)的數(shù)據(jù)庫入口,通常在應(yīng)用啟動(dòng)時(shí)創(chuàng)建一次,然后整個(gè)應(yīng)用復(fù)用。
基礎(chǔ)寫法:
from sqlalchemy import create_engine
from app.core.config import settings
engine = create_engine(
# SQLAlchemy 根據(jù)這個(gè) URL 判斷數(shù)據(jù)庫類型、驅(qū)動(dòng)、賬號(hào)、密碼、主機(jī)和庫名
settings.database_url,
# echo=True 會(huì)把 SQL 打到控制臺(tái),排查問題時(shí)很有用
echo=settings.database_echo,
# 每次從連接池取連接前先檢查連接是否還活著
# MySQL 長時(shí)間空閑后可能主動(dòng)斷開連接,這個(gè)參數(shù)能減少斷連錯(cuò)誤
pool_pre_ping=True,
)
常見參數(shù):
echo=True:打印 SQL,適合調(diào)試echo=False:不打印 SQL,適合正常開發(fā)pool_pre_ping=True:從連接池取連接前先檢查連接是否可用,減少 MySQL 空閑連接斷開導(dǎo)致的問題
MySQL 服務(wù)端可能會(huì)關(guān)閉長時(shí)間空閑的連接。后端服務(wù)如果繼續(xù)拿到這條已經(jīng)失效的連接,就會(huì)在執(zhí)行 SQL 時(shí)失敗。pool_pre_ping=True 可以讓 SQLAlchemy 在使用連接前做一次輕量檢查,發(fā)現(xiàn)連接不可用就重新連接。
五、創(chuàng)建 SessionLocal
Session 是 ORM 的操作上下文。
可以把它理解成:Session = 一次數(shù)據(jù)庫讀寫上下文 + 事務(wù)邊界
Session 這個(gè)名字容易誤會(huì)。它不是瀏覽器登錄態(tài)里的 Session,也不是用戶會(huì)話。
在 SQLAlchemy 里,Session 表示一次數(shù)據(jù)庫操作上下文:它知道你查了哪些對(duì)象、改了哪些對(duì)象、哪些對(duì)象準(zhǔn)備新增、最后要不要提交事務(wù)。
SessionLocal 則是一個(gè)“Session 工廠”。它不是具體的 Session,而是用來創(chuàng)建 Session 的函數(shù)式對(duì)象。每個(gè)請(qǐng)求進(jìn)來時(shí),都通過它創(chuàng)建一個(gè)新的 Session。
創(chuàng)建 Session 工廠:
from sqlalchemy.orm import sessionmaker
SessionLocal = sessionmaker(
# 這個(gè) Session 工廠創(chuàng)建出來的 Session,都使用前面創(chuàng)建的 engine
bind=engine,
# 不在查詢前自動(dòng) flush,減少隱式數(shù)據(jù)庫寫入時(shí)機(jī)
autoflush=False,
# 不自動(dòng)提交事務(wù)。寫操作完成后必須手動(dòng) db.commit()
autocommit=False,
# commit 后對(duì)象字段仍然可讀,不會(huì)立刻過期
expire_on_commit=False,
)
參數(shù)含義:
bind=engine:這個(gè) Session 工廠使用哪個(gè) Engineautoflush=False:不在查詢前自動(dòng)刷新變更,減少新手階段的隱式行為autocommit=False:不自動(dòng)提交事務(wù),寫操作必須顯式commitexpire_on_commit=False:提交后對(duì)象字段仍可直接讀取
使用方式:
from sqlalchemy import select
# with 負(fù)責(zé)在代碼塊結(jié)束后關(guān)閉 Session
# 關(guān)閉 Session 不等于關(guān)閉 MySQL 服務(wù),而是把連接還回連接池
with SessionLocal() as db:
# select(User) 生成查詢 users 表的 SQLAlchemy 查詢對(duì)象
# scalars(...) 表示返回 ORM 對(duì)象,而不是原始行對(duì)象
users = db.scalars(select(User)).all()
with 退出時(shí)會(huì)關(guān)閉 Session,把連接歸還給連接池。
六、定義 ORM 模型
ORM 模型用 Python 類描述數(shù)據(jù)庫表。
模型 指的是“數(shù)據(jù)庫表在 Python 代碼里的表示”。表里每一行數(shù)據(jù),讀取出來后可以變成一個(gè) Python 對(duì)象。對(duì)象的字段,對(duì)應(yīng)數(shù)據(jù)庫表的列。
先定義統(tǒng)一的 Base:
from sqlalchemy.orm import DeclarativeBase
# 所有 ORM 模型都繼承 Base
# SQLAlchemy 會(huì)通過 Base.metadata 收集所有表結(jié)構(gòu)
class Base(DeclarativeBase):
pass
再定義業(yè)務(wù)模型:
from datetime import datetime
from sqlalchemy import DateTime, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class User(Base):
# __tablename__ 指定這個(gè)模型對(duì)應(yīng)數(shù)據(jù)庫里的哪張表
__tablename__ = "users"
# primary_key=True 表示主鍵
# autoincrement=True 表示由 MySQL 自動(dòng)生成遞增 ID
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# unique=True 表示數(shù)據(jù)庫層面不允許重復(fù)
# nullable=False 表示這一列不能為空
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
email: Mapped[str] = mapped_column(String(191), unique=True, nullable=False)
# default=1 是 Python 側(cè)默認(rèn)值。創(chuàng)建 User 時(shí)不傳 status,就默認(rèn)是 1
status: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
# server_default=func.now() 表示由數(shù)據(jù)庫服務(wù)器生成默認(rèn)時(shí)間
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
這段代碼表示:
User 類
-> users 表
User.username
-> users.username 字段
User.email
-> users.email 字段
Mapped[...] 和 mapped_column(...) 是 SQLAlchemy 2.x 推薦的聲明式寫法。類型標(biāo)注不僅給編輯器看,也幫助 ORM 更清楚地理解字段類型。
七、執(zhí)行增刪改查
1. 查詢
查詢?nèi)坑脩簦?/p>
from sqlalchemy import select
from sqlalchemy.orm import Session
def list_users(db: Session) -> list[User]:
return list(db.scalars(select(User)).all())
按主鍵查詢:
from sqlalchemy.orm import Session
def get_user(db: Session, user_id: int) -> User | None:
return db.get(User, user_id)
按條件查詢:
from sqlalchemy import select
from sqlalchemy.orm import Session
def get_user_by_username(db: Session, username: str) -> User | None:
return db.scalar(select(User).where(User.username == username))
分頁查詢:
from sqlalchemy import func, select
from sqlalchemy.orm import Session
def page_users(db: Session, page: int, page_size: int) -> dict[str, object]:
# 第 1 頁跳過 0 條,第 2 頁跳過 page_size 條
offset = (page - 1) * page_size
# 先查總數(shù),用來給前端顯示一共有多少條數(shù)據(jù)
total = db.scalar(select(func.count()).select_from(User)) or 0
# 再查當(dāng)前頁數(shù)據(jù)
items = db.scalars(
select(User)
.order_by(User.id.desc())
.offset(offset)
.limit(page_size)
).all()
return {
"items": list(items),
"total": total,
"page": page,
"page_size": page_size,
}
2. 新增
from sqlalchemy.orm import Session
def create_user(db: Session, username: str, email: str) -> User:
# 這里只是在 Python 內(nèi)存里創(chuàng)建 ORM 對(duì)象,還沒有寫入數(shù)據(jù)庫
user = User(
username=username,
email=email,
status=1,
)
# 加入當(dāng)前 Session,表示這條數(shù)據(jù)準(zhǔn)備新增
db.add(user)
# 提交事務(wù)后,INSERT 才會(huì)真正執(zhí)行
db.commit()
# 刷新對(duì)象,拿到數(shù)據(jù)庫生成的 id、created_at 等字段
db.refresh(user)
return user
關(guān)鍵點(diǎn):
db.add(user):把對(duì)象加入當(dāng)前 Sessiondb.commit():提交事務(wù),真正寫入數(shù)據(jù)庫db.refresh(user):從數(shù)據(jù)庫刷新對(duì)象,拿到自增 ID、默認(rèn)值等字段
3. 更新
from sqlalchemy.orm import Session
def update_user_email(db: Session, user_id: int, email: str) -> User | None:
# db.get 適合按主鍵查詢
user = db.get(User, user_id)
if user is None:
return None
# 修改 ORM 對(duì)象字段后,SQLAlchemy 會(huì)記錄這次變化
user.email = email
# commit 時(shí) SQLAlchemy 會(huì)生成 UPDATE 語句
db.commit()
db.refresh(user)
return user
只要對(duì)象是從當(dāng)前 Session 查出來的,修改字段后執(zhí)行 commit(),SQLAlchemy 就會(huì)生成對(duì)應(yīng)的 UPDATE。
4. 刪除
from sqlalchemy.orm import Session
def delete_user(db: Session, user_id: int) -> bool:
user = db.get(User, user_id)
if user is None:
return False
# 標(biāo)記這個(gè) ORM 對(duì)象要?jiǎng)h除
db.delete(user)
# commit 時(shí) SQLAlchemy 會(huì)生成 DELETE 語句
db.commit()
return True
刪除數(shù)據(jù)時(shí)要考慮外鍵約束、關(guān)聯(lián)表、業(yè)務(wù)規(guī)則和是否允許物理刪除。真實(shí)項(xiàng)目里,用戶、訂單、支付記錄這類數(shù)據(jù)通常不會(huì)隨意物理刪除。
八、事務(wù)怎么處理
數(shù)據(jù)庫寫操作必須有清晰的事務(wù)邊界。
最常見的寫法是:
from sqlalchemy.orm import Session
def create_user_with_role(db: Session, user: User, role: Role) -> User:
try:
# 兩個(gè)新增動(dòng)作放在同一個(gè)事務(wù)里
db.add(user)
db.add(role)
# 兩個(gè)對(duì)象都寫入成功,事務(wù)才提交
db.commit()
db.refresh(user)
return user
except Exception:
# 任意一步失敗,都撤銷本次事務(wù)里的所有數(shù)據(jù)庫變更
db.rollback()
raise
事務(wù)的規(guī)則很簡單:
全部成功
-> commit
中途失敗
-> rollback
也可以使用 begin() 上下文:
# begin 會(huì)自動(dòng)管理事務(wù):
# 正常退出自動(dòng) commit,出現(xiàn)異常自動(dòng) rollback
with SessionLocal.begin() as db:
db.add(user)
db.add(role)
begin() 正常退出時(shí)自動(dòng)提交,發(fā)生異常時(shí)自動(dòng)回滾。
在業(yè)務(wù)接口里,更常見的是把 Session 交給 Service 或 Repository,然后在明確的業(yè)務(wù)動(dòng)作完成后提交。這樣讀起來更清楚,也方便處理業(yè)務(wù)異常。
九、在 FastAPI 中注入數(shù)據(jù)庫 Session
FastAPI 推薦用依賴注入管理請(qǐng)求級(jí)資源。
數(shù)據(jù)庫 Session 的依賴函數(shù)可以這樣寫:
from collections.abc import Generator
from sqlalchemy.orm import Session
from app.db.session import SessionLocal
def get_db() -> Generator[Session, None, None]:
# 每個(gè)請(qǐng)求進(jìn)來時(shí),創(chuàng)建一個(gè)新的數(shù)據(jù)庫 Session
db = SessionLocal()
try:
# yield 會(huì)把 db 交給路由函數(shù)使用
# 路由函數(shù)執(zhí)行完成后,代碼會(huì)繼續(xù)走到 finally
yield db
finally:
# 請(qǐng)求結(jié)束后關(guān)閉 Session,把連接還回連接池
db.close()
這段代碼的行為是:
請(qǐng)求進(jìn)入
-> 創(chuàng)建一個(gè) Session
路由函數(shù)執(zhí)行
-> 使用這個(gè) Session 查詢或?qū)懭霐?shù)據(jù)庫
請(qǐng)求結(jié)束
-> finally 關(guān)閉 Session
路由里使用:
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.dependencies.database import get_db
from app.modules.users.service import UserService
# prefix 表示這個(gè) router 下的接口都以 /users 開頭
# tags 用來給 OpenAPI 文檔分組
router = APIRouter(prefix="/users", tags=["users"])
# 聲明一個(gè)可復(fù)用的數(shù)據(jù)庫依賴類型
# 以后路由參數(shù)寫 db: SessionDep,就會(huì)自動(dòng)拿到 get_db 提供的 Session
SessionDep = Annotated[Session, Depends(get_db)]
@router.get("/{user_id}")
def get_user(user_id: int, db: SessionDep) -> dict[str, object]:
# 路由層不直接寫數(shù)據(jù)庫查詢,而是調(diào)用 Service
user = UserService.get_user(db, user_id)
if user is None:
# Service 返回 None,路由層把它轉(zhuǎn)換成 HTTP 404
raise HTTPException(status_code=404, detail="User not found")
return {
"id": user.id,
"username": user.username,
"email": user.email,
}
這里路由層只做三件事:
- 接收 HTTP 參數(shù)
- 調(diào)用業(yè)務(wù)邏輯
- 返回 HTTP 響應(yīng)
它不負(fù)責(zé)創(chuàng)建數(shù)據(jù)庫連接,也不直接堆很多 SQLAlchemy 查詢。
十、從零搭一個(gè)可維護(hù)的工程結(jié)構(gòu)
項(xiàng)目小的時(shí)候,一個(gè) main.py 就能跑。
項(xiàng)目變大后,不建議把配置、數(shù)據(jù)庫連接、模型、路由、業(yè)務(wù)邏輯都塞在一起。更穩(wěn)的結(jié)構(gòu)是:
todo-api/
├── app/
│ ├── main.py
│ ├── api/
│ │ └── v1/
│ │ └── router.py
│ ├── core/
│ │ └── config.py
│ ├── db/
│ │ ├── base.py
│ │ ├── models.py
│ │ └── session.py
│ ├── dependencies/
│ │ └── database.py
│ └── modules/
│ └── users/
│ ├── models.py
│ ├── schemas.py
│ ├── repository.py
│ ├── service.py
│ └── router.py
├── migrations/
├── tests/
├── .env
└── pyproject.toml
每一層的職責(zé)要清楚。
app/core/config.py -> 讀取環(huán)境變量 app/db/session.py -> 創(chuàng)建 Engine、SessionLocal、init_database app/db/base.py -> 定義 SQLAlchemy Base app/db/models.py -> 統(tǒng)一導(dǎo)入所有 ORM 模型,方便 create_all 或 Alembic 發(fā)現(xiàn)模型 app/dependencies/database.py -> 定義 get_db app/modules/users/models.py -> 用戶表 ORM 模型 app/modules/users/schemas.py -> 用戶接口的請(qǐng)求體和響應(yīng)體 app/modules/users/repository.py -> 用戶相關(guān)數(shù)據(jù)庫讀寫 app/modules/users/service.py -> 用戶業(yè)務(wù)規(guī)則 app/modules/users/router.py -> 用戶 HTTP 接口
這個(gè)結(jié)構(gòu)的核心原則是:
- 公共基礎(chǔ)設(shè)施放 app/core、app/db、app/dependencies
- 業(yè)務(wù)代碼按模塊放 app/modules/{module_name}
十一、核心文件怎么寫
1. app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
# 指定 .env 文件作為本地配置來源
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
)
# 應(yīng)用名稱,會(huì)展示在接口文檔里
app_name: str = "Todo API"
# MySQL 連接串,例如 mysql+pymysql://root:password@127.0.0.1:3306/todo_api
database_url: str
# 是否打印 SQL
database_echo: bool = False
# 是否啟動(dòng)時(shí)自動(dòng) create_all 建表
database_auto_create: bool = False
# 全局配置對(duì)象。其他模塊只讀取它,不在業(yè)務(wù)代碼里手寫環(huán)境變量解析
settings = Settings()
2. app/db/base.py
from sqlalchemy.orm import DeclarativeBase
# 所有 ORM 模型共同繼承的基類
# 后續(xù) User、Todo、Role 等模型都會(huì)注冊(cè)到 Base.metadata 中
class Base(DeclarativeBase):
pass
3. app/modules/users/models.py
from datetime import datetime
from sqlalchemy import DateTime, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class User(Base):
# users 是數(shù)據(jù)庫里的真實(shí)表名
__tablename__ = "users"
# id 是主鍵,由 MySQL 自增生成
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# username 和 email 都要求唯一且不能為空
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
email: Mapped[str] = mapped_column(String(191), unique=True, nullable=False)
# status 可以用來表示啟用、禁用等業(yè)務(wù)狀態(tài)
status: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
# created_at 由數(shù)據(jù)庫寫入當(dāng)前時(shí)間
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
4. app/db/models.py
from app.modules.users.models import User # 統(tǒng)一導(dǎo)出模型,方便其他地方一次性導(dǎo)入 __all__ = ["User"]
這個(gè)文件看起來簡單,但很重要。
SQLAlchemy 只有在模型類被 Python 導(dǎo)入后,Base.metadata 才知道有哪些表。集中導(dǎo)入模型,可以避免“明明寫了模型,create_all 或 Alembic 卻找不到表”的問題。
5. app/db/session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
from app.db.base import Base
from app.db import models
engine = create_engine(
# 從配置讀取連接串,避免把賬號(hào)密碼寫死在代碼里
settings.database_url,
# 控制是否打印 SQL
echo=settings.database_echo,
# 防止 MySQL 空閑連接失效后,請(qǐng)求第一次查詢就報(bào)錯(cuò)
pool_pre_ping=True,
)
SessionLocal = sessionmaker(
# SessionLocal 創(chuàng)建出的 Session 都綁定到這個(gè) engine
bind=engine,
autoflush=False,
autocommit=False,
expire_on_commit=False,
)
def init_database() -> None:
# 只在明確開啟時(shí)自動(dòng)建表
# 正式項(xiàng)目通常使用 Alembic,不依賴 create_all
if settings.database_auto_create:
Base.metadata.create_all(bind=engine)
from app.db import models 不是為了在代碼里直接使用 models 變量,而是為了確保所有 ORM 模型都被導(dǎo)入,讓 Base.metadata 收集到表結(jié)構(gòu)。
6. app/dependencies/database.py
from collections.abc import Generator
from sqlalchemy.orm import Session
from app.db.session import SessionLocal
def get_db() -> Generator[Session, None, None]:
# 為當(dāng)前請(qǐng)求創(chuàng)建 Session
db = SessionLocal()
try:
# 把 Session 交給 FastAPI 路由函數(shù)
yield db
finally:
# 無論接口成功還是失敗,最終都關(guān)閉 Session
db.close()
7. app/modules/users/schemas.py
from datetime import datetime
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
# 創(chuàng)建用戶時(shí),前端必須傳 username 和 email
username: str
email: EmailStr
class UserRead(BaseModel):
# 返回給前端的用戶字段
id: int
username: str
email: EmailStr
status: int
created_at: datetime
如果使用 EmailStr,需要安裝郵箱校驗(yàn)依賴:
uv add "pydantic[email]"
或者:
pip install "pydantic[email]"
如果不想加這個(gè)依賴,可以先把 EmailStr 改成普通的 str。
8. app/modules/users/repository.py
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.modules.users.models import User
class UserRepository:
@staticmethod
def get_by_id(db: Session, user_id: int) -> User | None:
# Repository 只關(guān)心數(shù)據(jù)庫怎么查
return db.get(User, user_id)
@staticmethod
def get_by_username(db: Session, username: str) -> User | None:
# 根據(jù)唯一用戶名查單個(gè)用戶
return db.scalar(select(User).where(User.username == username))
@staticmethod
def create(db: Session, username: str, email: str) -> User:
# 這里只 add,不 commit
# commit 由 Service 在完整業(yè)務(wù)動(dòng)作結(jié)束后統(tǒng)一處理
user = User(username=username, email=email)
db.add(user)
return user
Repository 只處理數(shù)據(jù)庫讀寫,不寫 HTTP 異常,也不關(guān)心接口怎么返回。
9. app/modules/users/service.py
from sqlalchemy.orm import Session
from app.modules.users.models import User
from app.modules.users.repository import UserRepository
from app.modules.users.schemas import UserCreate
class UserService:
@staticmethod
def get_user(db: Session, user_id: int) -> User | None:
# Service 可以直接復(fù)用 Repository
return UserRepository.get_by_id(db, user_id)
@staticmethod
def create_user(db: Session, data: UserCreate) -> User:
# 業(yè)務(wù)規(guī)則:用戶名不能重復(fù)
exists = UserRepository.get_by_username(db, data.username)
if exists is not None:
raise ValueError("Username already exists")
try:
# 先執(zhí)行數(shù)據(jù)庫新增動(dòng)作
user = UserRepository.create(
db,
username=data.username,
email=data.email,
)
# 業(yè)務(wù)動(dòng)作完整成功后再提交事務(wù)
db.commit()
db.refresh(user)
return user
except Exception:
# 提交前或提交時(shí)出錯(cuò),都回滾本次事務(wù)
db.rollback()
raise
Service 負(fù)責(zé)業(yè)務(wù)規(guī)則和事務(wù)邊界。
比如“用戶名不能重復(fù)”屬于業(yè)務(wù)規(guī)則,應(yīng)該放在 Service。commit 和 rollback 也更適合放在完成一個(gè)業(yè)務(wù)動(dòng)作的位置。
10. app/modules/users/router.py
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.dependencies.database import get_db
from app.modules.users.schemas import UserCreate, UserRead
from app.modules.users.service import UserService
# 這個(gè)文件只定義 users 模塊自己的 HTTP 接口
router = APIRouter(prefix="/users", tags=["users"])
SessionDep = Annotated[Session, Depends(get_db)]
@router.get("/{user_id}", response_model=UserRead)
def get_user(user_id: int, db: SessionDep) -> UserRead:
# 1. 路由層接收 user_id
# 2. 把 db 和 user_id 交給 Service
user = UserService.get_user(db, user_id)
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
# ORM 對(duì)象轉(zhuǎn)換成響應(yīng)模型,再由 FastAPI 轉(zhuǎn)成 JSON
return UserRead.model_validate(user, from_attributes=True)
@router.post("", response_model=UserRead, status_code=status.HTTP_201_CREATED)
def create_user(data: UserCreate, db: SessionDep) -> UserRead:
try:
# data 已經(jīng)被 Pydantic 校驗(yàn)過,這里交給 Service 處理業(yè)務(wù)
user = UserService.create_user(db, data)
except ValueError as exc:
# 業(yè)務(wù)異常在路由層轉(zhuǎn)換成 HTTP 狀態(tài)碼
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=str(exc),
) from exc
return UserRead.model_validate(user, from_attributes=True)
路由層要薄。
它應(yīng)該關(guān)心 HTTP 語義,比如:
- 什么路徑
- 什么方法
- 什么狀態(tài)碼
- 請(qǐng)求體是什么
- 響應(yīng)體是什么
- 業(yè)務(wù)異常轉(zhuǎn)成什么 HTTP 錯(cuò)誤
它不應(yīng)該塞滿數(shù)據(jù)庫查詢。
11. app/api/v1/router.py
from fastapi import APIRouter from app.modules.users.router import router as users_router api_router = APIRouter(prefix="/api/v1") # 把 users 模塊的接口掛到 /api/v1 下 api_router.include_router(users_router)
12. app/main.py
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.api.v1.router import api_router
from app.core.config import settings
from app.db.session import init_database
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
# 應(yīng)用啟動(dòng)時(shí)執(zhí)行一次初始化邏輯
init_database()
# yield 之前是啟動(dòng)邏輯,yield 之后可以寫關(guān)閉邏輯
yield
app = FastAPI(
title=settings.app_name,
# lifespan 用來管理應(yīng)用啟動(dòng)和關(guān)閉階段的邏輯
lifespan=lifespan,
)
# 注冊(cè)總路由
app.include_router(api_router)
啟動(dòng):
uv run fastapi dev app/main.py
也可以用 Uvicorn:
uv run uvicorn app.main:app --reload
十二、一條創(chuàng)建用戶請(qǐng)求怎么走完整流程
現(xiàn)在把前面的文件串起來,看一條真實(shí)請(qǐng)求:
POST /api/v1/users
Content-Type: application/json
{
"username": "copyer",
"email": "copyer@example.com"
}它在后端的執(zhí)行順序是這樣的。
1. app/main.py
FastAPI 應(yīng)用已經(jīng) include_router(api_router)
2. app/api/v1/router.py
/api/v1 前綴命中,總路由繼續(xù)把請(qǐng)求交給 users_router
3. app/modules/users/router.py
POST /users 命中 create_user 路由函數(shù)
4. FastAPI 發(fā)現(xiàn)路由參數(shù)里有 db: SessionDep
于是先執(zhí)行 get_db()
5. app/dependencies/database.py
get_db() 通過 SessionLocal() 創(chuàng)建一個(gè)新的數(shù)據(jù)庫 Session
6. router.py
create_user(data, db) 開始執(zhí)行
data 是 Pydantic 校驗(yàn)后的 UserCreate
db 是 get_db() 提供的 SQLAlchemy Session
7. service.py
UserService.create_user(db, data) 執(zhí)行業(yè)務(wù)規(guī)則
先檢查 username 是否重復(fù)
8. repository.py
UserRepository.get_by_username(db, data.username)
使用 SQLAlchemy 查詢 User 表
9. SQLAlchemy
把 select(User).where(...) 翻譯成 SELECT SQL
10. PyMySQL
把 SELECT SQL 發(fā)給 MySQL
11. MySQL
執(zhí)行查詢,返回是否已有這個(gè)用戶
12. service.py
如果用戶名不存在,調(diào)用 Repository 創(chuàng)建 User 對(duì)象
13. repository.py
db.add(user)
把 user 標(biāo)記為準(zhǔn)備新增,但此時(shí)還沒有真正寫入數(shù)據(jù)庫
14. service.py
db.commit()
提交事務(wù),SQLAlchemy 生成 INSERT SQL
15. PyMySQL
把 INSERT SQL 發(fā)給 MySQL
16. MySQL
寫入 users 表,生成自增 id 和默認(rèn)時(shí)間
17. service.py
db.refresh(user)
把數(shù)據(jù)庫生成的新字段刷新回 Python 對(duì)象
18. router.py
把 ORM 對(duì)象轉(zhuǎn)換成 UserRead
19. FastAPI
把 UserRead 轉(zhuǎn)成 JSON 響應(yīng)
20. get_db()
請(qǐng)求結(jié)束,finally 執(zhí)行 db.close()
Session 關(guān)閉,連接歸還連接池這條鏈路里,最重要的是不要把職責(zé)混在一起。
Router
-> 只處理 HTTP 輸入輸出
Service
-> 處理業(yè)務(wù)規(guī)則和事務(wù)
Repository
-> 處理數(shù)據(jù)庫讀寫細(xì)節(jié)
Session
-> 記錄本次數(shù)據(jù)庫操作,并負(fù)責(zé) commit / rollback
Engine
-> 提供數(shù)據(jù)庫連接入口和連接池
PyMySQL
-> 真正和 MySQL 通信
如果創(chuàng)建用戶失敗,流程會(huì)變成:
Service 執(zhí)行中出錯(cuò)
-> except 捕獲異常
-> db.rollback()
-> 撤銷本次事務(wù)里已經(jīng)準(zhǔn)備寫入的變更
-> raise 把異常繼續(xù)拋出
-> Router 或全局異常處理把異常轉(zhuǎn)換成 HTTP 響應(yīng)
-> get_db finally 關(guān)閉 Session
所以 commit、rollback、close 是三個(gè)不同動(dòng)作:
commit
-> 確認(rèn)寫入數(shù)據(jù)庫
rollback
-> 撤銷本次事務(wù)
close
-> 關(guān)閉當(dāng)前 Session,把連接還回連接池
不要用 close 代替 rollback,也不要以為 add 之后數(shù)據(jù)已經(jīng)寫進(jìn)數(shù)據(jù)庫。真正落庫發(fā)生在 commit。
十三、同步還是異步
這篇使用的是同步 SQLAlchemy + PyMySQL。
也就是說:
SQLAlchemy create_engine
-> PyMySQL
-> 同步數(shù)據(jù)庫調(diào)用
這種方式適合大多數(shù)入門項(xiàng)目和普通后臺(tái)系統(tǒng),代碼簡單,資料多,問題也容易排查。
如果要做全鏈路異步,依賴會(huì)變成另一套:
SQLAlchemy create_async_engine
-> asyncmy 或 aiomysql
-> AsyncSession
-> async def 路由
不要把同步 Session 和異步 Engine 混在一起,也不要只因?yàn)?FastAPI 支持 async def 就強(qiáng)行把數(shù)據(jù)庫層改成異步。先把同步版本寫清楚,比一開始就堆異步概念更重要。
十四、常見錯(cuò)誤
1. 忘記提交事務(wù)
只寫:
db.add(user)
數(shù)據(jù)不會(huì)真正落庫。寫操作要執(zhí)行:
db.commit()
如果還要拿到自增 ID 或數(shù)據(jù)庫默認(rèn)值,再執(zhí)行:
db.refresh(user)
2. 異常后不回滾
寫操作失敗后,應(yīng)該:
db.rollback()
否則當(dāng)前 Session 可能處于失敗狀態(tài),后續(xù)繼續(xù)使用會(huì)報(bào)錯(cuò)。
3. 在路由里直接創(chuàng)建 Engine
不要這樣寫:
@router.get("/users")
def list_users():
engine = create_engine(...)
Engine 應(yīng)該在應(yīng)用啟動(dòng)時(shí)創(chuàng)建一次,復(fù)用連接池。每個(gè)請(qǐng)求里只創(chuàng)建和關(guān)閉 Session。
4. 把數(shù)據(jù)庫密碼寫進(jìn)代碼
不要把連接串硬編碼在 Python 文件里。
更好的方式:
.env
-> 本地開發(fā)配置
生產(chǎn)環(huán)境變量
-> 生產(chǎn)配置
.env 通常不提交到 Git。
5. 模型沒有被導(dǎo)入
如果調(diào)用 Base.metadata.create_all(bind=engine) 后沒有生成表,常見原因是模型類沒有被導(dǎo)入。
解決方式是建立一個(gè)統(tǒng)一的模型導(dǎo)入文件:
from app.modules.users.models import User __all__ = ["User"]
并在初始化數(shù)據(jù)庫前導(dǎo)入它。
以上就是Python FastAPI連接操作MySQL數(shù)據(jù)庫指南的詳細(xì)內(nèi)容,更多關(guān)于Python FastAPI連接MySQL的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 利用Python連接MySQL超詳細(xì)實(shí)戰(zhàn)教程
- Python腳本連接MySQL數(shù)據(jù)庫的完整指南
- Python腳本實(shí)現(xiàn)mysql數(shù)據(jù)庫連接并插入數(shù)據(jù)
- Python連接MySQL、PostgreSQL數(shù)據(jù)庫實(shí)現(xiàn)過程
- Python進(jìn)行SQLite和MySQL數(shù)據(jù)庫連接與操作的完整指南
- Python連接MySQL數(shù)據(jù)庫連接池的操作詳解
- Python3.6連接MySQL的詳細(xì)步驟
- Python連接MySQL數(shù)據(jù)庫的四種方法
相關(guān)文章
python 在指定范圍內(nèi)隨機(jī)生成不重復(fù)的n個(gè)數(shù)實(shí)例
今天小編就為大家分享一篇python 在指定范圍內(nèi)隨機(jī)生成不重復(fù)的n個(gè)數(shù)實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-01-01
使用Python在Excel文檔中創(chuàng)建表格并應(yīng)用表格樣式
本文介紹了使用FreeSpire.XLS for Python在Excel中創(chuàng)建表格并應(yīng)用各種內(nèi)置樣式的方法,通過編程實(shí)現(xiàn)數(shù)據(jù)的自動(dòng)格式化,提升報(bào)表的專業(yè)性和效率,涵蓋淺色、中等和深色樣式系列,以及配置總計(jì)行、行條紋、列條紋和篩選功能,需要的朋友可以參考下2026-04-04
python利用opencv實(shí)現(xiàn)SIFT特征提取與匹配
這篇文章主要為大家詳細(xì)介紹了python利用opencv實(shí)現(xiàn)SIFT特征提取與匹配,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03

