开始之前先说一句大实话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/crudapp/modelsSQLAlchemy ORM 模型对应数据库表结构crud、alembicapp/schemasPydantic 模型定义 API 的入参与出参api 层app/crud数据库操作封装一行一个函数service 层app/services业务逻辑多个 crud 组合和加工api 层alembic数据库迁移脚本不参与运行时这个结构最大的好处是单向依赖路由可以调用服务服务可以调用 crudcrud 才碰模型模型不反向依赖任何东西。一旦出现循环导入八成是你把依赖关系搞反了回头检查这里就行。提示别把 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_encodingutf-8, case_sensitiveTrue, ) settings Settings()把配置集中在app/core/config.py里之后其他文件只需要from app.core.config import settings这里有几个细节我要多说一句model_config里的env_file.env表示自动读取项目根目录的.env文件这个文件的变量名必须和 Settings 里的字段名一致大小写匹配case_sensitiveTrue时。.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.toml和uv.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__.pytouch 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_pingTrue, # 检测连接是否可用 echoFalse, # 调试时改 True可打印 SQL futureTrue, ) SessionLocal sessionmaker( bindengine, autocommitFalse, autoflushFalse, futureTrue, ) 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_keyTrue, autoincrementTrue) email: Mapped[str] mapped_column( String(255), uniqueTrue, indexTrue, nullableFalse ) hashed_password: Mapped[str] mapped_column(String(255), nullableFalse) nickname: Mapped[str] mapped_column(String(64), default) is_active: Mapped[bool] mapped_column(Boolean, defaultTrue) created_at: Mapped[datetime] mapped_column( DateTime, server_defaultfunc.now() ) updated_at: Mapped[datetime] mapped_column( DateTime, server_defaultfunc.now(), onupdatefunc.now() )注意created_at和updated_at我用了server_defaultfunc.now()而不是 Python 的datetime.now。原因是时间应该由数据库统一生成而不是由每个应用进程各自生成尤其在多实例部署的时候服务端时间才是权威。这一点在后续审计、排查数据问题时特别有用。3.5 初始化 Alembic 并接入项目在项目根目录执行alembic init alembic这会在你的项目里创建alembic.ini和alembic/目录versions 子目录也在里面。但默认生成的配置不知道你的模型和数据库 URL需要改两个地方。第一步改alembic.ini里的数据库 URL。我建议不要直接写死而是让它读取环境中的DATABASE_URL。修改 ini 里的这一行通常在最底部sqlalchemy.url driver://user:passlocalhost/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.metadataAlembic 才能对比模型和数据库 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 # noqa3.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_version和users两张表。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( titlesettings.PROJECT_NAME, openapi_urlf{settings.API_V1_PREFIX}/openapi.json, ) # 路由注册统一走 v1 app.include_router(users.router, prefixsettings.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”但明明改了模型这是新手上路遇到最多的问题发生原因有几种按概率排序模型文件没有被导入到app/models/__init__.py。这种情况最典型Alembic 只认Base.metadata里有注册的表你 models 目录下的文件如果没有被 import 过metadata 里就没有对应表。env.py里的target_metadata指向了错误的 Base。比如你模型的基类是从别的文件 import 的而 env.py 里的 Base 是另起炉灶的两边压根不是同一个 metadata。数据库中已经有这张表并且表结构完全一致。这不算错误但会让人觉得“咦怎么没变化”。排查套路先写一个最小脚本打印所有表看看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 工具对于 SQLiteAlembic 干脆是在事务里执行 DDL一失败就回滚。我的习惯是任何时候上迁移先备份再升级。备份可以从数据库层面做 dump也可以在迁移脚本里用事务包裹数据变更。Alembic 的迁移文件默认在事务中执行如果中间的某一步报错整批回滚。4.3 本地开发时发现了迁移脚本写错了怎么回滚Alembic 提供了降级能力alembic downgrade -1-1表示回滚最近一个迁移版本。如果只想回滚到某个特定版本用alembic downgrade revision_idrevision_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 history5. 再聊几点团队协作与上生产的建议目录和迁移这关过了你的项目就已经有了一个像样的地基。但地基之上还有几个我觉得同等重要的点顺手补充给你。5.1 模型变更流程先别急着生成迁移团队协作时最忌讳的是一个人改了模型另一个人也不知道马上 autogenerate 了一份迁移两个人对着同一个数据库来回踩。我建议的流程是先在代码里改好模型本地生成迁移并确认 upgrade/downgrade 都没问题commit 时同时提交模型改动和迁移文件commit message 写清楚其他人 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.py和app/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 配置我已经被项目验证过很多遍直接拿去用完全可以。等你跑通一遍之后再按自己团队的习惯微调也不迟。建议你一定要动手把这条链路完整走一遍建目录、写配置、定义模型、生成迁移、应用迁移。光看不练这些细节记不住的。
