零基础入门python48:FastAPI SQLAlchemy模型与会话
2026/9/3 8:01:04 网站建设 项目流程

零基础入门python48:FastAPI SQLAlchemy模型与会话

一、上一篇课后练习讲解

上一篇练习围绕“Pydantic请求响应模型”。参考做法是先运行上一篇的测试,再用一个成功请求和一个失败请求验证边界;本篇在同一项目上增加新能力。

上一篇课后练习完整答案

上一篇练习已经落实到完整文件,运行下面代码可以观察本篇要求的成功和失败状态;数据库写入全部放在明确事务边界内。
答案要点:Pydantic ArticleCreate 限制标题和正文长度,ArticleRead 使用 from_attributes 输出稳定 JSON,非法输入自动 422。

完整答案文件:app/schemas.py

完整参考答案文件

完整文件:app/schemas.py

frompydanticimportBaseModel,FieldclassArticleCreate(BaseModel):title:str=Field(min_length=1,max_length=120)content:str=Field(min_length=1)classArticleRead(ArticleCreate):id:intmodel_config={"from_attributes":True}

验收:运行项目测试(FastAPI/Flask 使用 python -m pytest -q,Django 使用 python manage.py test),再按本文 curl 或 Docker 命令检查预期状态码。

完整参考答案文件

下面是交付项目中真实存在的完整文件 fastapi-blog/app/schemas.py。它覆盖本篇新增逻辑以及前文已经完成的依赖代码;复制单个函数会丢失上下文,因此这里提供整份文件。

fromdatetimeimportdatetimefrompydanticimportBaseModel,ConfigDict,EmailStr,Field,field_validatorclassUserCreate(BaseModel):email:EmailStr password:str=Field(min_length=8,max_length=128)classUserRead(BaseModel):model_config=ConfigDict(from_attributes=True)id:intemail:strclassToken(BaseModel):access_token:strtoken_type:str='bearer'classArticleCreate(BaseModel):title:str=Field(min_length=2,max_length=120)content:str=Field(min_length=10,max_length=20000)@field_validator('title')@classmethoddefclean_title(cls,value):returnvalue.strip()classArticleRead(ArticleCreate):model_config=ConfigDict(from_attributes=True)id:intauthor_id:intcreated_at:datetimeclassCommentCreate(BaseModel):content:str=Field(min_length=1,max_length=500)classWritingRequest(BaseModel):content:str=Field(min_length=20,max_length=10000)

FastAPI 会在进入路由前完成 JSON 解析和字段校验。default_factory=list避免多个请求共享同一个可变默认列表;这是 Python 对象生命周期的实际坑。

二、本篇要解决的真实问题

声明 User、Article 等表,Session 负责一次请求的查询和提交;请求结束必须关闭 Session。

三、请求是怎样走完整条链路的

客户端请求 → 路由匹配 → Pydantic 校验 → Depends 注入用户/Session → SQLAlchemy 查询或业务服务 → 提交事务 → 响应模型序列化。每一步都有明确责任,排错时按这个顺序定位。

四、先看一个最小代码片段

@router.get("/api/health")defhealth():return{"status":"ok"}

这里的注释不是装饰:它说明数据从哪里来、为什么不能相信客户端,以及失败时系统应保持什么状态。

五、完整文件代码

app/models.py(当前阶段完整文件)

fromdatetimeimportdatetime,timezonefromsqlalchemyimportForeignKey,String,Text,UniqueConstraintfromsqlalchemy.ormimportMapped,mapped_column,relationshipfrom.databaseimportBaseclassUser(Base):__tablename__='users'id:Mapped[int]=mapped_column(primary_key=True)email:Mapped[str]=mapped_column(String(120),unique=True,index=True)password_hash:Mapped[str]=mapped_column(String(255))created_at:Mapped[datetime]=mapped_column(default=lambda:datetime.now(timezone.utc))classArticle(Base):__tablename__='articles'id:Mapped[int]=mapped_column(primary_key=True)title:Mapped[str]=mapped_column(String(120),index=True)content:Mapped[str]=mapped_column(Text)author_id:Mapped[int]=mapped_column(ForeignKey('users.id'),index=True)created_at:Mapped[datetime]=mapped_column(default=lambda:datetime.now(timezone.utc))comments:Mapped[list['Comment']]=relationship(cascade='all, delete-orphan')classComment(Base):__tablename__='comments'id:Mapped[int]=mapped_column(primary_key=True)content:Mapped[str]=mapped_column(String(500))article_id:Mapped[int]=mapped_column(ForeignKey('articles.id'))user_id:Mapped[int]=mapped_column(ForeignKey('users.id'))classLike(Base):__tablename__='likes'__table_args__=(UniqueConstraint('article_id','user_id'),)id:Mapped[int]=mapped_column(primary_key=True)article_id:Mapped[int]=mapped_column(ForeignKey('articles.id'))user_id:Mapped[int]=mapped_column(ForeignKey('users.id'))

阅读方法:先找路由函数,再找它的 Depends、输入 schema、数据库操作和 response_model;这五处合起来才是一个功能。

app/database.py(当前阶段完整文件)

fromsqlalchemyimportcreate_enginefromsqlalchemy.ormimportDeclarativeBase,sessionmaker DATABASE_URL='sqlite:///./blog.db'engine=create_engine(DATABASE_URL,connect_args={'check_same_thread':False})SessionLocal=sessionmaker(bind=engine,autoflush=False,expire_on_commit=False)classBase(DeclarativeBase):passdefget_db():db=SessionLocal()try:yielddbfinally:db.close()

阅读方法:先找路由函数,再找它的 Depends、输入 schema、数据库操作和 response_model;这五处合起来才是一个功能。

六、安装、启动与验收

..\..\.venv\Scripts\python.exe-m pip install-r requirements.txt..\..\.venv\Scripts\python.exe-m pytest-q..\..\.venv\Scripts\python.exe-m uvicorn app.main:app--reload

打开/docs,按顺序完成注册、登录、创建文章、分页查询、修改删除、评论点赞。错误请求必须看到明确状态码:校验错误 422,未登录 401,无权限 403,重复点赞 409。测试应全部通过。

七、本篇验收清单

  • 能指出输入校验发生在哪个 schema。
  • 能解释 Session 何时提交、何时回滚。
  • 能用第二个用户验证资源隔离。
  • 能复现一个失败场景并说明原因。

八、课后练习

围绕“SQLAlchemy模型与会话”新增一个测试用例,写出请求、预期响应和断言;下一篇开头会给出参考实现,并继续使用本项目。

五、Session 生命周期和事务边界

一个请求拿一个 Session,请求结束立即关闭。Session 不是全局连接,也不是缓存;它跟踪当前事务中的对象。全局 Session 会让并发请求互相看到未提交状态,并最终耗尽连接。

fromsqlalchemyimportcreate_enginefromsqlalchemy.ormimportDeclarativeBase,sessionmaker DATABASE_URL="sqlite:///./blog.db"engine=create_engine(DATABASE_URL,connect_args={"check_same_thread":False})SessionLocal=sessionmaker(bind=engine,autoflush=False,expire_on_commit=False)classBase(DeclarativeBase):passdefget_db():"""无论路由成功还是异常,finally 都关闭当前请求的 Session。"""db=SessionLocal()try:yielddbfinally:db.close()

提交失败必须 rollback:

try:db.add(row)db.commit()exceptIntegrityError:db.rollback()raiseHTTPException(status_code=409,detail="数据已经存在")

在 create_article 中故意在 commit 前抛异常,数据库不应出现半条文章;在 commit 后抛异常则已经持久化,说明函数结束和事务提交不是一回事。SQLite 的 check_same_thread=False 只是允许测试客户端跨线程访问,不代表生产并发配置。

六、当前项目中的完整文件(对照阅读)

七、用一个失败事务理解 Session

向 likes 表插入重复记录后不要立即再执行查询,先 rollback 再查询。可以在 Python shell 中打印 db.in_transaction(),观察 commit 前后状态。把异常处理写在最靠近 commit 的位置,服务层向上只抛业务可理解的异常。

查询列表时使用 db.scalars(stmt).all(),单行查询使用 db.scalar 或 db.get;不要为了统一而把所有结果都取成 list。返回 ORM 对象前确认 Session 仍然打开,否则延迟加载关系会出现 DetachedInstanceError。

八、补充代码文件

本篇结束:完整模块文件

本节不是代码片段,而是本篇结束时该模块的完整版本。请先备份旧文件,再整体替换;替换后重新运行本篇命令和测试。阅读时重点看本篇新增的函数、事务边界和错误处理,未涉及的代码先不要自行删减。

app/models.py

fromdatetimeimportdatetime,timezonefromsqlalchemyimportForeignKey,String,Text,UniqueConstraintfromsqlalchemy.ormimportMapped,mapped_column,relationshipfrom.databaseimportBaseclassUser(Base):__tablename__='users'id:Mapped[int]=mapped_column(primary_key=True)email:Mapped[str]=mapped_column(String(120),unique=True,index=True)password_hash:Mapped[str]=mapped_column(String(255))created_at:Mapped[datetime]=mapped_column(default=lambda:datetime.now(timezone.utc))classArticle(Base):__tablename__='articles'id:Mapped[int]=mapped_column(primary_key=True)title:Mapped[str]=mapped_column(String(120),index=True)content:Mapped[str]=mapped_column(Text)author_id:Mapped[int]=mapped_column(ForeignKey('users.id'),index=True)created_at:Mapped[datetime]=mapped_column(default=lambda:datetime.now(timezone.utc))comments:Mapped[list['Comment']]=relationship(cascade='all, delete-orphan')classComment(Base):__tablename__='comments'id:Mapped[int]=mapped_column(primary_key=True)content:Mapped[str]=mapped_column(String(500))article_id:Mapped[int]=mapped_column(ForeignKey('articles.id'))user_id:Mapped[int]=mapped_column(ForeignKey('users.id'))classLike(Base):__tablename__='likes'__table_args__=(UniqueConstraint('article_id','user_id'),)id:Mapped[int]=mapped_column(primary_key=True)article_id:Mapped[int]=mapped_column(ForeignKey('articles.id'))user_id:Mapped[int]=mapped_column(ForeignKey('users.id'))

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询