# Alembic 数据库迁移指南 ## 概述 Jointo 项目使用 Alembic 进行数据库版本化迁移管理。Alembic 是 SQLAlchemy 官方推荐的迁移工具,支持自动生成迁移脚本、版本管理和回滚操作。 ## 快速开始 ### 首次部署(推荐) ```bash # 进入 server 目录 cd server # 启动容器(自动执行数据库迁移) ./start_docker.sh --clean --build ``` 脚本会自动完成: 1. 清理旧容器和卷 2. 构建 Docker 镜像 3. 启动所有服务 4. **自动执行数据库迁移**(创建所有表和 UUID v7 函数) 5. 创建迁移标记文件 ### 后续部署 ```bash # 快速重启(不执行迁移) ./start_docker.sh # 如果更新了数据库模型,手动执行迁移 ./migrate_db.sh ``` ### 0. 全新环境初始化 **首次部署时直接执行迁移即可**: ```bash # 启动容器(UUID v7 函数会自动创建) ./start_docker.sh --clean --build ``` ⚠️ **注意**: - 使用 `./start_docker.sh` 启动容器时,首次部署会自动执行迁移 - 初始化迁移脚本(`3a3a2a1417de`)会自动创建 UUID v7 函数 - 后续部署需要手动执行迁移:`./migrate_db.sh` ### 1. 创建新迁移 **自动生成迁移(推荐)**: ```bash # 方式 1:使用便捷脚本 python scripts/db_migrate.py create "添加用户表" --autogenerate # 方式 2:使用 Shell 脚本 ./migrate_db.sh create "添加用户表" # 方式 3:直接使用 Alembic cd server alembic revision --autogenerate -m "添加用户表" ``` **手动创建空白迁移**: ```bash python scripts/db_migrate.py create "自定义迁移" ``` ### 2. 执行迁移 ```bash # 升级到最新版本 python scripts/db_migrate.py upgrade # 或 ./migrate_db.sh # 升级一个版本 python scripts/db_migrate.py upgrade +1 # 升级到指定版本 python scripts/db_migrate.py upgrade <版本号> ``` ### 3. 回滚迁移 ```bash # 回滚一个版本 python scripts/db_migrate.py downgrade # 或 ./migrate_db.sh downgrade # 回滚两个版本 python scripts/db_migrate.py downgrade -2 # 回滚到指定版本 python scripts/db_migrate.py downgrade <版本号> # 回滚所有迁移 python scripts/db_migrate.py downgrade base ``` ### 4. 查看状态 ```bash # 查看当前版本 python scripts/db_migrate.py current # 查看迁移历史 python scripts/db_migrate.py history # 查看最新版本 python scripts/db_migrate.py heads ``` ## 工作流程 ### 典型开发流程 1. **修改模型** ```python # server/app/models/user.py class User(SQLModel, table=True): __tablename__ = "users" user_id: UUID = Field(default_factory=uuid7, primary_key=True) username: str = Field(max_length=255, unique=True) email: Optional[str] = Field(default=None, max_length=255) # 新增字段 phone: Optional[str] = Field(default=None, max_length=20) ``` 2. **生成迁移脚本** ```bash python scripts/db_migrate.py create "添加用户手机号字段" --autogenerate ``` 3. **检查生成的迁移文件** ```python # server/alembic/versions/20260127_1430_abc123_添加用户手机号字段.py def upgrade() -> None: op.add_column('users', sa.Column('phone', sa.String(length=20), nullable=True)) def downgrade() -> None: op.drop_column('users', 'phone') ``` 4. **执行迁移** ```bash python scripts/db_migrate.py upgrade ``` 5. **测试回滚** ```bash # 开发环境测试回滚 python scripts/db_migrate.py downgrade python scripts/db_migrate.py upgrade ``` 6. **提交代码** ```bash git add server/alembic/versions/ git commit -m "feat: 添加用户手机号字段" ``` ## 迁移文件结构 ### 文件命名 Alembic 自动生成的文件名格式: ``` YYYYMMDD_HHMM__.py ``` 示例: ``` 20260127_1430_abc123def456_添加用户手机号字段.py ``` ### 文件内容 ```python """添加用户手机号字段 Revision ID: abc123def456 Revises: xyz789 Create Date: 2026-01-27 14:30:00.000000 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa import sqlmodel # revision identifiers, used by Alembic. revision: str = 'abc123def456' down_revision: Union[str, None] = 'xyz789' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """升级数据库""" op.add_column('users', sa.Column('phone', sa.String(length=20), nullable=True)) op.create_index('ix_users_phone', 'users', ['phone']) def downgrade() -> None: """回滚数据库""" op.drop_index('ix_users_phone', table_name='users') op.drop_column('users', 'phone') ``` ## 常见操作 ### 添加表 ```python def upgrade() -> None: op.create_table( 'products', sa.Column('id', sa.UUID(), nullable=False), sa.Column('name', sa.String(length=255), nullable=False), sa.Column('price', sa.Numeric(10, 2), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), sa.PrimaryKeyConstraint('id') ) op.create_index('ix_products_name', 'products', ['name']) def downgrade() -> None: op.drop_index('ix_products_name', table_name='products') op.drop_table('products') ``` ### 添加列 ```python def upgrade() -> None: op.add_column('users', sa.Column('bio', sa.Text(), nullable=True)) def downgrade() -> None: op.drop_column('users', 'bio') ``` ### 修改列 ```python def upgrade() -> None: # 修改列类型 op.alter_column('users', 'username', existing_type=sa.String(length=100), type_=sa.String(length=255), existing_nullable=False) # 修改列默认值 op.alter_column('users', 'is_active', server_default=sa.text('true')) def downgrade() -> None: op.alter_column('users', 'username', existing_type=sa.String(length=255), type_=sa.String(length=100), existing_nullable=False) op.alter_column('users', 'is_active', server_default=None) ``` ### 重命名列 ```python def upgrade() -> None: op.alter_column('users', 'old_name', new_column_name='new_name') def downgrade() -> None: op.alter_column('users', 'new_name', new_column_name='old_name') ``` ### 添加索引 ```python def upgrade() -> None: op.create_index('ix_users_email', 'users', ['email']) # 唯一索引 op.create_index('ix_users_username', 'users', ['username'], unique=True) # 复合索引 op.create_index('ix_users_phone_country', 'users', ['phone', 'country_code']) def downgrade() -> None: op.drop_index('ix_users_phone_country', table_name='users') op.drop_index('ix_users_username', table_name='users') op.drop_index('ix_users_email', table_name='users') ``` ### 数据迁移 ```python from sqlalchemy import text def upgrade() -> None: # 添加新列 op.add_column('users', sa.Column('full_name', sa.String(length=255), nullable=True)) # 数据迁移 connection = op.get_bind() connection.execute( text("UPDATE users SET full_name = CONCAT(first_name, ' ', last_name)") ) # 设置为非空 op.alter_column('users', 'full_name', nullable=False) def downgrade() -> None: op.drop_column('users', 'full_name') ``` ## Jointo 项目规范 ### 1. 禁止外键约束 根据 Jointo 技术栈规范,**禁止在数据库层创建物理外键**。 ❌ **错误示例**: ```python def upgrade() -> None: op.create_table( 'projects', sa.Column('id', sa.UUID(), nullable=False), sa.Column('user_id', sa.UUID(), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['users.user_id']), # ❌ 禁止 sa.PrimaryKeyConstraint('id') ) ``` ✅ **正确示例**: ```python def upgrade() -> None: op.create_table( 'projects', sa.Column('id', sa.UUID(), nullable=False), sa.Column('user_id', sa.UUID(), nullable=False), # 仅字段,无外键约束 sa.PrimaryKeyConstraint('id') ) # 创建索引以提高查询性能 op.create_index('ix_projects_user_id', 'projects', ['user_id']) ``` ### 2. 枚举类型使用 SMALLINT ❌ **错误示例**: ```python def upgrade() -> None: op.create_table( 'projects', sa.Column('status', sa.Enum('active', 'archived', name='project_status'), nullable=False) # ❌ ) ``` ✅ **正确示例**: ```python def upgrade() -> None: op.create_table( 'projects', sa.Column('status', sa.SmallInteger(), nullable=False), # ✅ 使用 SMALLINT sa.PrimaryKeyConstraint('id') ) # 在 Python 模型中使用 IntEnum 映射 ``` ### 3. UUID v7 主键 ```python def upgrade() -> None: op.create_table( 'users', sa.Column('user_id', sa.UUID(), server_default=sa.text('gen_uuid_v7()'), nullable=False), sa.PrimaryKeyConstraint('user_id') ) ``` ### 4. 时间戳字段 ```python def upgrade() -> None: op.create_table( 'users', sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), # 软删除 sa.PrimaryKeyConstraint('user_id') ) # 添加 updated_at 自动更新触发器 op.execute(""" CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); """) def downgrade() -> None: op.execute("DROP TRIGGER IF EXISTS update_users_updated_at ON users") op.drop_table('users') ``` ## 高级用法 ### 条件迁移 ```python from alembic import op import sqlalchemy as sa from sqlalchemy import inspect def upgrade() -> None: conn = op.get_bind() inspector = inspect(conn) # 检查列是否存在 if 'phone' not in [c['name'] for c in inspector.get_columns('users')]: op.add_column('users', sa.Column('phone', sa.String(length=20), nullable=True)) ``` ### 批量操作 ```python def upgrade() -> None: with op.batch_alter_table('users') as batch_op: batch_op.add_column(sa.Column('phone', sa.String(length=20), nullable=True)) batch_op.add_column(sa.Column('bio', sa.Text(), nullable=True)) batch_op.create_index('ix_users_phone', ['phone']) ``` ### 执行原始 SQL ```python def upgrade() -> None: op.execute(""" CREATE OR REPLACE FUNCTION gen_uuid_v7() RETURNS UUID AS $$ BEGIN RETURN uuid_generate_v7(); END; $$ LANGUAGE plpgsql; """) def downgrade() -> None: op.execute("DROP FUNCTION IF EXISTS gen_uuid_v7()") ``` ## 故障排查 ### 问题 1:迁移冲突 **症状**:多个分支同时创建迁移,导致版本冲突 **解决**: ```bash # 查看当前头部 alembic heads # 合并分支 alembic merge -m "合并迁移" ``` ### 问题 2:迁移失败 **症状**:执行迁移时出错,数据库处于不一致状态 **解决**: ```bash # 1. 查看当前版本 python scripts/db_migrate.py current # 2. 手动修复数据库 # 3. 标记为已迁移(不执行) python scripts/db_migrate.py stamp ``` ### 问题 3:自动生成不准确 **症状**:autogenerate 生成的迁移不符合预期 **解决**: 1. 检查所有模型是否已导入到 `alembic/env.py` 2. 手动编辑生成的迁移文件 3. 测试 upgrade 和 downgrade ## 最佳实践 ### 1. 迁移原则 - ✅ 每次模型变更立即创建迁移 - ✅ 迁移描述清晰明确 - ✅ 提交前测试 upgrade 和 downgrade - ✅ 生产环境部署前备份数据库 - ✅ 使用事务确保原子性 ### 2. 命名规范 - 使用动词开头:`添加`、`修改`、`删除` - 包含表名和字段名 - 示例: - `添加用户手机号字段` - `修改项目状态枚举` - `删除旧的会话表` ### 3. 代码审查 迁移文件必须经过代码审查: - 检查是否符合 Jointo 规范 - 验证 downgrade 逻辑正确 - 确认数据迁移安全 ### 4. 生产环境部署 ```bash # 1. 备份数据库 pg_dump -U postgres jointo > backup_$(date +%Y%m%d_%H%M%S).sql # 2. 查看待执行的迁移 python scripts/db_migrate.py history # 3. 执行迁移 python scripts/db_migrate.py upgrade # 4. 验证结果 python scripts/db_migrate.py current ``` ## 全新环境部署 ### 首次部署(空数据库) 全新环境可以直接执行迁移,自动创建所有表结构: ```bash # 1. 启动 Docker 容器 docker compose up -d # 2. 执行迁移(自动创建所有表) ./migrate_db.sh # 或 docker exec jointo-server-app alembic upgrade head # 3. 验证 docker exec jointo-server-app alembic current # 输出:c496e41f07ff (head) ``` ### 现有环境(表已存在) 如果数据库已经有表结构,需要标记为已应用: ```bash # 标记为已应用最新迁移(不执行建表) docker exec jointo-server-app alembic stamp head # 验证 docker exec jointo-server-app alembic current # 输出:c496e41f07ff (head) ``` ## 从旧迁移系统迁移 如果你的数据库使用旧的迁移系统(`app/migrations/`),已经废弃,请使用 Alembic: ### 步骤 1:标记当前状态 ```bash # 标记为已应用最新迁移(不执行) docker exec jointo-server-app alembic stamp head ``` ### 步骤 2:验证 ```bash # 查看当前版本 docker exec jointo-server-app alembic current # 查看历史 docker exec jointo-server-app alembic history ``` ### 步骤 3:归档旧迁移 旧迁移文件已移至 `server/app/migrations/` 并标记为废弃,无需手动操作。 ## 参考资料 - [Alembic 官方文档](https://alembic.sqlalchemy.org/) - [SQLAlchemy 文档](https://docs.sqlalchemy.org/) - [Jointo 技术栈规范](../../.claude/skills/jointo-tech-stack/references/database.md) ## 常见命令速查 ```bash # 创建迁移 python scripts/db_migrate.py create "描述" --autogenerate # 执行迁移 python scripts/db_migrate.py upgrade # 回滚迁移 python scripts/db_migrate.py downgrade # 查看状态 python scripts/db_migrate.py current python scripts/db_migrate.py history # 标记版本 python scripts/db_migrate.py stamp head ```