数据库实战:FastAPI + SQLAlchemy 2.0 + Alembic 从零搭建,踩坑实录

写接口一时爽,改模型火葬场?别怕,这篇手把手带你避开所有暗礁。

📌 摘要

本文以实战为导向,从零开始搭建 FastAPI + SQLAlchemy 2.0 + Alembic 的数据库层。通过生活化比喻和踩坑案例,讲透异步引擎配置、模型定义新写法、迁移脚本生成与审核等核心环节,帮你建立一套可靠、可维护的数据库操作实践。适合所有被数据库折腾过的后端开发者。


🎯 开篇:你是不是也半夜被报警吵醒过?

“叮——” 线上服务报错了:sqlalchemy.exc.StatementError。原因?可能就是顺手改了个模型字段名,却忘了生成迁移脚本,导致生产环境表结构对不上。

这种经历,我相信不少朋友都有过。尤其是从 FastAPI + SQLAlchemy 起步的时候,异步怎么配?模型怎么写?Alembic 为什么总是识别不到变更?一个坑接一个坑。今天,咱们就来聊聊,从零到一搭一套靠谱的数据库层,顺便把我踩过的坑都给你标上警示牌。🎯

🚦 先理一下咱们要干的事

🔹 第一部分:为什么是这套组合? 聊聊选型逻辑

🔹 第二部分:手把手搭环境 从配数据库到写第一个模型

🔹 第三部分:Alembic 接入与避坑 自动生成脚本的那些坑

🔹 第四部分:进阶思考 连接池、异步、以及工程化建议

🍽️ 一、为什么是 FastAPI + SQLAlchemy 2.0 + Alembic?

咱们可以把 API 比作一家餐厅:FastAPI 是那个手脚麻利的点餐员,能快速把客人的需求(HTTP请求)传给后厨;SQLAlchemy 就是后厨的食材管理员,负责管理所有食材(数据)的进出和记录;而 Alembic 则是食材管理员的变更日志本,每次新增食材或调整存储方式,都得在本子上记一笔,保证后厨和仓库一致。

SQLAlchemy 2.0 之后,语法更清爽了,但同时也带来了一些变化——比如必须用 Mapped 和 mapped_column,如果你还抱着 1.x 的写法,跑起来就会报错。别问我怎么知道的,我第一次升级项目时,整个 models.py 一片飘红。

🛠️ 二、从零开始搭环境(附代码,可复制)

📦 安装依赖

先装好这些包,注意版本:

pip install fastapi uvicorn sqlalchemy alembic asyncpg # asyncpg 是 PostgreSQL 异步驱动

这里多说一句:如果你用 MySQL,请装 aiomysql 或 asyncmy,千万别装错,否则异步引擎跑不起来。

🗂️ 项目结构

myproject/
├── app/
│ ├── __init__.py
│ ├── database.py # 引擎、会话配置
│ ├── models.py # SQLAlchemy 模型
│ └── main.py # FastAPI 应用
├── alembic.ini
└── alembic/ # 迁移目录

🔌 配置数据库连接(database.py)

这是最容易踩坑的地方。SQLAlchemy 2.0 推荐使用异步方式,但很多人照抄旧代码,配了个同步引擎,然后和 FastAPI 的异步路由打架。

from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase

DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/dbname"

# 创建异步引擎
engine = create_async_engine(DATABASE_URL, echo=True) # echo=True 会打印SQL,开发时很有用

# 创建异步会话工厂
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

# 基类,所有模型都要继承它
class Base(DeclarativeBase):
    pass

注意看,我用了 create_async_engineasync_sessionmaker,这才是异步的正确姿势。echo=True 在开发时打开,能让你看到背后执行的 SQL,但线上记得关掉,否则日志会爆炸。

🧱 定义模型(models.py)

SQLAlchemy 2.0 的新写法:用 Mappedmapped_column 替代老旧的 Column。刚开始我很不习惯,但用顺手后发现类型提示更香了。

from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(nullable=False)
    email: Mapped[str] = mapped_column(unique=True, index=True)
    age: Mapped[int | None] # 允许为空的字段,用 Optional 或 | None

注意:如果字段允许为空,类型注解必须用 | None,否则 mapped_column 会默认 nullable=False。我一开始就漏了这个,导致插入数据时一直报错。

⚡ 在 FastAPI 中使用异步会话

接下来,咱们在 main.py 里写一个依赖,每次请求生成独立的会话:

from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import AsyncSessionLocal
from app import models

app = FastAPI()

async def get_db() -> AsyncSession:
    async with AsyncSessionLocal() as session:
        yield session

@app.get("/users")
async def get_users(db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(models.User))
    users = result.scalars().all()
    return users

这里用了 async with 自动管理会话生命周期,用完即关,不会泄露连接。这是官方推荐的做法,比手动 close 靠谱多了。

🔄 三、Alembic 接入与自动迁移

现在模型有了,但数据库里还没表呢。这时候 Alembic 就该登场了。很多人觉得 Alembic 麻烦,但其实只要配一次,后面爽到飞起。

⚙️ 初始化 Alembic

alembic init alembic

这会在项目根目录生成 alembic.ini alembic/ 文件夹。然后打开 alembic.ini,修改数据库连接字符串:

sqlalchemy.url = postgresql+asyncpg://user:pass@localhost/dbname

注意:这里必须用异步驱动格式,否则后面生成迁移时会报错。

🔧 修改 env.py 支持异步

这是最关键的一步!默认生成的 env.py 是给同步用的,我们需要改成异步模式。打开 alembic/env.py,找到 run_migrations_online 函数,修改如下:

from sqlalchemy.ext.asyncio import async_engine_from_config
from sqlalchemy import pool
import asyncio

# 在 run_migrations_online 内部:
def do_run_migrations(connection):
    context.configure(connection=connection, target_metadata=target_metadata)
    with context.begin_transaction():
        context.run_migrations()

async def run_async_migrations():
    connectable = async_engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )
    async with connectable.connect() as connection:
        await connection.run_sync(do_run_migrations)

asyncio.run(run_async_migrations())

这段代码的作用是让 Alembic 用异步引擎连接数据库,然后通过 run_sync 调用同步的迁移配置。如果不改这里,你执行迁移时会得到 “asyncpg can‘t run in sync” 之类的错误。这个坑我替你们填了,直接复制就行。

还有一处要更改,否则会报错 env.py does not provide a MetaData object or sequence of objects to the context

from app.models import User

# target_metadata = None
target_metadata = User.metadata

📝 生成并执行迁移

alembic revision --autogenerate -m "init user table"
alembic upgrade head

运行第一条命令后,去 alembic/versions/ 下检查生成的脚本。千万别直接相信自动生成的脚本! 有时候它可能漏掉索引,或者把 nullable 搞反。我每次都会手动 review 一遍,比如看有没有创建唯一约束,有没有遗漏外键。这是线上事故的高发区。

确认无误后,执行 upgrade,表就建好了。以后每次修改模型,重复这两步即可。

🧠 四、进阶思考与注意事项

🔁 同步与异步的抉择

如果你是新项目,强烈建议用异步。但如果你维护的老项目是同步的,也别急着重构。同步配合 fastapi 的 run_in_threadpool 也能用,只是性能上限低一些。不过根据我的线上经验,大部分业务场景异步的收益并没有想象中大,除非你是高并发 IO 密集型。

💡 连接池调优

默认的连接池参数可能不适合你的并发量。在 create_async_engine 中可以调整:

engine = create_async_engine(DATABASE_URL, pool_size=20, max_overflow=10)

pool_size 是核心连接数,max_overflow 是峰值时最多可以创建的额外连接数。

我通常会根据服务器的数据库最大连接数来设置,比如 MySQL 默认 151,那么 pool_size+max_overflow 最好不要超过 150,留一点给管理工具。

🚨 永远不要在迁移脚本中直接改数据

Alembic 是用来改表结构的,不是用来改数据的。如果你需要数据迁移(比如把用户名从两列合并成一列),请在 upgrade 函数里用 op.execute() 执行原生 SQL,并且务必写好 downgrade 回退脚本。

血的教训:有一次我直接在脚本里写了 Python 循环更新数据,结果生产执行了半小时,锁表锁到崩溃。


👋 最后啰嗦一句

好了,这套组合拳打下来,你应该能顺利搭出一个稳当的数据库层了。其实这些工具都很成熟,难的是那些细枝末节的配置和习惯。我今天分享的每一个坑,都是真金白银换来的,希望你能绕过。

如果你觉得这篇文章有用,点赞、收藏、关注 走一波,下次找出来也方便。还有什么数据库相关的问题,欢迎留言,咱们一起讨论。毕竟,程序员最懂程序员,不是吗?😉

posted @ 2026-03-09 22:35  一名程序媛呀  阅读(9)  评论(1)    收藏  举报