你是不是也经历过这种纠结:想用 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 那种「浑然一体」的感觉。但工具再好,也得多写多试。希望这篇实战笔记能帮你绕过我当年踩过的坑,早点下班!
如果你在项目中遇到了其他奇葩问题,欢迎评论区留言,咱们一起吐槽一起解决~
🎁 老朋友的经验,不点赞收藏可就亏大了