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.
9.3 KiB
9.3 KiB
ADR 007: 数据库迁移最佳实践
状态: 已采纳
日期: 2026-01-30
决策者: 开发团队
相关问题: AI Service 迁移幂等性问题
背景
在实施 AI Service 数据库迁移时,遇到了以下问题:
- JSONB GIN 索引创建失败: 错误使用
USING gin (column)而非USING gin (column jsonb_path_ops) - Model 与数据库不一致: SQLModel 自动创建的表与迁移脚本定义不一致
- 非幂等迁移:
CREATE TABLE IF NOT EXISTS后续的索引和注释语句未做条件检查 - 列类型不匹配: 数据库中为
json类型,Model 定义为JSONB类型
这些问题导致迁移脚本无法重复执行,影响开发和部署流程。
决策
制定以下数据库迁移规范,所有迁移脚本必须遵守:
1. 迁移脚本必须完全幂等
原则: 迁移脚本可以安全地重复执行多次,不会产生错误或副作用。
1.1 表创建
# ✅ 正确 - 使用 IF NOT EXISTS
op.execute("""
CREATE TABLE IF NOT EXISTS table_name (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
data JSONB NOT NULL DEFAULT '{}'
);
""")
# ❌ 错误 - 直接创建
op.create_table('table_name', ...)
1.2 列添加
# ✅ 正确 - 检查列是否存在
op.execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'table_name'
AND column_name = 'new_column'
) THEN
ALTER TABLE table_name ADD COLUMN new_column TEXT;
END IF;
END $$;
""")
# ❌ 错误 - 直接添加
op.add_column('table_name', sa.Column('new_column', sa.Text))
1.3 索引创建
# ✅ 正确 - 使用 IF NOT EXISTS 并检查列存在
op.execute("""
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'table_name'
AND column_name = 'column_name'
) THEN
CREATE INDEX IF NOT EXISTS idx_name
ON table_name (column_name);
END IF;
END $$;
""")
# ❌ 错误 - 直接创建
op.create_index('idx_name', 'table_name', ['column_name'])
1.4 约束添加
# ✅ 正确 - 检查约束是否存在
op.execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'constraint_name'
) THEN
ALTER TABLE table_name
ADD CONSTRAINT constraint_name CHECK (condition);
END IF;
END $$;
""")
1.5 触发器创建
# ✅ 正确 - 检查触发器是否存在
op.execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'trigger_name'
) THEN
CREATE TRIGGER trigger_name
BEFORE UPDATE ON table_name
FOR EACH ROW
EXECUTE FUNCTION function_name();
END IF;
END $$;
""")
2. JSONB 列和索引规范
2.1 列定义
# ✅ 正确 - 明确使用 JSONB 类型
op.execute("""
CREATE TABLE IF NOT EXISTS table_name (
id UUID PRIMARY KEY,
config JSONB NOT NULL DEFAULT '{}',
metadata JSONB
);
""")
# ❌ 错误 - 使用 JSON 类型
config JSON NOT NULL DEFAULT '{}'
2.2 GIN 索引
# ✅ 正确 - 使用 jsonb_path_ops 操作符类
op.execute("""
CREATE INDEX IF NOT EXISTS idx_table_config_gin
ON table_name USING gin (config jsonb_path_ops);
""")
# ❌ 错误 - 缺少操作符类
CREATE INDEX idx_table_config_gin
ON table_name USING gin (config);
操作符类说明:
jsonb_path_ops: 仅支持@>操作符,索引更小,性能更好(推荐)jsonb_ops: 支持所有 JSONB 操作符,索引更大
2.3 类型转换
如果需要将 json 转换为 jsonb:
op.execute("""
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'table_name'
AND column_name = 'config'
AND data_type = 'json'
) THEN
ALTER TABLE table_name
ALTER COLUMN config TYPE JSONB USING config::jsonb;
END IF;
END $$;
""")
3. 禁止 SQLModel 自动创建表
问题: SQLModel 的 create_all() 会在应用启动时自动创建表,可能导致:
- 表结构与迁移脚本不一致
- 列名、类型不匹配
- 缺少索引、约束、注释
解决方案:
3.1 移除自动创建代码
# ❌ 禁止使用
from sqlmodel import SQLModel, create_engine
engine = create_engine(DATABASE_URL)
SQLModel.metadata.create_all(engine) # 禁止!
3.2 仅依赖 Alembic
所有表结构变更必须通过 Alembic 迁移脚本管理:
# 创建迁移
alembic revision --autogenerate -m "description"
# 执行迁移
alembic upgrade head
# 回滚迁移
alembic downgrade -1
4. Model 定义与迁移脚本一致性
4.1 字段类型映射
| Python 类型 | SQLModel 类型 | PostgreSQL 类型 | 说明 |
|---|---|---|---|
dict |
JSONB |
JSONB |
✅ 推荐 |
dict |
JSON |
JSON |
❌ 避免使用 |
int |
SmallInteger |
SMALLINT |
枚举类型 |
datetime |
TIMESTAMP(timezone=True) |
TIMESTAMPTZ |
时间戳 |
UUID |
PG_UUID(as_uuid=True) |
UUID |
主键/外键 |
4.2 字段命名一致性
# ✅ 正确 - Model 字段名与数据库列名一致
class AIUsageLog(SQLModel, table=True):
extra_metadata: dict = Field(
default_factory=dict,
sa_column=Column(JSONB, ...)
)
# ❌ 错误 - 字段名不一致
class AIUsageLog(SQLModel, table=True):
extra_data: dict = Field(...) # 数据库列名是 extra_metadata
5. 迁移脚本命名规范
YYYYMMDD_HHMM_description.py
示例:
20260130_1800_create_ai_service_tables.py20260130_1900_add_user_avatar_column.py20260130_2000_fix_timestamp_timezone.py
6. 注释和文档
6.1 表注释
op.execute("COMMENT ON TABLE table_name IS '表描述';")
6.2 列注释
op.execute("COMMENT ON COLUMN table_name.column_name IS '列描述';")
6.3 迁移脚本注释
"""create ai service tables
Revision ID: 20260129_1800
Revises: 20260129_1700
Create Date: 2026-01-29 18:00:00.000000
说明:
- 创建 AI Service 相关的 4 个表
- 使用 JSONB 类型存储配置和元数据
- 所有外键关系在应用层维护
- 完全幂等,可重复执行
"""
7. 迁移测试
7.1 本地测试
# 1. 执行迁移
alembic upgrade head
# 2. 验证表结构
psql -U user -d db -c "\d table_name"
# 3. 回滚测试
alembic downgrade -1
# 4. 重新执行(幂等性测试)
alembic upgrade head
alembic upgrade head # 第二次执行不应报错
7.2 CI/CD 测试
在 CI/CD 流程中添加迁移测试:
test-migrations:
script:
- docker-compose up -d postgres
- alembic upgrade head
- alembic upgrade head # 幂等性测试
- alembic downgrade base
- alembic upgrade head # 完整流程测试
8. 常见错误和解决方案
8.1 GIN 索引错误
错误: operator class "jsonb_path_ops" does not accept data type json
原因: 列类型是 json 而非 jsonb
解决:
ALTER TABLE table_name
ALTER COLUMN column_name TYPE JSONB USING column_name::jsonb;
8.2 列不存在错误
错误: column "column_name" does not exist
原因: 在索引或注释语句中引用了不存在的列
解决: 添加列存在性检查
8.3 约束冲突
错误: constraint "constraint_name" already exists
原因: 重复创建约束
解决: 使用 IF NOT EXISTS 检查
后果
优点
- ✅ 迁移脚本完全幂等,可安全重复执行
- ✅ 避免 SQLModel 自动创建表导致的不一致
- ✅ JSONB 列和索引使用规范,性能最优
- ✅ Model 定义与数据库结构完全一致
- ✅ 迁移过程可追溯、可回滚
缺点
- ⚠️ 迁移脚本编写更复杂,需要更多条件检查
- ⚠️ 禁用 SQLModel 自动创建后,开发时需手动执行迁移
风险
- 开发人员可能忘记遵守规范
- 需要在 Code Review 中严格检查迁移脚本
实施计划
阶段 1: 规范制定(已完成)
- 创建 ADR 文档
- 定义迁移脚本规范
- 定义 JSONB 使用规范
阶段 2: 现有代码修复
- 修复 AI Service 迁移脚本
- 审查所有现有迁移脚本
- 修复不符合规范的迁移
阶段 3: 工具和流程
- 创建迁移脚本模板
- 添加 pre-commit hook 检查
- 更新 CI/CD 流程
- 编写迁移测试工具
阶段 4: 团队培训
- 团队分享会
- 更新开发文档
- Code Review Checklist
参考资料
相关 ADR
- ADR 001: UUID v7 迁移
- ADR 002: PostgreSQL 17 UUID v7
- ADR 006: TIMESTAMPTZ 时间戳标准
变更历史
- 2026-01-30: 初始版本,基于 AI Service 迁移问题总结