# ADR 007: 数据库迁移最佳实践 **状态**: 已采纳 **日期**: 2026-01-30 **决策者**: 开发团队 **相关问题**: AI Service 迁移幂等性问题 ## 背景 在实施 AI Service 数据库迁移时,遇到了以下问题: 1. **JSONB GIN 索引创建失败**: 错误使用 `USING gin (column)` 而非 `USING gin (column jsonb_path_ops)` 2. **Model 与数据库不一致**: SQLModel 自动创建的表与迁移脚本定义不一致 3. **非幂等迁移**: `CREATE TABLE IF NOT EXISTS` 后续的索引和注释语句未做条件检查 4. **列类型不匹配**: 数据库中为 `json` 类型,Model 定义为 `JSONB` 类型 这些问题导致迁移脚本无法重复执行,影响开发和部署流程。 ## 决策 制定以下数据库迁移规范,所有迁移脚本必须遵守: ### 1. 迁移脚本必须完全幂等 **原则**: 迁移脚本可以安全地重复执行多次,不会产生错误或副作用。 #### 1.1 表创建 ```python # ✅ 正确 - 使用 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 列添加 ```python # ✅ 正确 - 检查列是否存在 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 索引创建 ```python # ✅ 正确 - 使用 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 约束添加 ```python # ✅ 正确 - 检查约束是否存在 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 触发器创建 ```python # ✅ 正确 - 检查触发器是否存在 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 列定义 ```python # ✅ 正确 - 明确使用 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 索引 ```python # ✅ 正确 - 使用 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`: ```python 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 移除自动创建代码 ```python # ❌ 禁止使用 from sqlmodel import SQLModel, create_engine engine = create_engine(DATABASE_URL) SQLModel.metadata.create_all(engine) # 禁止! ``` #### 3.2 仅依赖 Alembic 所有表结构变更必须通过 Alembic 迁移脚本管理: ```bash # 创建迁移 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 字段命名一致性 ```python # ✅ 正确 - 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.py` - `20260130_1900_add_user_avatar_column.py` - `20260130_2000_fix_timestamp_timezone.py` ### 6. 注释和文档 #### 6.1 表注释 ```python op.execute("COMMENT ON TABLE table_name IS '表描述';") ``` #### 6.2 列注释 ```python op.execute("COMMENT ON COLUMN table_name.column_name IS '列描述';") ``` #### 6.3 迁移脚本注释 ```python """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 本地测试 ```bash # 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 流程中添加迁移测试: ```yaml 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` **解决**: ```python 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` 检查 ## 后果 ### 优点 1. ✅ 迁移脚本完全幂等,可安全重复执行 2. ✅ 避免 SQLModel 自动创建表导致的不一致 3. ✅ JSONB 列和索引使用规范,性能最优 4. ✅ Model 定义与数据库结构完全一致 5. ✅ 迁移过程可追溯、可回滚 ### 缺点 1. ⚠️ 迁移脚本编写更复杂,需要更多条件检查 2. ⚠️ 禁用 SQLModel 自动创建后,开发时需手动执行迁移 ### 风险 1. 开发人员可能忘记遵守规范 2. 需要在 Code Review 中严格检查迁移脚本 ## 实施计划 ### 阶段 1: 规范制定(已完成) - [x] 创建 ADR 文档 - [x] 定义迁移脚本规范 - [x] 定义 JSONB 使用规范 ### 阶段 2: 现有代码修复 - [x] 修复 AI Service 迁移脚本 - [ ] 审查所有现有迁移脚本 - [ ] 修复不符合规范的迁移 ### 阶段 3: 工具和流程 - [ ] 创建迁移脚本模板 - [ ] 添加 pre-commit hook 检查 - [ ] 更新 CI/CD 流程 - [ ] 编写迁移测试工具 ### 阶段 4: 团队培训 - [ ] 团队分享会 - [ ] 更新开发文档 - [ ] Code Review Checklist ## 参考资料 - [PostgreSQL JSONB 文档](https://www.postgresql.org/docs/current/datatype-json.html) - [PostgreSQL GIN 索引](https://www.postgresql.org/docs/current/gin-intro.html) - [Alembic 文档](https://alembic.sqlalchemy.org/) - [SQLModel 文档](https://sqlmodel.tiangolo.com/) ## 相关 ADR - ADR 001: UUID v7 迁移 - ADR 002: PostgreSQL 17 UUID v7 - ADR 006: TIMESTAMPTZ 时间戳标准 ## 变更历史 - 2026-01-30: 初始版本,基于 AI Service 迁移问题总结