文章目录
-
- 前言
- 一、阶段学习目标
- 二、整体项目分层目录
- 三、环境依赖安装
- 四、分步完整代码实现
-
- [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登录鉴权。
一、阶段学习目标
-
搭建FastAPI + SQLModel标准分层项目目录;
-
使用pydantic-settings管理多环境数据库配置;
-
封装全局Engine与yield数据库会话依赖(生产标准写法);
-
通用BaseCRUD父类封装,复用增删改查、分页逻辑;
-
分层设计数据库实体models、入出参DTO schemas;
-
实现一对多关联查询、嵌套返回脱敏数据;
-
整合JWT鉴权,实现登录用户操作数据库接口;
-
对接Alembic迁移工具,线上禁用create_all;
-
完整实战:用户注册登录、个人信息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 标准迁移流程
-
修改models表结构
-
生成迁移脚本:
alembic revision --autogenerate -m "新增用户手机号字段" -
执行升级:
alembic upgrade head -
出错回滚:
alembic downgrade -1
生产环境删除main.py内create_dev_tables函数,禁止自动建表。
六、阶段综合实战流程
-
启动项目访问/docs;
-
调用注册接口创建账号;
-
使用账号密码登录获取Bearer Token;
-
携带token调用个人信息、分页列表、更新、删除接口;
-
数据库自动生成user、address两张关联表;
-
修改模型后使用Alembic生成版本脚本管理表变更。
七、阶段核心总结
-
标准五层工程目录:配置/数据库/模型/DTO/CRUD/路由完全解耦;
-
数据库核心:pydantic-settings多环境配置、yield会话自动释放连接;
-
复用SQLModel能力:table实体、DTO分层、一对多关系、selectinload优化N+1;
-
通用BaseCRUD大幅减少重复数据库代码,分页/新增/删除统一封装;
-
业务接口统一搭配JWT鉴权依赖,未登录自动拦截;
-
生产标准:废弃create_all,使用Alembic版本迁移管理表结构。
八、新手避坑指南
-
❌ 不用yield,手动创建Session忘记close,造成连接池耗尽;
-
❌ 不分层,数据库实体直接返回前端泄露密码;
-
❌ 关联列表查询不加selectinload,线上大量N+1慢查询;
-
❌ 生产环境保留create_all,表结构更新丢失字段;
-
❌ 硬编码数据库地址、JWT密钥,多环境切换繁琐;
-
✅ 统一BaseCRUD封装通用数据库逻辑;
-
✅ 所有需要登录路由统一挂载全局鉴权依赖;
-
✅ 开发仅临时使用自动建表,上线全部迁移脚本。