作为一名大三的 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)

在我修正完主要错误后,又碰到了两个警告:

  1. PydanticDeprecatedSince20class Config 写法过时,需要改用 model_config = ConfigDict(from_attributes=True)
    → 我查了官方迁移指南,发现这是 V2 的 breaking change,顺手改成了新写法。

  2. 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 都是最好的老师。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐