FastAPI + SQLAlchemy ORM 初探:那些新手必踩的坑与思考
作为一名大三的 Python 初学者,最近在尝试用 FastAPI 写一个用户管理 API,顺便练习 SQLAlchemy ORM。本以为「增删改查」很简单,结果一上来就被几个报错卡了半天。今天就把这些问题和我的思考整理出来,希望能帮到同样在踩坑的同学。
一、写之前先梳理思路
在开始写博客前,我先问自己三个问题:
-
我遇到了什么具体问题?
代码报错MappedAnnotationError,说我的UserCreate类注解有问题。 -
我当时是怎么想的?
以为UserCreate继承 SQLAlchemy 的Base就能自动映射成表,后来才知道完全理解错了。 -
现在回头看,收获了什么?
悟到了 数据库模型(ORM)和 请求/响应模型(Pydantic)是两个完全不同的东西,职责要分开。
带着这个思路,我开始写正文。
二、搭骨架,填充细节(避免回忆录)
1. 问题现场:一个让人摸不着头脑的报错
我按照网上的教程写了以下代码:
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str]
# 我以为这里也要继承 Base 才能做请求体验证
class UserCreate(Base):
username: str
password: str
然后运行 FastAPI 项目,终端直接甩给我一长串红字:
text
sqlalchemy.orm.exc.MappedAnnotationError: Type annotation for "UserCreate.username" can't be correctly interpreted for Annotated Declarative Table form.
2. 排查过程:我做了什么?
-
第一步,检查缩进和导包 —— 没问题。
-
第二步,尝试把
UserCreate里的字段加上Mapped[]—— 更奇怪了,因为UserCreate只是用来接收 POST 请求的,根本不需要映射到数据库。 -
第三步,认真读报错最后一行:“To allow Annotated Declarative to disregard legacy annotations which don't use Mapped[] to pass, set
__allow_unmapped__ = True”。
我一看就明白了:SQLAlchemy 把UserCreate当成了另一个数据库模型来解析,但它里面的username: str不符合Mapped[]的要求,所以拒绝工作。
3. 根本原因:混淆了两种「模型」
-
SQLAlchemy 模型:继承
DeclarativeBase,字段用Mapped[类型],负责定义数据库表结构。 -
Pydantic 模型:继承
BaseModel(注意是pydantic.BaseModel),字段直接用 Python 类型,负责请求/响应数据的校验和序列化。
我错误地让 UserCreate 也继承了 SQLAlchemy 的 Base,导致 SQLAlchemy 试图把它当作一张表来映射。这就像把“菜谱”和“做好的菜”混在一起 —— 概念上就错了。
4. 正确做法:各司其职
python
from pydantic import BaseModel # 重要:不是 SQLAlchemy 的 Base
class UserCreate(BaseModel):
username: str
password: str
class UserOut(BaseModel):
id: int
username: str
create_time: datetime
update_time: datetime
三、增加亮点:个人心得 + 代码验证猜想
亮点一:举一反三 —— 不止是模型继承,整个工程中都应该「关注点分离」
这个问题让我意识到,初学 Web 框架时容易犯一个毛病:总想少写几个类,复用一些看似「差不多」的东西。但 FastAPI + SQLAlchemy 的优雅恰恰在于 分层清晰:
-
User(SQLAlchemy 模型)—— 数据库层 -
UserCreate(Pydantic 模型)—— 输入层 -
UserOut(Pydantic 模型)—— 输出层
如果为了省事让他们混在一起,轻则报错,重则带来安全隐患(比如把密码字段暴露给前端)。从此我养成了一个习惯:每写一个类之前,先问自己它的职责是什么。
亮点二:写代码验证我的猜想 —— 为什么继承 BaseModel 就对了?
为了彻底弄懂,我写了一个小测试脚本(不用启动 FastAPI):
python
from pydantic import BaseModel
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class SQLABase(DeclarativeBase):
pass
class User(SQLABase):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
class UserCreate(BaseModel): # 继承 pydantic.BaseModel
username: str
print(UserCreate(username="alice")) # 正常输出 username='alice'
print(User.__table__) # 输出 Table('users', ...)
结果完全符合预期:UserCreate 只负责数据校验,User 只关心表结构。如果我把 UserCreate 换成继承 SQLABase,打印 UserCreate.__table__ 会直接报错,因为 SQLAlchemy 找不到 Mapped 注解。
这个验证过程让我对「类型注解在不同上下文中的含义」理解深了一层。
亮点三:顺带解决的两个小警告(Pydantic V2 和 Uvicorn reload)
在我修正完主要错误后,又碰到了两个警告:
-
PydanticDeprecatedSince20:
class Config写法过时,需要改用model_config = ConfigDict(from_attributes=True)。
→ 我查了官方迁移指南,发现这是 V2 的 breaking change,顺手改成了新写法。 -
You must pass the application as an import string to enable 'reload':直接传
app对象无法热重载。
→ 把uvicorn.run(app, reload=True)改成uvicorn.run("main:app", reload=True)。
这两个警告虽然不影响运行,但让我意识到:初学者不能只满足于「代码能跑」,应该留意每一个 warning。很多 warning 背后是版本升级带来的最佳实践变化,及时修正能避免以后踩更大的坑。
总结
这次踩坑经历虽然只有半天,但收获颇丰:
-
区分了 SQLAlchemy ORM 模型和 Pydantic 模型 —— 本质是「数据存储」与「数据传输」的分离。
-
学会了主动写小代码块验证自己的猜想,而不是盲目复制粘贴。
-
留意并解决了两个老旧写法导致的 warning,为以后升级依赖扫清了障碍。
如果你也是 Python 初学者,正在学习 FastAPI 和 SQLAlchemy,希望这篇博客能帮你少走一些弯路。欢迎在评论区分享你踩过的坑,我们一起进步!
最后送自己一句话:报错不可怕,可怕的是不看报错信息。每一个
Error都是最好的老师。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)