You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

14 KiB

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

# ❌ 错误做法
# 1. 修改 Model
# 2. 直接运行应用(SQLModel 自动同步)
# 3. 数据库已变但无迁移文件

# ✅ 正确做法
# 1. 修改 Model
# 2. alembic revision --autogenerate -m "描述"
# 3. 检查并调整迁移文件
# 4. alembic upgrade head
# 5. 测试
# 6. 提交代码(Model + 迁移文件)

3. Atomic Commits

# 每次提交必须包含
git add server/app/models/xxx.py
git add server/alembic/versions/xxx_description.py
git commit -m "feat: 添加 XXX 字段"

工作流规范

🔄 标准迁移工作流

第一步:修改 Model

# server/app/models/user.py
class User(SQLModel, table=True):
    # ... 现有字段
    
    # ✅ 新增字段(AI 建议或手动添加)
    avatar_url: str | None = Field(default=None, max_length=500)

第二步:立即创建迁移

# 进入服务器目录
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

第三步:检查迁移文件

# ⚠️ 必须手动检查 AI 生成的迁移是否正确

# 检查点:
# 1. 是否使用了 IF NOT EXISTS(幂等性)
# 2. 是否正确处理了默认值
# 3. 是否需要数据迁移逻辑
# 4. 索引是否正确创建

第四步:应用迁移

# 在开发环境测试
docker exec jointo-server-app alembic upgrade head

# 检查结果
docker exec jointo-server-postgres psql -U jointoAI -d jointo -c "\d users"

第五步:回滚测试

# 测试 downgrade 是否可用
docker exec jointo-server-app alembic downgrade -1
docker exec jointo-server-app alembic upgrade head

第六步:提交代码

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. 始终使用幂等操作

添加列(幂等)

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';
    """)

创建索引(幂等)

def upgrade() -> None:
    """为 email 创建唯一索引"""
    op.execute("""
        CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email
        ON users (email);
    """)

创建函数(幂等)

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. 数据迁移模板

添加非空字段(带默认值)

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;
    """)

重命名列(保留旧数据)

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

# 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. 项目级提示词

# .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 提示词

当我修改 SQLModel 时:
1. 提醒我创建 Alembic 迁移
2. 生成幂等的迁移脚本
3. 包含 upgrade 和 downgrade
4. 使用原生 SQL 而非 Alembic ORM API

迁移冲突解决

场景 1:分支合并冲突

# 问题:两个分支都创建了迁移
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:遗漏的迁移

# 问题:Model 已改但忘记创建迁移

# 诊断
alembic check  # 查看是否有未迁移的变更

# 解决
# 1. 创建迁移
alembic revision --autogenerate -m "补充遗漏的迁移"

# 2. 检查生成的迁移是否正确
cat alembic/versions/xxx_missing_changes.py

# 3. 应用
alembic upgrade head

场景 3:生产环境与开发环境不一致

# 问题:生产环境缺少某些迁移

# 1. 检查生产环境版本
alembic current

# 2. 查看缺失的迁移
alembic history

# 3. 逐步应用(避免一次性 upgrade head)
alembic upgrade +1  # 应用下一个
alembic upgrade +1  # 再应用一个
# ... 直到同步

# 4. 或直接升级到最新(风险较高)
alembic upgrade head

团队协作规范

1. 迁移文件命名约定

# 格式:YYYYMMDD_HHMM_<hash>_<description>.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 检查清单

## 迁移检查清单

- [ ] Model 变更与迁移文件一致
- [ ] 迁移文件使用幂等操作
- [ ] 包含 upgrade 和 downgrade
- [ ] 本地测试通过
- [ ] 回滚测试通过
- [ ] 迁移链无断裂(alembic history)
- [ ] 生产环境部署计划已准备

3. Code Review 重点

# 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

#!/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

#!/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

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

# 错误信息
alembic.util.exc.CommandError: Multiple head revisions are present

# 原因:两个分支创建了并行的迁移

# 解决:合并分支
alembic merge <rev1> <rev2> -m "merge branches"

错误 2:Can't locate revision

# 错误信息
Can't locate revision identified by 'abc123'

# 原因:缺少中间迁移文件

# 解决:
# 1. 从其他分支或团队成员获取缺失的迁移文件
# 2. 或重置迁移历史(慎用)

错误 3:Target database is not up to date

# 错误信息
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:避免创建并行迁移分支

快速参考

常用命令

# 创建迁移
alembic revision --autogenerate -m "description"

# 应用迁移
alembic upgrade head
alembic upgrade +1              # 应用下一个
alembic upgrade <revision>      # 应用到指定版本

# 回滚迁移
alembic downgrade -1            # 回滚一个
alembic downgrade <revision>    # 回滚到指定版本

# 查看状态
alembic current                 # 当前版本
alembic history                 # 迁移历史
alembic check                   # 检查未迁移变更

# 合并分支
alembic merge <rev1> <rev2> -m "merge"

紧急恢复

# 数据库完全混乱时
# 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