在 FastAPI 后端中接入 MySQL,不只是写一条连接串那么简单。我们需要同时解决驱动选择、ORM 映射、会话管理、事务控制以及项目结构等问题。下面以同步版为例,梳理一条从安装依赖到增删改查的完整链路。
一、依赖安装
项目需要以下核心库:- pip install "fastapi[standard]" sqlalchemy pymysql pydantic-settings
复制代码 如果使用新版 MySQL 认证方式(如 caching_sha2_password),可以加装 RSA 支持:- pip install "pymysql[rsa]"
复制代码 正式环境建议额外安装 Alembic 管理数据库迁移:
二、环境配置
数据库连接串和开关参数放在 .env 文件中,避免硬编码:- .env
- DATABASE_URL="mysql+pymysql://root:password@127.0.0.1:3306/todo_api?charset=utf8mb4"
- DATABASE_ECHO=false
- DATABASE_AUTO_CREATE=false
复制代码 用 pydantic-settings 读取:- from pydantic_settings import BaseSettings, SettingsConfigDict
- class Settings(BaseSettings):
- model_config = SettingsConfigDict(
- env_file=".env",
- env_file_encoding="utf-8",
- )
- database_url: str
- database_echo: bool = False
- database_auto_create: bool = False
- settings = Settings()
复制代码
三、创建 Engine 与 SessionLocal
Engine 是数据库入口,管理连接池:- from sqlalchemy import create_engine
- from app.core.config import settings
- engine = create_engine(
- settings.database_url,
- echo=settings.database_echo,
- pool_pre_ping=True, # 取出连接前检查是否存活
- )
复制代码 SessionLocal 是一个会话工厂,每个请求创建一个独立会话:- from sqlalchemy.orm import sessionmaker
- SessionLocal = sessionmaker(
- bind=engine,
- autoflush=False,
- autocommit=False,
- expire_on_commit=False,
- )
复制代码
四、定义 ORM 模型
首先创建基类:- from sqlalchemy.orm import DeclarativeBase
- class Base(DeclarativeBase):
- pass
复制代码 接着定义业务模型,以 User 为例:- 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__ = "users"
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
- username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
- email: Mapped[str] = mapped_column(String(191), unique=True, nullable=False)
- status: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
- created_at: Mapped[datetime] = mapped_column(
- DateTime(timezone=True),
- server_default=func.now(),
- nullable=False,
- )
复制代码
五、增删改查实现
1. 查询- from sqlalchemy import select
- from sqlalchemy.orm import Session
- def list_users(db: Session) -> list[User]:
- return list(db.scalars(select(User)).all())
- def get_user(db: Session, user_id: int) -> User | None:
- return db.get(User, user_id)
- def get_user_by_username(db: Session, username: str) -> User | None:
- return db.scalar(select(User).where(User.username == username))
- def page_users(db: Session, page: int, page_size: int) -> dict:
- offset = (page - 1) * page_size
- total = db.scalar(select(func.count()).select_from(User)) or 0
- 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. 新增- def create_user(db: Session, username: str, email: str) -> User:
- user = User(username=username, email=email, status=1)
- db.add(user)
- db.commit()
- db.refresh(user)
- return user
复制代码 注意:commit 后必须 refresh 才能拿到数据库生成的自增 ID 和默认时间戳。
3. 更新- def update_user_email(db: Session, user_id: int, email: str) -> User | None:
- user = db.get(User, user_id)
- if user is None:
- return None
- user.email = email
- db.commit()
- db.refresh(user)
- return user
复制代码 直接从会话查出的对象,修改字段后 commit 即可生成 UPDATE。
4. 删除- def delete_user(db: Session, user_id: int) -> bool:
- user = db.get(User, user_id)
- if user is None:
- return False
- db.delete(user)
- db.commit()
- return True
复制代码
六、事务管理
写操作建议用 try/commit/rollback 包裹,或者使用 sessionmaker 的 begin 上下文:- from sqlalchemy.orm import Session
- def create_user_with_role(db: Session, user: User, role: Role) -> User:
- try:
- db.add(user)
- db.add(role)
- db.commit()
- db.refresh(user)
- return user
- except Exception:
- db.rollback()
- raise
复制代码 等价写法:- with SessionLocal.begin() as db:
- db.add(user)
- db.add(role)
复制代码
七、在 FastAPI 中注入 Session
每个请求应获取独立会话,依赖注入是最佳实践:- from collections.abc import Generator
- from sqlalchemy.orm import Session
- from app.db.session import SessionLocal
- def get_db() -> Generator[Session, None, None]:
- db = SessionLocal()
- try:
- yield db
- finally:
- db.close()
复制代码 路由中使用:- from typing import Annotated
- from fastapi import APIRouter, Depends
- from sqlalchemy.orm import Session
- from app.dependencies.database import get_db
- router = APIRouter(prefix="/users")
- @router.get("/")
- def get_users(db: Session = Depends(get_db)):
- # ...
复制代码 也可以定义类型别名:- SessionDep = Annotated[Session, Depends(get_db)]
复制代码
八、推荐工程结构
一个可维护的目录布局示例:- app/
- ├── core/
- │ └── config.py # 配置读取
- ├── db/
- │ ├── base.py # DeclarativeBase
- │ ├── session.py # engine 和 SessionLocal
- │ └── models.py # 汇总导入所有 ORM 模型
- ├── dependencies/
- │ └── database.py # get_db 依赖
- ├── modules/
- │ └── users/
- │ ├── models.py # User ORM 模型
- │ ├── schemas.py # Pydantic 数据模型
- │ ├── repository.py # 数据库查询封装
- │ ├── service.py # 业务逻辑
- │ └── router.py # 路由定义
- ├── api/
- │ └── v1/
- │ └── router.py # 聚合所有模块路由
- └── main.py # FastAPI 应用入口
复制代码
九、常见错误排查
1. 忘记提交事务:写操作完成后必须调用 db.commit(),否则数据不会写入。
2. 异常后未回滚:捕获异常后应调用 db.rollback() 释放会话状态,否则后续查询可能报“Pending rollback”错误。
3. 在路由中直接创建 Engine:每个路由函数都创建新的 Engine 会导致连接泄漏,应将 Engine 作为全局单例。
4. 密码硬编码:数据库密码应放在环境变量或 .env 文件中,切勿写死在代码仓库。
5. 模型未被导入:如果使用 Base.metadata.create_all 自动建表,必须确保所有模型模块被提前导入,否则对应表不会创建。
全文围绕 FastAPI + SQLAlchemy + PyMySQL 构建 MySQL 后端,覆盖配置、模型、CRUD、事务与依赖注入,适合作为工程初始化的基础参考。 |