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
14 KiB
Alembic 数据库迁移指南
概述
Jointo 项目使用 Alembic 进行数据库版本化迁移管理。Alembic 是 SQLAlchemy 官方推荐的迁移工具,支持自动生成迁移脚本、版本管理和回滚操作。
快速开始
首次部署(推荐)
# 进入 server 目录
cd server
# 启动容器(自动执行数据库迁移)
./start_docker.sh --clean --build
脚本会自动完成:
- 清理旧容器和卷
- 构建 Docker 镜像
- 启动所有服务
- 自动执行数据库迁移(创建所有表和 UUID v7 函数)
- 创建迁移标记文件
后续部署
# 快速重启(不执行迁移)
./start_docker.sh
# 如果更新了数据库模型,手动执行迁移
./migrate_db.sh
0. 全新环境初始化
首次部署时直接执行迁移即可:
# 启动容器(UUID v7 函数会自动创建)
./start_docker.sh --clean --build
⚠️ 注意:
- 使用
./start_docker.sh启动容器时,首次部署会自动执行迁移 - 初始化迁移脚本(
3a3a2a1417de)会自动创建 UUID v7 函数 - 后续部署需要手动执行迁移:
./migrate_db.sh
1. 创建新迁移
自动生成迁移(推荐):
# 方式 1:使用便捷脚本
python scripts/db_migrate.py create "添加用户表" --autogenerate
# 方式 2:使用 Shell 脚本
./migrate_db.sh create "添加用户表"
# 方式 3:直接使用 Alembic
cd server
alembic revision --autogenerate -m "添加用户表"
手动创建空白迁移:
python scripts/db_migrate.py create "自定义迁移"
2. 执行迁移
# 升级到最新版本
python scripts/db_migrate.py upgrade
# 或
./migrate_db.sh
# 升级一个版本
python scripts/db_migrate.py upgrade +1
# 升级到指定版本
python scripts/db_migrate.py upgrade <版本号>
3. 回滚迁移
# 回滚一个版本
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. 查看状态
# 查看当前版本
python scripts/db_migrate.py current
# 查看迁移历史
python scripts/db_migrate.py history
# 查看最新版本
python scripts/db_migrate.py heads
工作流程
典型开发流程
-
修改模型
# 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) -
生成迁移脚本
python scripts/db_migrate.py create "添加用户手机号字段" --autogenerate -
检查生成的迁移文件
# 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') -
执行迁移
python scripts/db_migrate.py upgrade -
测试回滚
# 开发环境测试回滚 python scripts/db_migrate.py downgrade python scripts/db_migrate.py upgrade -
提交代码
git add server/alembic/versions/ git commit -m "feat: 添加用户手机号字段"
迁移文件结构
文件命名
Alembic 自动生成的文件名格式:
YYYYMMDD_HHMM_<revision>_<slug>.py
示例:
20260127_1430_abc123def456_添加用户手机号字段.py
文件内容
"""添加用户手机号字段
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')
常见操作
添加表
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')
添加列
def upgrade() -> None:
op.add_column('users', sa.Column('bio', sa.Text(), nullable=True))
def downgrade() -> None:
op.drop_column('users', 'bio')
修改列
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)
重命名列
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')
添加索引
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')
数据迁移
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 技术栈规范,禁止在数据库层创建物理外键。
❌ 错误示例:
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')
)
✅ 正确示例:
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
❌ 错误示例:
def upgrade() -> None:
op.create_table(
'projects',
sa.Column('status', sa.Enum('active', 'archived', name='project_status'), nullable=False) # ❌
)
✅ 正确示例:
def upgrade() -> None:
op.create_table(
'projects',
sa.Column('status', sa.SmallInteger(), nullable=False), # ✅ 使用 SMALLINT
sa.PrimaryKeyConstraint('id')
)
# 在 Python 模型中使用 IntEnum 映射
3. UUID v7 主键
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. 时间戳字段
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')
高级用法
条件迁移
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))
批量操作
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
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:迁移冲突
症状:多个分支同时创建迁移,导致版本冲突
解决:
# 查看当前头部
alembic heads
# 合并分支
alembic merge <revision1> <revision2> -m "合并迁移"
问题 2:迁移失败
症状:执行迁移时出错,数据库处于不一致状态
解决:
# 1. 查看当前版本
python scripts/db_migrate.py current
# 2. 手动修复数据库
# 3. 标记为已迁移(不执行)
python scripts/db_migrate.py stamp <revision>
问题 3:自动生成不准确
症状:autogenerate 生成的迁移不符合预期
解决:
- 检查所有模型是否已导入到
alembic/env.py - 手动编辑生成的迁移文件
- 测试 upgrade 和 downgrade
最佳实践
1. 迁移原则
- ✅ 每次模型变更立即创建迁移
- ✅ 迁移描述清晰明确
- ✅ 提交前测试 upgrade 和 downgrade
- ✅ 生产环境部署前备份数据库
- ✅ 使用事务确保原子性
2. 命名规范
- 使用动词开头:
添加、修改、删除 - 包含表名和字段名
- 示例:
添加用户手机号字段修改项目状态枚举删除旧的会话表
3. 代码审查
迁移文件必须经过代码审查:
- 检查是否符合 Jointo 规范
- 验证 downgrade 逻辑正确
- 确认数据迁移安全
4. 生产环境部署
# 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
全新环境部署
首次部署(空数据库)
全新环境可以直接执行迁移,自动创建所有表结构:
# 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)
现有环境(表已存在)
如果数据库已经有表结构,需要标记为已应用:
# 标记为已应用最新迁移(不执行建表)
docker exec jointo-server-app alembic stamp head
# 验证
docker exec jointo-server-app alembic current
# 输出:c496e41f07ff (head)
从旧迁移系统迁移
如果你的数据库使用旧的迁移系统(app/migrations/),已经废弃,请使用 Alembic:
步骤 1:标记当前状态
# 标记为已应用最新迁移(不执行)
docker exec jointo-server-app alembic stamp head
步骤 2:验证
# 查看当前版本
docker exec jointo-server-app alembic current
# 查看历史
docker exec jointo-server-app alembic history
步骤 3:归档旧迁移
旧迁移文件已移至 server/app/migrations/ 并标记为废弃,无需手动操作。
参考资料
常见命令速查
# 创建迁移
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