🐢 从0到1,FastAPI + PostgreSQL + Tortoise ORM 实战避坑指南

你是不是也经历过这种纠结:想用 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 那种「浑然一体」的感觉。但工具再好,也得多写多试。希望这篇实战笔记能帮你绕过我当年踩过的坑,早点下班!

如果你在项目中遇到了其他奇葩问题,欢迎评论区留言,咱们一起吐槽一起解决~


🎁 老朋友的经验,不点赞收藏可就亏大了

相关推荐
J.Kuchiki3 小时前
【PostgreSQL内核学习:修复 WindowAgg Run Condition 判断逻辑错误的优化】
数据库·学习·postgresql
高铭杰13 小时前
Postgresql源码(157)Redo系列MultiXact Redo (RM_MULTIXACT_ID = 6)
数据库·postgresql
rising start13 小时前
FastAPI进阶开发:中间件、依赖注入
中间件·fastapi·依赖注入
西柚小萌新14 小时前
【数据库】--PostgreSQL 详细安装教程
数据库·postgresql
安安爸Chris14 小时前
Ubuntu 24版本安装openclaw 3.2安装 bug:systemctl is-enabled unavailable Command failed
ubuntu·postgresql·bug
不想起名字111111118 小时前
5 分钟配置 PostgreSQL MCP:Claude Code 数据库神技
postgresql·mcp
IvorySQL18 小时前
PostgreSQL 技术日报 (3月11日)|4库合一性能提升350倍与内核新讨论
数据库·postgresql·开源
IvorySQL18 小时前
谁动了我的查询结果?PostgreSQL 联表加锁的隐藏陷阱
数据库·postgresql·开源