FastAPI零基础教程(六)- 数据库集成,SQLModel无缝衔接

文章目录

    • 前言
    • 一、阶段学习目标
    • 二、整体项目分层目录
    • 三、环境依赖安装
    • 四、分步完整代码实现
      • [4\.1 \.env 环境配置文件](#4.1 .env 环境配置文件)
      • [4\.2 config/settings\.py 配置管理](#4.2 config/settings.py 配置管理)
      • [4\.3 database/session\.py 引擎与会话依赖(核心)](#4.3 database/session.py 引擎与会话依赖(核心))
      • [4\.4 common/response\.py 全局统一泛型返回](#4.4 common/response.py 全局统一泛型返回)
      • [4\.5 crud/base\.py 通用CRUD父类](#4.5 crud/base.py 通用CRUD父类)
      • [4\.6 models/user\.py 数据库实体(一对多)](#4.6 models/user.py 数据库实体(一对多))
      • [4\.7 models/address\.py 子表实体](#4.7 models/address.py 子表实体)
      • [4\.8 schemas/user\_schema DTO分层模型](#4.8 schemas/user_schema DTO分层模型)
      • [4\.9 dependencies/auth JWT鉴权依赖(复用第五阶段)](#4.9 dependencies/auth JWT鉴权依赖(复用第五阶段))
      • [4\.10 crud/user\_crud 业务CRUD](#4.10 crud/user_crud 业务CRUD)
      • [4\.11 routers/auth\_router 注册登录接口](#4.11 routers/auth_router 注册登录接口)
      • [4\.12 routers/user\_router 带鉴权数据库CRUD接口](#4.12 routers/user_router 带鉴权数据库CRUD接口)
      • [4\.13 main\.py 项目入口](#4.13 main.py 项目入口)
    • 五、Alembic数据库迁移适配(生产必备)
      • [5\.1 初始化迁移](#5.1 初始化迁移)
      • [5\.2 修改alembic/env\.py](#5.2 修改alembic/env.py)
      • [5\.3 标准迁移流程](#5.3 标准迁移流程)
    • 六、阶段综合实战流程
    • 七、阶段核心总结
    • 八、新手避坑指南

前言

前面我们已经完整学完FastAPI基础、请求响应、文件异常、依赖注入、JWT安全认证,但是所有案例都使用内存假数据,无法落地真实业务。

本阶段将把之前精通的SQLModel 完整接入FastAPI,实现一套标准化分层后端工程:

统一数据库引擎、yield会话依赖、分层models/schemas/crud、分页通用逻辑、关联查询、Alembic迁移适配,同时整合前面JWT鉴权接口,实现带登录校验的数据库CRUD接口。

整套代码可直接作为企业项目模板,1天学完即可独立开发数据库驱动的后端接口。

前置储备:熟练Pydantic、SQLModel全套语法、FastAPI Depends依赖、JWT登录鉴权。

一、阶段学习目标

  1. 搭建FastAPI + SQLModel标准分层项目目录;

  2. 使用pydantic-settings管理多环境数据库配置;

  3. 封装全局Engine与yield数据库会话依赖(生产标准写法);

  4. 通用BaseCRUD父类封装,复用增删改查、分页逻辑;

  5. 分层设计数据库实体models、入出参DTO schemas;

  6. 实现一对多关联查询、嵌套返回脱敏数据;

  7. 整合JWT鉴权,实现登录用户操作数据库接口;

  8. 对接Alembic迁移工具,线上禁用create_all;

  9. 完整实战:用户注册登录、个人信息CRUD、分页列表。

二、整体项目分层目录

plaintext 复制代码
fastapi_sqlmodel_demo/
├── .env                # 环境配置文件
├── alembic/            # 数据库迁移文件夹
├── alembic.ini
├── config/
│   └── settings.py     # pydantic-settings全局配置
├── database/
│   └── session.py      # Engine、DB会话依赖
├── models/             # SQLModel数据库实体 table=True
│   ├── user.py
│   └── address.py
├── schemas/            # DTO分层模型 Create/Update/Public
│   ├── user_schema.py
│   └── address_schema.py
├── crud/
│   ├── base.py         # 通用CRUD父类
│   ├── user_crud.py
│   └── address_crud.py
├── routers/
│   ├── auth_router.py   # 登录注册接口
│   ├── user_router.py   # 用户业务接口
│   └── __init__.py
├── dependencies/
│   └── auth.py         # JWT鉴权依赖链
├── common/
│   ├── response.py     # 全局泛型统一返回
│   └── exception.py    # 全局异常捕获
└── main.py             # 项目入口

三、环境依赖安装

bash 复制代码
pip install fastapi uvicorn sqlmodel pydantic-settings python-dotenv passlib[bcrypt] python-jose[cryptography] alembic

四、分步完整代码实现

4.1 .env 环境配置文件

plaintext 复制代码
# 项目基础配置
APP_ENV=dev
DEBUG=True
# 数据库配置
DB_URL=sqlite:///./dev.db
# JWT安全配置
SECRET_KEY=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
TOKEN_EXPIRE_MIN=30

4.2 config/settings.py 配置管理

python 复制代码
from pydantic_settings import BaseSettings, SettingsConfigDict

class GlobalSettings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
    app_env: str
    debug: bool
    db_url: str
    secret_key: str
    token_expire_min: int

settings = GlobalSettings()

4.3 database/session.py 引擎与会话依赖(核心)

python 复制代码
from sqlmodel import create_engine, Session
from config.settings import settings

# 区分开发/生产环境引擎参数
if settings.app == "dev":
    engine = create_engine(
        settings.db_url,
        echo=True,
        connect_args={"check_same_thread": False}
    )
else:
    engine = create_engine(settings.db_url, echo=False, pool_size=15)

# 标准yield数据库会话依赖,自动关闭连接无泄露
def get_db():
    with Session(engine) as db_session:
        yield db_session

4.4 common/response.py 全局统一泛型返回

python 复制代码
from typing import Generic, TypeVar, Optional, List
from sqlmodel import SQLModel

T = TypeVar("T")

# 分页通用结构
class PageData(SQLModel, Generic[T]):
    page: int
    page_size: int
    total: int
    items: List[T]

# 全局接口返回模板
class ApiResp(SQLModel, Generic[T]):
    code: int = 200
    msg: str = "success"
    data: Optional[T] = None

def success_resp(data=None) -> ApiResp:
    return ApiResp(data=data)

def fail_resp(msg: str, code: int = 400) -> ApiResp:
    return Api(code=code, msg=msg)

4.5 crud/base.py 通用CRUD父类

python 复制代码
from typing import Type, TypeVar, Generic, Optional
from sqlmodel import SQLModel, Session, select, func

ModelT = TypeVar("ModelT", bound=SQLModel)
CreateT = TypeVar("CreateT", bound=SQLModel)

class BaseCRUD(Generic[ModelT, CreateT]):
    def __init__(self, model: Type[ModelT]):
        self.model = model

    # 根据主键查询
    def get_by_id(self, db: Session, obj_id: int) -> Optional[ModelT]:
        return db.get(self.model, obj_id)

    # 分页查询
    def page_list(self, db: Session, page: int, page_size: int):
        offset = (page - 1) * page_size
        stmt = select(self.model).offset(offset).limit(page_size)
        items = db.exec(stmt).all()
        total = db.exec(select(func.count(self.model.id))).scalar()
        return {"page": page, "page_size": page_size, "total": total, "items": items}

    # 新增数据
    def create(self, db: Session, obj_in: CreateT) -> ModelT:
        db_obj = self.model.model_validate(obj_in)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj

    # 局部更新
    def update(self, db: Session, db_obj: Model, update_dict: dict):
        for k, v in update_dict.items():
            if hasattr(db_obj, k):
                setattr(db_obj, v)
        db.commit()
        db.refresh(db_obj)
        return db_obj

    # 删除
    def delete(self, db: Session, obj_id: int):
        obj = self.get_by_id(db, obj_id)
        if obj:
            db.delete(obj)
            db.commit()
        return obj

4.6 models/user.py 数据库实体(一对多)

python 复制代码
from sqlmodel import SQLModel, Field, Relationship
from typing import Optional, List
from datetime import datetime

class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    username: str = Field(min_length=3, unique=True, index=True)
    email: str = Field(index=True)
    hashed_password: str = Field(exclude=True)
    role: str = Field(default="user")
    disabled: bool = Field(default=False)
    create_time: datetime = Field(default_factory=datetime.utcnow)
    # 一对多关联地址,级联删除
    addresses: List["Address"] = Relationship(back_populates="user", cascade_delete=True)

# 导入解决循环引用
from models.address import Address
User.model_rebuild()

4.7 models/address.py 子表实体

python 复制代码
from sqlmodel import SQLModel, Field, Relationship
from typing import Optional

class Address(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    province: str
    city: str
    detail: str
    user_id: Optional[int] = Field(default=None, foreign_key="user.id", ondelete="CASCADE")
    user: Optional["User"] = Relationship(back_populates="user")

from models.user import User
Address.model_rebuild()

4.8 schemas/user_schema DTO分层模型

python 复制代码
from sqlmodel import SQLModel
from pydantic import EmailStr, field_validator
from typing import Optional, List
from schemas.address_schema import AddressPublic

# 基础公共字段
class UserBase(SQLModel):
    username: str
    email: EmailStr

# 注册入参
class UserCreate(UserBase):
    password: str

    @field_validator("password")
    def check_pwd(cls, v):
        import re
        if len(v) < 8 or not re.search(r"[A-Z]", v) or not re.search(r"\d", v):
            raise ValueError("密码8位以上,包含大写字母+数字")
        return v

# 更新入参
class UserUpdate(SQLModel):
    username: Optional[str] = None
    email: Optional[EmailStr] = None

# 返回脱敏模型(嵌套地址,隐藏密码)
class UserPublic(UserBase):
    id: int
    role: str
    disabled: bool
    create_time: datetime
    addresses: List[AddressPublic] = []

4.9 dependencies/auth JWT鉴权依赖(复用第五阶段)

python 复制代码
from datetime import timedelta
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from sqlmodel import Session
from database.session import get_db
from models.user import User
from crud.user_crud import user_crud
from config.settings import settings
from typing import Annotated

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
cred_exception = HTTPException(status.HTTP_401_UNAUTHORIZED, detail="登录失效,请重新登录", headers={"WWW-Authenticate":"Bearer"})

# 生成令牌
def create_token(data: dict):
    expire = timedelta(minutes=settings.token_expire_min)
    to_encode = data.copy()
    to_encode.update({"exp": timedelta.utcnow() + expire})
    return jwt.encode(to_encode, settings.secret_key, algorithm="HS256")

# 解析token
async def parse_token(token: Annotated[str, Depends(oauth2_scheme)]):
    try:
        payload = jwt.decode(token, settings.secret_key, algorithms=["HS256"])
        username = payload.get("sub")
        if not username:
            raise cred_exception
        return username
    except JWTError:
        raise cred_exception

# 获取当前登录用户
async def get_current_user(
    db: Annotated[Session, Depends(get_db)],
    username: Annotated[str, Depends(parse_token)]
):
    user = user_crud.get_by_username(db, username)
    if not user:
        raise cred_exception
    return user

# 校验账号未禁用
async def get_active_user(user: Annotated[User, Depends(get_current_user)]):
    if user.disabled:
        raise HTTPException(status_code=403, detail="账号已禁用")
    return user

4.10 crud/user_crud 业务CRUD

python 复制代码
from crud.base import BaseCRUD
from models.user import User
from schemas.user_schema import UserCreate
from sqlmodel import Session, select

class UserCRUD(BaseCRUD[User, UserCreate]):
    # 根据用户名查询
    def get_by_username(self, db: Session, username: str):
        return db.exec(select(User).where(User.username == username)).first()

user_crud = UserCRUD(User)

4.11 routers/auth_router 注册登录接口

python 复制代码
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import Session
from database.session import get_db
from schemas.user_schema import UserCreate
from crud.user_crud import user_crud
from dependencies.auth import create_token
from passlib.context import CryptContext
from common.response import ApiResp, success_resp

pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
auth_router = APIRouter(prefix="/api/auth", tags=["登录注册"])

# 用户注册
@auth_router.post("/register", response_model=ApiResp)
def register(user_in: UserCreate, db: Session = Depends(get_db)):
    exist = user_crud.get_by_username(db, user_in.username)
    if exist:
        raise HTTPException(400, "用户名已存在")
    # 密码加密
    hash_pwd = pwd_ctx.hash(user_in.password)
    db_user = User.model_validate(user_in, update={"hashed_password": hash_pwd})
    db.add(db_user)
    db.commit()
    return success_resp(msg="注册成功")

# 登录获取token
@auth_router.post("/login")
def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
    user = user_crud.get_by_username(db, form.username)
    if not user or not pwd_ctx.verify(form.password, user.hashed_password):
        raise HTTPException(401, "用户名或密码错误")
    token = create_token({"sub": user.username})
    return {"access_token": token, "token_type": "bearer"}

4.12 routers/user_router 带鉴权数据库CRUD接口

python 复制代码
from fastapi import APIRouter, Depends, Query
from sqlmodel import Session, select
from sqlalchemy.orm import selectinload
from database.session import get_db
from dependencies.auth import get_active_user
from models.user import User
from schemas.user_schema import UserPublic, UserUpdate
from crud.user_crud import user_crud
from common.response import ApiResp, PageData, success_resp

user_router = APIRouter(prefix="/api/user", tags=["用户管理"], dependencies=[Depends(get_active_user)])

# 获取当前登录用户信息
@user_router.get("/me", response_model=ApiResp[UserPublic])
def get_my_info(user: User = Depends(get_active_user)):
    return success_resp(data=user)

# 用户分页列表(预加载地址解决N+1)
@user_router.get("/list", response_model=ApiResp[PageData[UserPublic]])
def list_user(
    page: int = Query(1, ge=1),
    page_size: int = Query(10, le=50),
    db: Session = Depends(get_db)
):
    stmt = select(User).options(selectinload(User.addresses))
    offset = (page - 1) * page_size
    items = db.exec(stmt.offset(offset).limit(page_size)).all()
    total = db.exec(select(func.count(User.id))).scalar()
    page_data = PageData(page=page, page_size=page_size, total=total, items=items)
    return success_resp(page_data)

# 修改个人信息
@user_router.put("/update", response_model=ApiResp[UserPublic])
def update_user(
    update_in: UserUpdate,
    user: User = Depends(get_active_user),
    db: Session = Depends(get_db)
):
    update_dict = update_in.model_dump(exclude_unset=True)
    new_user = user_crud.update(db, user, update_dict)
    return success_resp(new_user)

# 删除账号
@user_router.delete("/del/{uid}", response_model=ApiResp)
def del_user(uid: int, db: Session = Depends(get_db)):
    obj = user_crud.delete(db, uid)
    if not obj:
        return fail_resp("用户不存在", 404)
    return success_resp(msg="删除成功")

4.13 main.py 项目入口

python 复制代码
from fastapi import FastAPI, CORSMiddleware
from sqlmodel import SQLModel
from database.session import engine
from routers import auth_router, user_router
from config.settings import settings

# 开发环境临时建表,生产禁用,使用Alembic
def create_dev_tables():
    if settings.app_env == "dev":
        SQLModel.metadata.create_all(bind=engine)

app = FastAPI(title="FastAPI+SQLModel综合项目", version="1.0")

# 跨域配置
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"]
)

# 启动事件
@app.on_event("startup")
def startup():
    create_dev_tables()

# 挂载路由
app.include_router(auth_router)
app.include_router(user_router)

@app.get("/")
def index():
    return {"msg":"项目启动成功,访问 /docs 调试接口"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("main:app", reload=True, host="127.0.0.1", port=8000)

五、Alembic数据库迁移适配(生产必备)

5.1 初始化迁移

bash 复制代码
alembic init alembic

5.2 修改alembic/env.py

导入全部models,绑定SQLModel.metadata,读取配置文件数据库地址,禁止硬编码URL。

5.3 标准迁移流程

  1. 修改models表结构

  2. 生成迁移脚本:alembic revision --autogenerate -m "新增用户手机号字段"

  3. 执行升级:alembic upgrade head

  4. 出错回滚:alembic downgrade -1

生产环境删除main.py内create_dev_tables函数,禁止自动建表。

六、阶段综合实战流程

  1. 启动项目访问/docs;

  2. 调用注册接口创建账号;

  3. 使用账号密码登录获取Bearer Token;

  4. 携带token调用个人信息、分页列表、更新、删除接口;

  5. 数据库自动生成user、address两张关联表;

  6. 修改模型后使用Alembic生成版本脚本管理表变更。

七、阶段核心总结

  1. 标准五层工程目录:配置/数据库/模型/DTO/CRUD/路由完全解耦;

  2. 数据库核心:pydantic-settings多环境配置、yield会话自动释放连接;

  3. 复用SQLModel能力:table实体、DTO分层、一对多关系、selectinload优化N+1;

  4. 通用BaseCRUD大幅减少重复数据库代码,分页/新增/删除统一封装;

  5. 业务接口统一搭配JWT鉴权依赖,未登录自动拦截;

  6. 生产标准:废弃create_all,使用Alembic版本迁移管理表结构。

八、新手避坑指南

  1. ❌ 不用yield,手动创建Session忘记close,造成连接池耗尽;

  2. ❌ 不分层,数据库实体直接返回前端泄露密码;

  3. ❌ 关联列表查询不加selectinload,线上大量N+1慢查询;

  4. ❌ 生产环境保留create_all,表结构更新丢失字段;

  5. ❌ 硬编码数据库地址、JWT密钥,多环境切换繁琐;

  6. ✅ 统一BaseCRUD封装通用数据库逻辑;

  7. ✅ 所有需要登录路由统一挂载全局鉴权依赖;

  8. ✅ 开发仅临时使用自动建表,上线全部迁移脚本。

相关推荐
xxie1237941 小时前
Python 闭包的调用方法与实践
开发语言·python
HZZD_HZZD1 小时前
用电行为异常检测VAE-基于PyTorch设计用电行为异常检测模型:从时序特征提取到变分自编码器部署的完整实战
人工智能·pytorch·python
思-无-涯1 小时前
AI Agent技能编写与质量保障
人工智能·python
2601_956319882 小时前
2026年下半年AI量化学习,分清表达开发和验证
人工智能·python
CaffeinePro2 小时前
FastAPI自动接口文档定制与美化、权限管控
后端·fastapi
CTA量化套保3 小时前
最新AI量化效率提升,用示例拆解练习压实路径
人工智能·python
zhiSiBuYu05173 小时前
混合检索实战指南:关键词与向量的完美融合
人工智能·python·机器学习
weixin_413063213 小时前
复现 MatchED 边缘检测模型(单张图片重复8次,训练200 epoch)
python·算法·计算机视觉·边缘检测模型
许彰午3 小时前
74_Python自动化办公之Excel操作
python·自动化·excel