如何避免0到1阶段使用FastAPI、PostgreSQL和Tortoise ORM的常见坑?

摘要:本文分享了 FastAPI 搭配 PostgreSQL 使用 Tortoise ORM 的完整实战经验,包括模型设计、数据迁移(aerich)、常见问题及解决方案,帮助你在异步世界中优雅地操作数据库。
你是不是也经历过这种纠结:想用 FastAPI 写个带数据库的项目,却在 SQLAlchemy 和 Tortoise ORM 之间反复横跳? 欢迎新老朋友👋!作为一名在代码堆里摸爬滚打多年的全栈程序媛,今天咱们就聊聊 FastAPI + PostgreSQL + Tortoise ORM 这套组合拳。我会把我自己踩过的坑、修复的数据迁移事故,全都摊开来跟你讲。这不是官方文档的复述,而是一份可以直接拿来用的「避坑实战笔记」。
🎯 本文能帮你解决什么? ✅ 快速搭好 FastAPI + PostgreSQL 的项目骨架 ✅ 搞懂 Tortoise ORM 的模型定义和关系用法(附代码片段) ✅ 用 Aerich 优雅地管理数据库迁移,不再手动改表 ✅ 整合 Jinja2 模板,让 ORM 查询结果直接渲染到前端 ✅ 总结 5 个最容易翻车的坑,附解决方案 📌 主要内容脉络 🔸 为什么要选 Tortoise ORM?——异步世界里的「翻译官」 🔸 环境搭建与配置——别在第一步就摔跤 🔸 模型定义与关系——像搭积木一样建表 - 字段类型避坑指南 - 一对多、多对多实战 🔸 数据迁移 Aerich——数据库的「版本控制」 - 初始化、变更、回滚全流程 🔸 模板渲染——把数据变成页面 🔸 常见问题 & 急救包 ⚙️ 第一部分:为什么是 Tortoise ORM? 你可能会问:FastAPI 官方文档里推荐用 SQLAlchemy 啊,为什么偏要用 Tortoise? 说实话,复杂大型项目还是老老实实配 SQLAlchemy + 异步驱动,它毕竟经过了时间的沉淀,够稳。但对于新手新项目或快速原型来说,就有点像穿着皮鞋跑步——能跑,但别扭。直到我发现了 Tortoise ORM,它简直就是为异步 Python 而生的。你可以把它想象成一个「实时翻译官」,你写 Python 对象,它自动翻译成 SQL,而且全程异步非阻塞,跟 FastAPI 的 async/await 天生一对。 💡 核心优势:类 Django ORM 的语法(上手快)、全异步支持、自带分页和信号,最关键的是——配合 Aerich 做迁移,比 Alembic 在异步环境下的配置简单太多了! 🔧 第二部分:搭建项目骨架(含配置代码) 好,咱们先来搭环境。假设你已经有了 Python 3.8+ 和 PostgreSQL 实例。 # 安装依赖 pip install fastapi uvicorn[standard] tortoise-orm[asyncpg] aerich asyncpg tomlkit jinja2 这里提醒一句:如果你偶尔要跑一些同步脚本,或者用一些依赖 psycopg2 的工具(比如某些数据库管理 GUI),那装个 psycopg2-binary 也无妨。记得用 binary 版本,别给自己找编译的麻烦 😉,千万别学我当初偷懒,直接用 psycopg2 而不是 psycopg2-binary,结果部署到 Linux 上编译报错……用 binary 版本省心很多。 📁 项目结构建议 my_project/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口 │ ├── models.py # Tortoise 模型定义 │ ├── schemas.py # Pydantic 模型(可选) │ ├── routers/ # 路由 │ └── templates/ # Jinja2 模板 ├── migrations/ # Aerich 迁移目录(自动生成) ├── aerich.ini # Aerich 配置 └── tortoise_config.py # 数据库配置 ⚡ 配置 Tortoise ORM(tortoise_config.py) TORTOISE_ORM = { 'connections': { 'default': { 'engine': 'tortoise.backends.asyncpg', # PostgreSQL 异步驱动 'credentials': { 'host': 'localhost', 'port': '5432', 'user': 'postgres', 'password': 'yourpassword', 'database': 'fastapi_db', } } }, 'apps': { 'models': { 'models': ['app.models', 'aerich.models'], # 必加 aerich.models 'default_connection': 'default', } } } 🧱 第三部分:定义模型(带着感情写代码) 咱们写一个简单的博客系统的模型:用户、文章、标签。看 Tortoise 怎么用 Python 类描述表关系。 # app/models.py from tortoise import Model, fields class User(Model): id = fields.IntField(pk=True) name = fields.CharField(max_length=100) email = fields.CharField(max_length=200, unique=True) created_at = fields.DatetimeField(auto_now_add=True) class Meta: table = "users" class Article(Model): id = fields.IntField(pk=True) title = fields.CharField(max_length=200) content = fields.TextField() author = fields.ForeignKeyField('models.User', related_name='articles') tags = fields.ManyToManyField('models.Tag', related_name='articles', through='article_tag') created_at = fields.DatetimeField(auto_now_add=True) class Tag(Model): id = fields.IntField(pk=True) name = fields.CharField(max_length=50, unique=True) 看到没有?ForeignKeyField 和 ManyToManyField 的写法几乎和 Django 一样。但有个坑:related_name 必须指定,否则查询时你会摸不着头脑。还有,多对多的 through 表可以自动生成,但如果你想自定义中间表,也可以单独定义模型。 🚚 第四部分:数据迁移 Aerich(装修不改图纸) 模型定义好了,怎么应用到数据库?这就是 Aerich 出场的时候了。它就像装修时的图纸版本管理,每次改模型就生成一份迁移文件。 初始化 Aerich(只在项目开始时做一次) aerich init -t tortoise_config.TORTOISE_ORM aerich init-db 执行完后,项目里会生成 migrations 文件夹和 aerich.ini 文件。注意:TORTOISE_ORM 配置里的 'models' 必须包含 'aerich.models',否则 init-db 会报错说找不到 aerich 表。 每次修改模型后 aerich migrate --name add_user_bio # 生成迁移文件 aerich upgrade # 应用迁移到数据库 这里分享一个我踩过的坑:如果你修改了字段名,Aerich 不会自动识别字段重命名,而是先 drop 原字段再 add 新字段,导致数据丢失!所以改字段名时,最好手动编辑迁移文件,用 rename 操作。 🖥️ 第五部分:在 FastAPI 中使用 ORM(附 CRUD 示例) 在 main.py 里初始化 Tortoise,并写几个接口试试。 # app/main.py from fastapi import FastAPI, Request from tortoise.contrib.fastapi import register_tortoise from app import models # 导入模型 from tortoise_config import TORTOISE_ORM app = FastAPI() register_tortoise( app, config=TORTOISE_ORM, generate_schemas=False, # 我们使用 aerich 管理,所以关掉自动生成 add_exception_handlers=True, ) @app.get("/users") async def get_users(): users = await models.User.all().values() return {"users": users} @app.post("/users") async def create_user(name: str, email: str): user = await models.User.create(name=name, email=email) return {"id": user.id} 看,查询直接用 await,一点阻塞都没有。而且 .values() 可以直接转成字典,省去了序列化的麻烦。 🎨 第六部分:模板渲染(让数据见人) 如果你想做一个带后端的网站,而不是纯 API,可以集成 Jinja2。把数据库里查出来的用户列表渲染到 HTML 上。 # main.py 添加 from fastapi.templating import Jinja2Templates templates = Jinja2Templates(directory="app/templates") @app.get("/users-page") async def users_page(request: Request): users = await models.User.all() return templates.TemplateResponse("users.html", {"request": request, "users": users}) 在 templates/users.html 里,直接用 Tortoise 返回的模型对象,可以像 {{ user.name }} 这样访问。但注意:模板里不能使用 await,所以如果你在查询时没有预取关联字段,模板里访问关联对象会报错。解决方案:要么在视图中用 .prefetch_related(),要么在模板中使用 {% for article in user.articles %} 时确保已经加载。 🚨 第七部分:常见问题 & 急救包 问题1: 执行 aerich migrate 提示 “No changes detected” 解决: 检查模型是否在 TORTOISE_ORM 的 apps.models.models 列表中正确引入,且模型有变化(包括 Meta 类中的 table 名称修改也算)。 问题2: 数据库连接数过多,导致 “too many clients” 解决: Tortoise 默认连接池大小为 20,可以在 credentials 里设置 'max_connections': 10 限制,并确保每次请求后释放连接——其实 register_tortoise 已经帮我们管理好了生命周期,通常不用手动关。 问题3: 事务操作失败不回滚 解决: 使用 @atomic() 装饰器或 async with in_transaction() 确保原子性。记住,Tortoise 的事务是基于连接上下文的,别在事务里切换连接。 问题4: 多对多关系查询重复数据 解决: 使用 .distinct() 或者通过中间表手动查询。 问题5: 迁移时字段类型变更导致数据截断 解决: 生产环境操作前先备份,或者编写数据迁移脚本。Aerich 不支持自动数据迁移,需要手动编辑迁移文件中的 SQL。 💬 最后啰嗦一句 Tortoise ORM 真的让我在 FastAPI 项目中找回了 Django 那种「浑然一体」的感觉。但工具再好,也得多写多试。希望这篇实战笔记能帮你绕过我当年踩过的坑,早点下班! 如果你在项目中遇到了其他奇葩问题,欢迎评论区留言,咱们一起吐槽一起解决~
🎁 老朋友的经验,不点赞收藏可就亏大了