# FastAPI + Alembic 迁移管理最佳实践(AI 编辑器时代) ## 问题背景 在使用 AI 编辑器(Cursor、Copilot、Claude)开发时,常见问题: ### 典型场景 ``` 1. AI 自动修改 Model 字段 2. 开发者忘记创建迁移文件 3. 多次修改累积后一次性创建迁移 4. 迁移文件冲突或断链 5. 团队成员迁移版本不一致 ``` ### 后果 - ❌ 迁移链断裂 - ❌ 数据库与 Model 不同步 - ❌ 团队成员环境不一致 - ❌ 生产部署失败 --- ## 核心原则 ### 1. **Model First,Migration Immediately** ``` 改 Model → 立即创建迁移 → 测试 → 提交 ``` ### 2. **Never Skip Migration** ```python # ❌ 错误做法 # 1. 修改 Model # 2. 直接运行应用(SQLModel 自动同步) # 3. 数据库已变但无迁移文件 # ✅ 正确做法 # 1. 修改 Model # 2. alembic revision --autogenerate -m "描述" # 3. 检查并调整迁移文件 # 4. alembic upgrade head # 5. 测试 # 6. 提交代码(Model + 迁移文件) ``` ### 3. **Atomic Commits** ```bash # 每次提交必须包含 git add server/app/models/xxx.py git add server/alembic/versions/xxx_description.py git commit -m "feat: 添加 XXX 字段" ``` --- ## 工作流规范 ### 🔄 标准迁移工作流 #### 第一步:修改 Model ```python # server/app/models/user.py class User(SQLModel, table=True): # ... 现有字段 # ✅ 新增字段(AI 建议或手动添加) avatar_url: str | None = Field(default=None, max_length=500) ``` #### 第二步:立即创建迁移 ```bash # 进入服务器目录 cd server # 自动生成迁移文件 docker exec jointo-server-app alembic revision --autogenerate -m "add user avatar_url field" # 输出示例: # Generating /app/alembic/versions/20260215_1630_abc123def456_add_user_avatar_url_field.py ... done ``` #### 第三步:检查迁移文件 ```python # ⚠️ 必须手动检查 AI 生成的迁移是否正确 # 检查点: # 1. 是否使用了 IF NOT EXISTS(幂等性) # 2. 是否正确处理了默认值 # 3. 是否需要数据迁移逻辑 # 4. 索引是否正确创建 ``` #### 第四步:应用迁移 ```bash # 在开发环境测试 docker exec jointo-server-app alembic upgrade head # 检查结果 docker exec jointo-server-postgres psql -U jointoAI -d jointo -c "\d users" ``` #### 第五步:回滚测试 ```bash # 测试 downgrade 是否可用 docker exec jointo-server-app alembic downgrade -1 docker exec jointo-server-app alembic upgrade head ``` #### 第六步:提交代码 ```bash git add server/app/models/user.py git add server/alembic/versions/20260215_1630_abc123def456_add_user_avatar_url_field.py git commit -m "feat(user): add avatar_url field for user profile" git push origin feature/user-avatar ``` --- ## 防御性迁移编写 ### 1. 始终使用幂等操作 #### ✅ 添加列(幂等) ```python def upgrade() -> None: """添加 avatar_url 字段""" op.execute(""" ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url VARCHAR(500); """) op.execute(""" COMMENT ON COLUMN users.avatar_url IS '用户头像URL'; """) ``` #### ✅ 创建索引(幂等) ```python def upgrade() -> None: """为 email 创建唯一索引""" op.execute(""" CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users (email); """) ``` #### ✅ 创建函数(幂等) ```python def upgrade() -> None: """创建触发器函数""" # 先删除旧的(包括依赖的触发器) op.execute("DROP FUNCTION IF EXISTS update_timestamp() CASCADE;") # 再创建新的 op.execute(""" CREATE FUNCTION update_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; """) ``` ### 2. 数据迁移模板 #### 添加非空字段(带默认值) ```python def upgrade() -> None: """添加非空字段 status""" # Step 1: 添加可空列 op.execute(""" ALTER TABLE projects ADD COLUMN IF NOT EXISTS status SMALLINT; """) # Step 2: 填充默认值 op.execute(""" UPDATE projects SET status = 1 WHERE status IS NULL; """) # Step 3: 设置为非空 op.execute(""" ALTER TABLE projects ALTER COLUMN status SET NOT NULL; """) # Step 4: 设置默认值 op.execute(""" ALTER TABLE projects ALTER COLUMN status SET DEFAULT 1; """) ``` #### 重命名列(保留旧数据) ```python def upgrade() -> None: """重命名 metadata -> meta_data""" # 检查列是否存在 op.execute(""" DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'projects' AND column_name = 'metadata' ) THEN ALTER TABLE projects RENAME COLUMN metadata TO meta_data; END IF; END $$; """) ``` --- ## AI 编辑器使用规范 ### Cursor 配置建议 #### 1. 创建 `.cursor/rules/alembic.md` ```markdown # Alembic 迁移规则 ## 严格禁止 1. ❌ 禁止直接修改已提交的迁移文件 2. ❌ 禁止跳过迁移文件创建 3. ❌ 禁止在 SQLModel 中使用 `create_all()`(开发环境除外) ## 必须遵守 1. ✅ 修改 Model 后必须立即创建迁移 2. ✅ 所有 DDL 操作必须使用 `IF NOT EXISTS` 3. ✅ 函数创建必须先 DROP CASCADE ## 迁移文件模板 \`\`\`python def upgrade() -> None: """描述变更内容""" # 使用原生 SQL 确保幂等性 op.execute(\"\"\" ALTER TABLE xxx ADD COLUMN IF NOT EXISTS yyy TYPE; \"\"\") def downgrade() -> None: """回滚操作""" op.execute(\"\"\" ALTER TABLE xxx DROP COLUMN IF EXISTS yyy; \"\"\") \`\`\` ``` #### 2. 项目级提示词 ```markdown # .cursorrules 或 CLAUDE.md ## 数据库迁移要求 当修改任何 SQLModel 模型时: 1. 生成迁移文件:`alembic revision --autogenerate -m "描述"` 2. 使用幂等 SQL:`IF NOT EXISTS`, `IF EXISTS` 3. 函数创建:先 `DROP IF EXISTS ... CASCADE` 4. 提供回滚逻辑:实现 `downgrade()` 5. 包含注释:`COMMENT ON COLUMN ...` ``` ### Copilot / Claude 提示词 ```markdown 当我修改 SQLModel 时: 1. 提醒我创建 Alembic 迁移 2. 生成幂等的迁移脚本 3. 包含 upgrade 和 downgrade 4. 使用原生 SQL 而非 Alembic ORM API ``` --- ## 迁移冲突解决 ### 场景 1:分支合并冲突 ```bash # 问题:两个分支都创建了迁移 main: 20260215_1000_aaa -> 20260215_1100_bbb feature: 20260215_1000_aaa -> 20260215_1050_ccc # 解决方案 # 1. 切换到 feature 分支 git checkout feature/xxx # 2. 拉取最新 main git fetch origin main # 3. 检查冲突 alembic history # 4. 调整 down_revision # 编辑 20260215_1050_ccc.py down_revision = '20260215_1100_bbb' # 改为指向最新的 main 迁移 # 5. 测试迁移链 alembic upgrade head # 6. 提交合并 git add server/alembic/versions/20260215_1050_ccc.py git commit -m "fix: resolve migration conflict" ``` ### 场景 2:遗漏的迁移 ```bash # 问题:Model 已改但忘记创建迁移 # 诊断 alembic check # 查看是否有未迁移的变更 # 解决 # 1. 创建迁移 alembic revision --autogenerate -m "补充遗漏的迁移" # 2. 检查生成的迁移是否正确 cat alembic/versions/xxx_missing_changes.py # 3. 应用 alembic upgrade head ``` ### 场景 3:生产环境与开发环境不一致 ```bash # 问题:生产环境缺少某些迁移 # 1. 检查生产环境版本 alembic current # 2. 查看缺失的迁移 alembic history # 3. 逐步应用(避免一次性 upgrade head) alembic upgrade +1 # 应用下一个 alembic upgrade +1 # 再应用一个 # ... 直到同步 # 4. 或直接升级到最新(风险较高) alembic upgrade head ``` --- ## 团队协作规范 ### 1. 迁移文件命名约定 ```bash # 格式:YYYYMMDD_HHMM__.py 20260215_1630_abc123def456_add_user_avatar_field.py # 描述应清晰 ✅ add_user_avatar_field ✅ fix_project_status_enum ✅ create_storyboard_tables ❌ update_models ❌ fix_bug ❌ temp_changes ``` ### 2. Pull Request 检查清单 ```markdown ## 迁移检查清单 - [ ] Model 变更与迁移文件一致 - [ ] 迁移文件使用幂等操作 - [ ] 包含 upgrade 和 downgrade - [ ] 本地测试通过 - [ ] 回滚测试通过 - [ ] 迁移链无断裂(alembic history) - [ ] 生产环境部署计划已准备 ``` ### 3. Code Review 重点 ```python # Reviewer 检查点: # ✅ 1. 幂等性 ALTER TABLE users ADD COLUMN IF NOT EXISTS ... # ✅ 2. 数据安全 # 添加非空列时先允许 NULL,填充数据后再改为 NOT NULL # ✅ 3. 性能影响 # 大表添加索引时考虑使用 CONCURRENTLY(PostgreSQL) CREATE INDEX CONCURRENTLY idx_name ON table(col); # ✅ 4. 回滚可行性 # downgrade 能否安全执行?是否会丢失数据? # ✅ 5. 函数/触发器 # 必须先 DROP CASCADE 再 CREATE ``` --- ## 自动化工具 ### 1. Pre-commit Hook 创建 `.git/hooks/pre-commit`: ```bash #!/bin/bash # 检查是否修改了 models 但没有迁移文件 if git diff --cached --name-only | grep -q "server/app/models/"; then if ! git diff --cached --name-only | grep -q "server/alembic/versions/"; then echo "⚠️ 检测到 Model 变更但未包含迁移文件!" echo "" echo "请执行:" echo " cd server" echo " alembic revision --autogenerate -m '描述变更'" echo "" echo "如果确认无需迁移,使用 --no-verify 跳过检查" exit 1 fi fi ``` ### 2. 迁移验证脚本 创建 `server/scripts/validate_migrations.py`: ```python #!/usr/bin/env python3 """ 验证迁移链完整性 """ import subprocess import sys def check_migration_chain(): """检查迁移链是否完整""" result = subprocess.run( ['alembic', 'history'], capture_output=True, text=True ) if result.returncode != 0: print("❌ 迁移链检查失败") print(result.stderr) return False print("✅ 迁移链完整") return True def check_pending_changes(): """检查是否有未迁移的变更""" result = subprocess.run( ['alembic', 'check'], capture_output=True, text=True ) if "No new upgrade operations detected" not in result.stdout: print("⚠️ 检测到未迁移的变更!") print(result.stdout) return False print("✅ 无待迁移变更") return True if __name__ == '__main__': success = check_migration_chain() and check_pending_changes() sys.exit(0 if success else 1) ``` ### 3. CI/CD 集成 `.github/workflows/validate-migrations.yml` 或 `.gitea/workflows/validate.yml`: ```yaml name: Validate Migrations on: [pull_request] jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 with: python-version: '3.12' - name: Install dependencies run: | cd server pip install -r requirements.txt - name: Check migration chain run: | cd server alembic history - name: Check for pending changes run: | cd server alembic check ``` --- ## 常见错误及解决方案 ### 错误 1:Multiple head revisions ```bash # 错误信息 alembic.util.exc.CommandError: Multiple head revisions are present # 原因:两个分支创建了并行的迁移 # 解决:合并分支 alembic merge -m "merge branches" ``` ### 错误 2:Can't locate revision ```bash # 错误信息 Can't locate revision identified by 'abc123' # 原因:缺少中间迁移文件 # 解决: # 1. 从其他分支或团队成员获取缺失的迁移文件 # 2. 或重置迁移历史(慎用) ``` ### 错误 3:Target database is not up to date ```bash # 错误信息 Target database is not up to date # 原因:数据库版本落后于代码 # 解决: alembic upgrade head ``` --- ## 最佳实践总结 ### ✅ DO(应该做) 1. **一次变更一个迁移**:每次 Model 改动立即创建迁移 2. **使用幂等操作**:`IF NOT EXISTS`, `IF EXISTS` 3. **原生 SQL 优先**:避免 Alembic ORM API 4. **测试回滚**:确保 downgrade 可用 5. **团队同步**:频繁拉取最新迁移 6. **命名清晰**:迁移描述应准确反映变更 7. **代码审查**:迁移文件必须 Review ### ❌ DON'T(不应该做) 1. **修改已提交的迁移**:已合并的迁移文件禁止修改 2. **跳过迁移**:不要直接改数据库 3. **手动删除迁移**:不要随意删除迁移文件 4. **忽略冲突**:合并冲突必须解决 5. **生产环境直接改**:所有变更必须通过迁移 6. **多个 head**:避免创建并行迁移分支 --- ## 快速参考 ### 常用命令 ```bash # 创建迁移 alembic revision --autogenerate -m "description" # 应用迁移 alembic upgrade head alembic upgrade +1 # 应用下一个 alembic upgrade # 应用到指定版本 # 回滚迁移 alembic downgrade -1 # 回滚一个 alembic downgrade # 回滚到指定版本 # 查看状态 alembic current # 当前版本 alembic history # 迁移历史 alembic check # 检查未迁移变更 # 合并分支 alembic merge -m "merge" ``` ### 紧急恢复 ```bash # 数据库完全混乱时 # 1. 备份数据库 pg_dump -U user -d db > backup.sql # 2. 删除 alembic_version 表 DROP TABLE alembic_version; # 3. 重新初始化 alembic stamp head # 4. 验证 alembic current ``` --- ## 总结 在 AI 编辑器时代,迁移管理的关键是: 1. 🚦 **规范化**:建立严格的工作流 2. 🛡️ **防御性**:所有操作都幂等 3. 🔄 **自动化**:Pre-commit + CI/CD 4. 👥 **团队化**:Code Review + 文档 **核心原则**:Model 变更必须伴随迁移,迁移必须幂等且可回滚! --- **文档版本**: v1.0 **更新日期**: 2026-02-15 **适用项目**: FastAPI + Alembic + SQLModel + PostgreSQL