FastAPI、Tortoise ORM 与 PostgreSQL 三件套 是否好用呢?

后端开发这些年,Python 生态里冒出过不少 ORM 框架,但真正能跟 FastAPI 的异步基因严丝合缝对上的,Tortoise ORM 算是头一份。它天生支持 async/await,写法上又刻意向 Django ORM 靠拢,让很多从 Django 转过来的开发者几乎零成本上手。搭配 PostgreSQL 这种功能完备、社区活跃的关系型数据库,这套组合拳在中小型项目乃至部分中大型项目里已经成了相当主流的选择 、。

下面这份报告会把这套技术栈拆开揉碎讲清楚,从架构原理到核心概念,再到工程落地的具体写法,尽量让刚接触后端开发的同学也能看明白。


一、整体架构 这三者是怎么协同工作的

在动手写代码之前,先搞清楚请求进来之后经历了什么,会让后面的学习事半功倍。简单说,FastAPI 负责接收 HTTP 请求、做参数校验、路由分发;Tortoise ORM 充当中间的翻译官,把 Python 对象操作转换成 SQL 语句;PostgreSQL 则是最终存数据的地方。

flowchart LR A[客户端请求] --> B[FastAPI 路由层] B --> C{Pydantic 参数校验} C -->|校验通过| D[业务逻辑处理] D --> E[Tortoise ORM 模型层] E --> F[生成 SQL 语句] F --> G[(PostgreSQL 数据库)] G --> F F --> E E --> H[Pydantic 序列化] H --> I[返回 JSON 响应]

整条链路里最关键的一点是,从 FastAPI 接收请求到 Tortoise 操作数据库,全程都是异步非阻塞的。这意味着当一个请求在等数据库返回结果时,服务器可以腾出手来处理别的请求,而不是傻等着------这也是这套组合在高并发场景下能打的核心原因 。


二、Tortoise ORM 的核心概念

想用好 Tortoise,得先把几个基础概念吃透。它们环环相扣,缺一不可。

graph TD A[Model 模型类] --> B[Field 字段] A --> C[Meta 元信息] A --> D[Relations 关系] D --> E[ForeignKeyField 外键] D --> F[ManyToManyField 多对多] D --> G[ReverseRelation 反向关系] A --> H[QuerySet 查询集] H --> I[filter 过滤] H --> J[prefetch_related 预加载] A --> K[pydantic_model_creator] K --> L[自动生成 Pydantic Schema]

1 Model 模型 数据表的映射蓝图

Tortoise 里每一个 Model 类都对应数据库里的一张表,写法跟 Django 几乎一模一样,继承自 tortoise.models.Model,然后用类属性声明字段。

python 复制代码
from tortoise import fields
from tortoise.models import Model


class User(Model):
    id = fields.IntField(pk=True)
    username = fields.CharField(max_length=50, unique=True)
    email = fields.CharField(max_length=100)
    is_active = fields.BooleanField(default=True)
    created_at = fields.DatetimeField(auto_now_add=True)

    class Meta:
        table = "users"

    def __str__(self):
        return self.username

Meta 内部类用来配置表名、排序规则等元信息,fields 模块则提供了 IntFieldCharFieldTextFieldBooleanFieldDatetimeFieldJSONField 等一整套字段类型,基本能覆盖日常业务需求 、。

2 Field 字段 数据类型与约束的载体

每个字段除了定义数据类型,还能配置默认值、是否可空、是否唯一、索引规则等约束条件。比如 unique=True 会在数据库层面加上唯一索引,index=True 会给字段建索引来加速查询。这些配置最终都会体现在生成的建表 SQL 里,所以设计模型的时候多花点心思在字段约束上,往往能省掉后期数据清洗的大麻烦。

3 关系字段 表与表之间怎么连起来

这是 ORM 里最容易绕晕人的部分,但理解了原理其实很直观。

对应的代码写法是这样的:

python 复制代码
class Post(Model):
    id = fields.IntField(pk=True)
    title = fields.CharField(max_length=200)
    content = fields.TextField()
    author = fields.ForeignKeyField(
        "models.User", related_name="posts", on_delete=fields.CASCADE
    )
    created_at = fields.DatetimeField(auto_now_add=True)

    class Meta:
        table = "posts"

ForeignKeyField 建立一对多关系,通过 related_name 参数在 User 那一侧生成反向查询入口,也就是 user.posts 能拿到某个用户的所有文章。除此之外还有 ManyToManyField 用来处理多对多关系,比如文章和标签的场景,Tortoise 会自动帮你创建中间关联表 。

4 QuerySet 查询集 数据操作的核心接口

QuerySet 是 Tortoise 里执行查询的核心机制,支持链式调用,写法非常接近自然语言。

python 复制代码
# 简单查询
user = await User.get(id=1)

# 条件过滤 加排序 加分页
users = await User.filter(is_active=True).order_by("-created_at").limit(10).offset(0)

# 模糊匹配
users = await User.filter(username__icontains="tom")

# 预加载关联数据 避免 N+1 查询问题
user = await User.get(id=1).prefetch_related("posts")

# 聚合统计
total = await User.filter(is_active=True).count()

# 安全获取 不存在时返回 None 而非抛异常
user = await User.get_or_none(id=999)

这里特别值得一提的是 prefetch_related 这个方法。如果不用它,查询关联对象时容易踩到经典的 N+1 查询陷阱------查一遍用户列表,再对每个用户单独查一次文章,SQL 语句数量瞬间爆炸。prefetch_related 会一次性把关联数据拉回来,性能上是质的提升 。

5 Pydantic 序列化 从数据库对象到 API 响应

FastAPI 的接口响应格式依赖 Pydantic 模型来定义 Schema,Tortoise 贴心地提供了 pydantic_model_creator 工具,能直接根据已有的 Model 自动生成对应的 Pydantic 类,省去了手动重复定义字段的麻烦。

python 复制代码
from tortoise.contrib.pydantic import pydantic_model_creator

# 用于响应的完整字段模型
User_Pydantic = pydantic_model_creator(User, name="User")

# 用于创建请求的输入模型 排除只读字段如 id created_at
UserIn_Pydantic = pydantic_model_creator(User, name="UserIn", exclude_readonly=True)

这一招省下的重复代码量相当可观,尤其是模型字段多、迭代频繁的项目,改一次 Model 定义,Schema 跟着自动同步,不用两边手动对齐 。


三、在 FastAPI 工程里具体怎么用

搞清楚了核心概念,接下来看看完整的工程落地流程。

第一步 注册数据库连接

Tortoise 提供了 register_tortoise 这个封装函数,专门用来跟 FastAPI 的生命周期挂钩,应用启动时自动建立连接池,应用关闭时自动释放。

python 复制代码
from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise

app = FastAPI(title="示例服务")

register_tortoise(
    app,
    db_url="postgres://user:password@localhost:5432/mydb",
    modules={"models": ["app.models"]},
    generate_schemas=True,      # 启动时自动建表 仅推荐用于开发环境
    add_exception_handlers=True,  # 自动处理 DoesNotExist 等异常
)

需要提醒一句,generate_schemas=True 这个选项开发调试很方便,但生产环境千万别用,正经项目应该走迁移工具来管理表结构变更 。

第二步 编写路由 完成增删改查

python 复制代码
from fastapi import FastAPI, HTTPException
from app.models import User
from app.schemas import User_Pydantic, UserIn_Pydantic

app = FastAPI()


@app.post("/users", response_model=User_Pydantic)
async def create_user(user: UserIn_Pydantic):
    user_obj = await User.create(**user.dict(exclude_unset=True))
    return await User_Pydantic.from_tortoise_orm(user_obj)


@app.get("/users/{user_id}", response_model=User_Pydantic)
async def get_user(user_id: int):
    user = await User.get_or_none(id=user_id)
    if not user:
        raise HTTPException(status_code=404, detail="用户不存在")
    return await User_Pydantic.from_tortoise_orm(user)


@app.put("/users/{user_id}", response_model=User_Pydantic)
async def update_user(user_id: int, user: UserIn_Pydantic):
    await User.filter(id=user_id).update(**user.dict(exclude_unset=True))
    updated = await User.get(id=user_id)
    return await User_Pydantic.from_tortoise_orm(updated)


@app.delete("/users/{user_id}")
async def delete_user(user_id: int):
    deleted_count = await User.filter(id=user_id).delete()
    if not deleted_count:
        raise HTTPException(status_code=404, detail="用户不存在")
    return {"message": "删除成功"}

这段代码把 CRUD 的完整流程走了一遍,可以看到每个数据库操作前面都带着 await,这正是异步 ORM 的精髓所在。

第三步 用 Aerich 管理数据库迁移

前面提到生产环境不能靠 generate_schemas 自动建表,正规做法是用 Aerich 这个专门为 Tortoise 打造的迁移工具,功能定位类似 SQLAlchemy 生态里的 Alembic 。

bash 复制代码
# 安装
pip install aerich

# 初始化配置 指向你的 TORTOISE_ORM 配置字典
aerich init -t app.settings.TORTOISE_ORM

# 初始化数据库 生成首个迁移文件
aerich init-db

# 模型变更后 生成新的迁移脚本
aerich migrate --name add_phone_field

# 应用迁移到数据库
aerich upgrade

值得一提的是,Tortoise ORM 从 1.0.0 版本开始内置了原生迁移系统,功能上正在逐步替代 Aerich,但目前 Aerich 生态更成熟,社区文档更丰富,绝大多数生产项目仍在用它 、。


四、这套技术栈的特点 优势与局限都摆出来看

突出的优势

异步原生这一条几乎是决定性的。Tortoise 从底层设计就是围绕 async/await 展开的,跟 FastAPI 的异步路由处理器配合起来毫无违和感,不需要像某些同步 ORM 那样借助线程池去模拟异步效果 。

学习曲线平缓 是另一大加分项。API 设计大量借鉴 Django ORM 的思路,filtergetcreateupdate 这些方法名和用法几乎照搬,Django 背景的开发者基本可以无缝迁移过来 。

Pydantic 深度集成 省掉了大量样板代码。前面提到的 pydantic_model_creator 能直接从 ORM 模型生成 API Schema,这在需要频繁调整字段的项目里体验相当丝滑。

需要留意的短板

生态成熟度上,Tortoise 相比 SQLAlchemy 还是年轻不少,第三方插件、社区案例、疑难杂症的解决方案数量都要少一截。碰到比较刁钻的复杂查询场景,有时候不得不写原生 SQL 来兜底,而不是完全依赖 ORM 的高级查询语法 、。

另外多对多关系和复杂聚合查询的支持力度,跟 SQLAlchemy 那种打磨了十几年的老牌框架相比,细节处理上还有差距。有开发者在实际项目里反馈过,遇到多表联查、复杂聚合统计的场景,Tortoise 的表达能力会稍显吃力 。

三种主流方案横向对比

对比维度 Tortoise ORM SQLAlchemy(异步模式) Django ORM
异步支持 原生支持 需搭配 asyncio 扩展 部分支持 较新版本
学习曲线 平缓 类 Django 风格 较陡 概念体系庞大 平缓
FastAPI 集成度 高 官方提供专用封装 中 需自行封装依赖注入 低 通常不搭配使用
迁移工具 Aerich 或内置迁移 Alembic 成熟稳定 内置 migrations
复杂查询能力 中等 强 表达能力丰富 中等
生态成熟度 中等 仍在快速发展 高 industry standard 高 但绑定 Django 框架

五、总结 这套组合适合什么样的项目

把这三样东西拼在一起用,本质上是在开发效率运行性能之间找到了一个相当讨喜的平衡点。如果项目对高并发有明确诉求,团队规模不算太大,希望尽快搭出一个结构清晰、维护友好的后端服务,FastAPI 加 Tortoise ORM 加 PostgreSQL 这套组合是相当值得考虑的选择。

但假如项目本身查询逻辑异常复杂,涉及大量跨表聚合、窗口函数、复杂子查询等高级 SQL 特性,那么可能还是得掂量一下是否要引入 SQLAlchemy,或者干脆在关键路径上直接手写原生 SQL 来配合 Tortoise 使用,两头兼顾往往才是更务实的工程决策。

技术选型这件事,没有放之四海皆准的标准答案,关键还是看团队熟悉程度、业务复杂度和长期维护成本这几个因素怎么权衡。


参考资料

1 Tortoise ORM 官方文档 FastAPI 集成指南 tortoise.github.io/contrib/fas...

2 Tortoise ORM 官方文档 Models 模型定义详解 tortoise.github.io/models.html

3 Tortoise ORM 官方文档 Query API 查询接口说明 tortoise.github.io/query.html

4 Aerich 官方仓库 数据库迁移工具说明 github.com/tortoise/ae...

5 Tortoise ORM 官方文档 内置迁移系统说明 tortoise.github.io/migration.h...

6 Tortoise ORM 官方文档 Pydantic 序列化指南 tortoise.github.io/contrib/pyd...

7 Medium 技术博客 Choosing the Right ORM for Your FastAPI Project medium.com/@marcnealer...

8 Reddit 社区讨论 ORM for FastAPI PostgreSQL Tortoise or SQLAlchemy www.reddit.com/r/Python/co...

9 Tessl Registry Tortoise ORM Models 文档镜像 tessl.io/registry/te...

10 Stack Overflow 问答 No DB associated to model problem FastAPI and Tortoise ORM Aerich stackoverflow.com/questions/6...

相关推荐
再吃一根胡萝卜1 小时前
从 Docker 到 Kubernetes:微服务的“操作系统”长什么样?
后端
龙虾PRO1 小时前
2026 DeepSeek Harness 部署完整教程:npx 一键启动至 Python SDK 全流程接入
开发语言·python
程序员爱钓鱼1 小时前
Rust Trait详解:定义共享行为与抽象接口
后端·面试·rust
再吃一根胡萝卜1 小时前
分布式事务:从“强一致”到“最终一致”,我为什么在微服务里放弃了 2PC?
后端
再吃一根胡萝卜1 小时前
从 Django 到 Spring Cloud:一个全栈开发者的微服务思考
后端
程序员爱钓鱼1 小时前
Go 编程实战:切片 Slice——灵活的动态数据集合
后端·面试·go
再吃一根胡萝卜8 小时前
微服务治理的“四大护法”:从 Django 视角理解 Spring Cloud 核心组件
后端
再吃一根胡萝卜8 小时前
面试终极挑战:如何用 Django 经验,回答 Java 微服务实战问题?
后端
To_OC9 小时前
装完 ESLint 它一声不吭?我还以为代码写得多好
后端·node.js·eslint