FastAPI企业级目录结构与Alembic数据库迁移实战指南
2026/9/15 1:20:00 网站建设 项目流程

开始之前,先说一句大实话

FastAPI 学到第三天,你大概率已经能写一个像模像样的接口了——路由会了,参数校验会了,Response Model 也会了。但等你想把它往“项目”里塞的时候,你会发现两个尴尬问题:第一,代码全堆在一个main.py里,别说同事看不懂,过三天你自己都找不到某个逻辑写在哪;第二,数据模型一旦要改动,数据库表和代码就脱节了,手动去数据库里改表结构,那是给自己埋雷。

这篇文章的内容,就是我踩完坑之后的总结:FastAPI 的企业级目录怎么拆,以及数据库迁移怎么用 Alembic 管起来。不是教科书式的理论,是我在自己的项目里跑通过、也翻过车之后的实操笔记。你跟着做一遍,后面再往项目里加模块、加表,心里会稳很多。

先说清楚这套东西到底解决什么问题。目录结构解决的是“代码放哪儿、谁来依赖谁”的问题;数据库迁移解决的是“表结构怎么跟着代码演变、又不丢数据”的问题。一个是骨架,一个是毛细血管。这俩搞不定,项目越大越痛苦。

适合谁看?如果你已经会用 FastAPI 写 CRUD,但还没想过“正经项目长什么样”,这篇就是给你准备的。纯零基础可能要先补一补路由和 Pydantic,不然有些地方会卡住。

1. 目录先别急着写代码:先想清楚边界

很多人的第一个 FastAPI 项目都是长这样的:一个main.py,里面先是配置,再是路由,然后中间夹着三五个模型定义,最底下还有一段建表的代码。跑是能跑,但负责任地说,这玩意儿连“小工具”都算不上,撑不起任何实际业务。

1.1 为什么一定要从“一个文件”升级到“一个包”

我见过太多人卡在这一步,不是因为不会写代码,而是觉得“项目还小,没必要搞那么复杂”。这个想法短期没毛病,但 FastAPI 的项目通常不是写完就完了,你得加登录、加权限、加定时任务、加对外接口……每加一个功能就往main.py里堆,文件会迅速膨胀到两三千行。

这时候你面临的不只是“难看不难看”的问题,而是修改风险的问题。改一个数据模型,可能要牵扯到路由层、校验层、CRUD 层;如果它们全在一个文件里,任何一个不经意的改动都可能把全站搞挂。拆成独立模块之后,改动被限制在非常有边界的小空间里,出问题的概率和排查范围都会小很多。

企业级目录的核心,说白了就四个字:关注点分离。路由只管接收请求,业务逻辑放到 service 层,数据存取放到 crud 层,模型和数据库打交道,Pydantic Schema 负责出入参校验。各管一段,互不越界。谁出了问题,直接定位那一层就完事。

1.2 落地方案:一个我实测很顺的目录模板

我不喜欢上来就抛一个“终极目录”,因为每个团队、每个项目的技术栈和习惯都不一样。但有一种结构经过大量项目验证,兼顾了扩展和简洁,这就是我一直在用的模板:

project_root/ ├── alembic/ │ └── versions/ ├── app/ │ ├── __init__.py │ ├── main.py │ ├── core/ │ │ ├── __init__.py │ │ ├── config.py │ │ └── database.py │ ├── api/ │ │ ├── __init__.py │ │ ├── deps.py │ │ └── v1/ │ │ ├── __init__.py │ │ └── endpoints/ │ ├── models/ │ │ ├── __init__.py │ │ └── user.py │ ├── schemas/ │ │ ├── __init__.py │ │ └── user.py │ ├── crud/ │ │ ├── __init__.py │ │ └── user.py │ └── services/ │ └── __init__.py ├── tests/ ├── .env ├── .gitignore ├── alembic.ini └── requirements.txt

每个目录有明确的职责,我在下面这张表里写清楚了:

目录/文件职责谁依赖它
app/core全局配置、数据库连接、日志等基础设施几乎所有模块
app/api路由定义、依赖注入、接口入口只调用 service/crud
app/modelsSQLAlchemy ORM 模型,对应数据库表结构crud、alembic
app/schemasPydantic 模型,定义 API 的入参与出参api 层
app/crud数据库操作封装,一行一个函数service 层
app/services业务逻辑,多个 crud 组合和加工api 层
alembic数据库迁移脚本不参与运行时

这个结构最大的好处是单向依赖:路由可以调用服务,服务可以调用 crud,crud 才碰模型,模型不反向依赖任何东西。一旦出现循环导入,八成是你把依赖关系搞反了,回头检查这里就行。

提示:别把 schemas 和 models 混在一个文件里。model 是数据库实体,schema 是接口数据契约,虽然字段经常长得一样,但它们是两套东西,混在一起后患无穷。

1.3 配置管理:别再import os.getenv到处飞了

初学者最常见的配置写法是散落一地的os.getenv("DATABASE_URL")。一开始看着还行,直到你发现某个变量的名字在不同文件里拼法不一致,或者你想区分开发环境和测试环境的配置时,痛苦就来了。

企业级项目里,我推荐用 Pydantic 的BaseSettings统一管配置。FastAPI 全家桶有一致性,Pydantic 的校验能力也不用白不用。做法是这样的:

# app/core/config.py from typing import Optional from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): PROJECT_NAME: str = "my-fastapi-project" API_V1_PREFIX: str = "/api/v1" SECRET_KEY: str = "change-me-in-prod" ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 DATABASE_URL: str = "sqlite:///./test.db" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=True, ) settings = Settings()

把配置集中在app/core/config.py里之后,其他文件只需要:

from app.core.config import settings

这里有几个细节我要多说一句:

  • model_config里的env_file=".env"表示自动读取项目根目录的.env文件,这个文件的变量名必须和 Settings 里的字段名一致,大小写匹配(case_sensitive=True时)。
  • .env文件千万别提交到 Git 仓库,里面是密钥和数据库地址,泄露了就是事故。记得在.gitignore里写上.env
  • 生产环境不要用默认值,宁可在部署时强制通过环境变量注入。

配置集中管理的好处,等你到了要切换“本地开发库”和“线上生产库”的时候就体会到了——改一个.env文件,全项目生效,不用翻代码。

2. 数据库迁移:为什么要用 Alembic,而不是手动改表

聊完目录,到了这篇文章的重头戏:数据库迁移。

2.1 没有迁移工具的时候,你是怎么改表结构的?

假设你做了个用户表,上线跑了一周,用户已经有两千条数据了。这时候产品说,要在用户表加一个nickname字段。没做过迁移的常规操作是什么?

你用 Navicat 或者命令行连上数据库,执行一句:

ALTER TABLE users ADD COLUMN nickname VARCHAR(64);

然后再回到代码里给模型加上字段。看起来没问题,对吧?但这个操作有致命伤:你的表结构和代码不同步了。同事拉下代码,连上自己本地的空数据库,创建的表里根本没有nickname字段,跑起来直接报错。你再告诉他“哦,你要手动跑一下那句 SQL”,好,噩梦开始了。

问题手动改表Alembic 迁移
表结构与代码同步靠口头传达迁移文件自带,自动同步
本地、测试、生产环境一致性容易漏执行一条命令统一升级
历史变更追溯无记录每次迁移都是版本记录
回滚旧版本基本不可能一条命令降级
团队协作互相覆盖、冲突文件化,Git 可合并

迁移工具的本质,就是把数据库 schema 的变化变成和代码一样的“版本控制”。每次变更生成一个迁移脚本,脚本能往前进(upgrade),也能往后退(downgrade)。数据库的状态不再是薛定谔的“大家各凭本事”,而是跟随代码的版本走。

2.2 SQLAlchemy + Alembic 的选型理由

FastAPI 生态里最主流的数据库工具就是 SQLAlchemy 2.x,配套的迁移工具几乎只有 Alembic 一个正经选择。理由很简单:

  • Alembic 是 SQLAlchemy 的作者 Mike Bayer 本尊写的,对 SQLAlchemy 模型的理解是原生的。
  • Alembic 支持自动生成迁移脚本——你改完模型,它能对比数据库现状,自动生成ALTER TABLE之类的 SQL 逻辑,不用手写。
  • 迁移文件就是 Python 代码,可以在里面写数据修复逻辑,比如“给已有用户批量生成昵称”。

所以技术选型没什么好纠结的。接下来说实操。

2.3 环境准备:用 uv 而不是 pip,能省一半心

先提一个非常实际的问题:Python 环境。我早年被pip install装出来的混乱环境坑了太多次,不同的库互相抢版本,项目带上生产环境直接起不来。现在新项目我统一用 uv 管理。

uv 是 Rust 写的 Python 包管理器,速度就不用吹了,关键它自带虚拟环境和锁文件机制,一句话就能创建一个干净环境并装完依赖:

uv venv .venv source .venv/bin/activate # Windows 用的是 .venv\Scripts\activate uv pip install -r requirements.txt

如果你是个新项目,甚至可以这样一步到位:

uv init fastapi-day3 cd fastapi-day3 uv add fastapi "uvicorn[standard]" sqlalchemy alembic pydantic-settings python-dotenv

这会在项目里生成pyproject.tomluv.lock,以后加依赖、删依赖都靠uv add / uv remove,不会自动升级你没让升级的库。对于团队项目,锁文件保证所有人拉下来跑的环境是一致的,这点比 pip 强太多。

实测感受:清理临时环境后,我从零到跑通 Alembic 迁移总共没超过十分钟。换 pip 的话,光排查某个传递依赖的版本冲突就能耗掉半天。

3. 实操全记录:从零搭建目录到跑通首次迁移

下面这部分是重点中的重点。我带大家从头把这套东西搭一遍,每一步我都复盘当年踩过的坑。

3.1 项目初始化与依赖安装

先建项目根目录,并初始化 uv 环境:

mkdir fastapi-enterprise-demo cd fastapi-enterprise-demo uv init

这会生成一个完整的 pyproject.toml。然后添加需要的依赖:

uv add fastapi uvicorn[standard] sqlalchemy alembic pydantic-settings python-dotenv

如果你需要连 PostgreSQL 或 MySQL,记得加对应驱动:

uv add psycopg2-binary # PostgreSQL 用,或者用 psycopg / asyncpg uv add pymysql # MySQL 用

我平时本地开发图省事会用 SQLite,生产切 PostgreSQL。这里的示例以 SQLite 为主,后续切换的坑在第 4 章会提到。

3.2 创建目录骨架

按上面 1.2 的结构手动创建目录,或者在项目根目录执行下面的命令快速生成空目录(Windows 在 Git Bash 里执行同样没问题):

mkdir -p app/core app/api/v1/endpoints app/models app/schemas app/crud app/services tests alembic/versions

然后创建 Python 包需要的__init__.py

touch app/__init__.py app/core/__init__.py app/api/__init__.py \ app/api/v1/__init__.py app/api/v1/endpoints/__init__.py \ app/models/__init__.py app/schemas/__init__.py \ app/crud/__init__.py app/services/__init__.py

这一步看起来没有什么技术含量,但很多人会漏掉__init__.py。没有它,Python 不会把这个目录当包,你后面from app.core.config import settings就会报 ModuleNotFoundError。我用过一次from app.core import config没问题,但换了个运行方式就找不到模块,教训就是:别省这些空文件

3.3 编写配置和数据库初始化文件

创建app/core/config.py,内容就是 1.3 节那一段。接着写app/core/database.py

# app/core/database.py from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase, sessionmaker from app.core.config import settings # 生产环境换成 PostgreSQL 时,只需改 settings.DATABASE_URL engine = create_engine( settings.DATABASE_URL, pool_pre_ping=True, # 检测连接是否可用 echo=False, # 调试时改 True,可打印 SQL future=True, ) SessionLocal = sessionmaker( bind=engine, autocommit=False, autoflush=False, future=True, ) class Base(DeclarativeBase): """所有 ORM 模型的基类""" pass def get_db(): """FastAPI 依赖注入用的数据库会话""" db = SessionLocal() try: yield db finally: db.close()

这里我故意没有用Base.metadata.create_all()。这也是企业级项目里非常重要的一道分水岭——create_all()适合疯狂改模型的开发早期,但它不会帮你更新已存在的表,也不产生任何迁移记录。从 Day 3 开始,建表和改表的事全部交给 Alembic。

3.4 定义第一个用户模型

下面来一个简单的用户模型,别让它太简陋,带上业务里常见的字段:

# app/models/user.py from datetime import datetime from sqlalchemy import Boolean, DateTime, String from sqlalchemy.orm import Mapped, mapped_column from app.core.database import Base class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) email: Mapped[str] = mapped_column( String(255), unique=True, index=True, nullable=False ) hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) nickname: Mapped[str] = mapped_column(String(64), default="") is_active: Mapped[bool] = mapped_column(Boolean, default=True) created_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now() )

注意created_atupdated_at,我用了server_default=func.now()而不是 Python 的datetime.now。原因是:时间应该由数据库统一生成,而不是由每个应用进程各自生成,尤其在多实例部署的时候,服务端时间才是权威。这一点在后续审计、排查数据问题时特别有用。

3.5 初始化 Alembic 并接入项目

在项目根目录执行:

alembic init alembic

这会在你的项目里创建alembic.inialembic/目录(versions 子目录也在里面)。但默认生成的配置不知道你的模型和数据库 URL,需要改两个地方。

第一步,改alembic.ini里的数据库 URL。我建议不要直接写死,而是让它读取环境中的DATABASE_URL。修改 ini 里的这一行(通常在最底部):

sqlalchemy.url = driver://user:pass@localhost/dbname

改为:

# 不在 ini 里写死 URL,实际运行时从环境变量或 .env 读取 # sqlalchemy.url = 占位无所谓,env.py 会覆盖

更彻底的做法是在alembic/env.py里动态读取配置,我们接着改。

第二步,改alembic/env.py,让它能识别我们的模型和配置:

# alembic/env.py 中需要修改的部分 from logging.config import fileConfig from sqlalchemy import engine_from_config, pool from alembic import context # 关键一步:导入配置和模型基类 from app.core.config import settings from app.core.database import Base from app import models # noqa: F401 确保所有模型都被注册 config = context.config if config.config_file_name is not None: fileConfig(config.config_file_name) # 用项目里的 DATABASE_URL 覆盖 alembic.ini 里的占位 config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) # target_metadata 指向 Base.metadata,Alembic 才能对比模型和数据库 target_metadata = Base.metadata

这里最关键的代码是from app import models。如果少了它,Alembic 不会知道你定义了哪些表,自动生成的迁移脚本会是空的。

踩坑提醒:Alembic 自动生成的迁移脚本,是基于“models 里注册的表”和“当前数据库里的真实表”之间的差异来生成的。如果你新加了一个模型文件,但没在app/models/__init__.py中把它导入,Alembic 完全看不到它。所以每次新增模型,记得在app/models/__init__.py加一行:

# app/models/__init__.py from app.models.user import User # noqa

3.6 生成并执行第一次迁移

当models和env配置好之后,在项目根目录执行:

alembic revision --autogenerate -m "create users table"

这条命令会输出类似下面的信息:

INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'users' Generating /path/to/project/alembic/versions/xxxx_create_users_table.py ...

它会在alembic/versions/下生成一个 Python 文件。打开看看,里面应该包含upgrade()里建表的代码和downgrade()里删表的代码。这个文件就是迁移历史的第一个版本,应当提交到 Git。

然后应用迁移:

alembic upgrade head

看到一行Running upgrade -> xxxx, create users table就说明成功了。可以用sqlite3或任何数据库客户端验证:

sqlite3 fastapi-enterprise-demo.db ".tables"

正常情况下会看到alembic_versionusers两张表。alembic_version是 Alembic 自己用来记录当前版本的,别去手贱删它。

3.7 把迁移集成到 FastAPI 启动流程

最后一个小步骤:在app/main.py里创建 FastAPI 实例,并注册一个健康检查接口,测试整个工程能跑起来:

# app/main.py from fastapi import FastAPI from app.api.v1.endpoints import users # 后续章节会写这个模块 from app.core.config import settings app = FastAPI( title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_PREFIX}/openapi.json", ) # 路由注册统一走 v1 app.include_router(users.router, prefix=settings.API_V1_PREFIX) @app.get("/health") def health_check(): return {"status": "ok"}

启动:

uvicorn app.main:app --reload

浏览器打开http://127.0.0.1:8000/health,看到{"status":"ok"},整个工程骨架就跑通了。

4. 数据库迁移的高频问题与排查技巧

这一章我打算把常见的坑一次性列全,你大概率会至少中一个。有些问题我当年排查了一晚上才弄明白,现在写成速查表,希望能帮你把这几个小时的弯路直接省掉。

4.1 autogenerate 提示 “No changes detected”,但明明改了模型

这是新手上路遇到最多的问题,发生原因有几种,按概率排序:

  1. 模型文件没有被导入到app/models/__init__.py。这种情况最典型,Alembic 只认Base.metadata里有注册的表,你 models 目录下的文件如果没有被 import 过,metadata 里就没有对应表。
  2. env.py里的target_metadata指向了错误的 Base。比如你模型的基类是从别的文件 import 的,而 env.py 里的 Base 是另起炉灶的,两边压根不是同一个 metadata。
  3. 数据库中已经有这张表,并且表结构完全一致。这不算错误,但会让人觉得“咦怎么没变化”。

排查套路:先写一个最小脚本打印所有表,看看Base.metadata里到底注册了哪些表:

python -c "from app.core.database import Base; from app import models; print(list(Base.metadata.tables.keys()))"

输出里如果没有users,那就是导入的问题;如果表齐全,再去检查数据库 URL 是不是指到了别的库。

4.2 生产环境迁移失败:数据库被别的连接占用

ALTER TABLE在 PostgreSQL/MySQL 上一般不会锁死,但如果你加了索引、改了约束,某些引擎会锁表。线上最稳妥的操作窗口是低峰期,或者使用在线 DDL 工具;对于 SQLite,Alembic 干脆是在事务里执行 DDL,一失败就回滚。

我的习惯是:任何时候上迁移,先备份,再升级。备份可以从数据库层面做 dump,也可以在迁移脚本里用事务包裹数据变更。Alembic 的迁移文件默认在事务中执行,如果中间的某一步报错,整批回滚。

4.3 本地开发时发现了迁移脚本写错了,怎么回滚

Alembic 提供了降级能力:

alembic downgrade -1

-1表示回滚最近一个迁移版本。如果只想回滚到某个特定版本,用:

alembic downgrade <revision_id>

revision_id可以从alembic_version表或迁移文件头部看到。回滚之后再修改迁移文件,或者重新生成迁移覆盖它。

4.4 SQLite 在迁移时的 ALTER TABLE 限制

如果你本地用的是 SQLite,后期切 PostgreSQL 之前会踩到一个限制:SQLite 只支持很有限的 ALTER TABLE,比如加一列、改表名;要删除一个字段,那得“重建整个表”。Alembic 的处理方式是模拟重建,在大表上会非常慢。

我的建议是:如果项目注定要上生产,开发阶段尽早切换 PostgreSQL 或 MySQL 本地实例,别用 SQLite 写了三个月再切,那会儿迁移脚本的坑能让你怀疑人生。数据库的差异(比如自增主键的语法、JSON 字段类型)在 Alembic 里不是完全透明能抹平的。

4.5 迁移文件太多之后,新人怎么快速看懂数据库演进

项目久了,alembic/versions下可能躺着几十个文件。不要慌,这恰恰是迁移工具的价值——数据库的所有变化都有历史可查。团队里可以约定一个规则:迁移文件的文件名里带上业务描述,比如xxxx_add_nickname_to_users.py,而不是默认的xxxx_auto.py,这样只看文件名就能大概知道这一版改了什么。

如果要看当前数据库处于哪个版本,直接查alembic_version表,或者执行:

alembic current

要完整看历史链:

alembic history

5. 再聊几点团队协作与上生产的建议

目录和迁移这关过了,你的项目就已经有了一个像样的地基。但地基之上,还有几个我觉得同等重要的点,顺手补充给你。

5.1 模型变更流程:先别急着生成迁移

团队协作时,最忌讳的是一个人改了模型,另一个人也不知道,马上 autogenerate 了一份迁移,两个人对着同一个数据库来回踩。

我建议的流程是:

  1. 先在代码里改好模型;
  2. 本地生成迁移并确认 upgrade/downgrade 都没问题;
  3. commit 时同时提交模型改动和迁移文件,commit message 写清楚;
  4. 其他人 pull 代码后执行alembic upgrade head,一条命令同步数据库。

这样就可以避免“改代码后忘记运行迁移”这类低级错误。

5.2 生产环境的迁移由谁执行

不同团队有不同的约定。我见过在 CI/CD 里自动跑的,也见过 DBA 手动执行的。各有利弊,但有一条底线:不要在多个实例上同时执行同一个迁移。如果你的服务是多个副本同时启动,启动钩子里又加了alembic upgrade head,那么多进程同时执行迁移有概率产生竞态问题。

稳妥的做法:单独安排一个 Job 在发布流程里先跑迁移,跑完确认成功后再滚动更新服务实例。这样比“让每个实例都去执行迁移”可控得多。

5.3 未来还能往这个骨架里加什么

写到这里,你会发现这套目录和迁移配置其实不依赖具体业务——它就是个通用底座。后面的 Day 4、Day 5 你大概率会继续加:

  • JWT 登录认证,放在app/api/v1/endpoints/auth.pyapp/services/auth.py
  • 用户 CRUD 接口,放在app/api/v1/endpoints/users.py,配合app/crud/user.py
  • 权限依赖,写在app/api/deps.py里;
  • 测试用例,覆盖 crud 和 api 层,放在tests/
  • 如果表结构变化多了,还能在迁移脚本里塞数据修复逻辑,比如把某个字段从一个格式迁移到另一个格式。

这些模块的扩展方式都一样:在对应目录加文件,在__init__.py里注册,路由挂到 v1 下。只要目录边界清晰,你永远不会因为加一个功能而重写已有代码。

6. 这一天的实操总结:稳定比炫技重要

说实话,我学 FastAPI 前三天最直观的感受是“框架太速成了”,一天能学会的东西比 Django 一周都多。但目录和迁移这两样东西,恰恰不是框架给你兜底的活儿,得自己动手搭。搭得越稳,后面的功能开发就越顺。

最后分享一个我在实际操作中特别有体会的小技巧:别忘了把alembic_version表也纳入你备份的范畴。很多人做数据库备份只备份业务表,恢复数据库之后发现 Alembic 的版本指向和业务表结构对不上,接着upgrade head就疯狂报错。数据备份和表结构备份会一起备份的那一天,希望大家不会再跟我一样,在一个凌晨为了一个版本号对不上,花两个多小时修复数据库到半夜。

今天的目录结构模板和 Alembic 配置,我已经被项目验证过很多遍,直接拿去用完全可以。等你跑通一遍之后,再按自己团队的习惯微调也不迟。建议你一定要动手把这条链路完整走一遍:建目录、写配置、定义模型、生成迁移、应用迁移。光看不练,这些细节记不住的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询