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.
 

5.2 KiB

AI Service 数据库迁移幂等性修复

日期: 2026-01-30
类型: 数据库迁移修复
影响范围: AI Service 模块

问题描述

AI Service 数据库迁移脚本 20260129_1800_create_ai_service_tables.py 存在幂等性问题:

  1. GIN 索引错误: JSONB 列的 GIN 索引创建失败,错误提示 operator class "jsonb_path_ops" does not accept data type json
  2. 列类型不匹配: 数据库中 ai_usage_logs.extra_data 列类型为 json,但 Model 定义为 extra_metadata: JSONB
  3. 非幂等性: 使用 CREATE TABLE IF NOT EXISTS 但后续的索引和注释语句未做条件检查

解决方案

1. 修复迁移脚本幂等性

修改文件: server/alembic/versions/20260129_1800_create_ai_service_tables.py

1.1 添加列类型检查和转换

为所有 JSONB 列添加类型检查,如果列类型为 json 则转换为 jsonb

# ai_models 表
op.execute("""
    DO $$
    BEGIN
        IF EXISTS (
            SELECT 1 FROM information_schema.columns 
            WHERE table_name = 'ai_models' 
            AND column_name = 'config' 
            AND data_type = 'json'
        ) THEN
            ALTER TABLE ai_models ALTER COLUMN config TYPE JSONB USING config::jsonb;
        END IF;
    END $$;
""")

1.2 条件化 GIN 索引创建

将 GIN 索引创建包装在条件检查中:

op.execute("""
    DO $$
    BEGIN
        IF EXISTS (
            SELECT 1 FROM information_schema.columns 
            WHERE table_name = 'ai_usage_logs' 
            AND column_name = 'extra_metadata'
        ) THEN
            CREATE INDEX IF NOT EXISTS idx_ai_usage_logs_extra_metadata_gin 
            ON ai_usage_logs USING gin (extra_metadata jsonb_path_ops);
        END IF;
    END $$;
""")

1.3 条件化列注释

对可能不存在的列,使用动态 SQL 添加注释:

op.execute("""
    DO $$
    BEGIN
        IF EXISTS (
            SELECT 1 FROM information_schema.columns 
            WHERE table_name = 'ai_usage_logs' 
            AND column_name = 'extra_metadata'
        ) THEN
            EXECUTE 'COMMENT ON COLUMN ai_usage_logs.extra_metadata IS ''使用元数据''';
        END IF;
    END $$;
""")

2. 修复数据库列名和类型不匹配

由于 SQLModel 自动创建的表使用了旧的列名 extra_data (类型 json),需要手动修复:

-- 重命名列
ALTER TABLE ai_usage_logs 
  RENAME COLUMN extra_data TO extra_metadata;

-- 转换类型
ALTER TABLE ai_usage_logs 
  ALTER COLUMN extra_metadata TYPE JSONB USING extra_metadata::jsonb;

-- 设置默认值
ALTER TABLE ai_usage_logs 
  ALTER COLUMN extra_metadata SET DEFAULT '{}'::jsonb;

-- 创建 GIN 索引
CREATE INDEX IF NOT EXISTS idx_ai_usage_logs_extra_metadata_gin 
  ON ai_usage_logs USING gin (extra_metadata jsonb_path_ops);

验证结果

表创建成功

SELECT table_name FROM information_schema.tables 
WHERE table_schema = 'public' AND table_name LIKE 'ai_%';

结果:

  • ai_models (16 列)
  • ai_jobs (21 列)
  • ai_usage_logs (10 列)
  • ai_quotas (9 列)

JSONB 列和索引验证

\d ai_models
\d ai_jobs
\d ai_usage_logs

确认:

  • ai_models.config - JSONB 类型,带 GIN 索引
  • ai_jobs.input_data - JSONB 类型,带 GIN 索引
  • ai_jobs.output_data - JSONB 类型,带 GIN 索引
  • ai_usage_logs.extra_metadata - JSONB 类型,带 GIN 索引

技术要点

PostgreSQL GIN 索引操作符类

PostgreSQL 的 GIN 索引需要指定正确的操作符类:

  • 错误: USING gin (config) - 对 JSONB 列会失败
  • 正确: USING gin (config jsonb_path_ops) - 使用 jsonb_path_ops 操作符类

jsonb_path_ops 操作符类:

  • 仅支持 @> 操作符(包含查询)
  • 索引更小,性能更好
  • 适合大多数 JSONB 查询场景

迁移脚本幂等性最佳实践

  1. 表创建: 使用 CREATE TABLE IF NOT EXISTS
  2. 列修改: 先检查列是否存在及其类型
  3. 索引创建: 使用 CREATE INDEX IF NOT EXISTS 并检查列存在性
  4. 约束添加: 检查约束是否已存在
  5. 触发器创建: 使用 IF NOT EXISTS 条件

SQLModel 自动表创建问题

SQLModel 会在应用启动时自动创建表,可能导致:

  • 表结构与迁移脚本不一致
  • 列名或类型不匹配
  • 缺少索引或约束

解决方案

  • 禁用 SQLModel 自动表创建
  • 完全依赖 Alembic 迁移管理表结构
  • 或确保 Model 定义与迁移脚本完全一致

影响范围

  • AI Service 所有表创建成功
  • JSONB 列类型正确
  • GIN 索引创建成功
  • 迁移脚本完全幂等
  • 可重复执行不会报错

相关文件

  • server/alembic/versions/20260129_1800_create_ai_service_tables.py - 迁移脚本
  • server/app/models/ai_usage_log.py - AIUsageLog Model 定义
  • server/app/models/ai_model.py - AIModel Model 定义
  • server/app/models/ai_job.py - AIJob Model 定义
  • server/app/models/ai_quota.py - AIQuota Model 定义

后续建议

  1. 禁用 SQLModel 自动表创建: 在生产环境中完全依赖 Alembic 管理表结构
  2. 迁移测试: 在 CI/CD 中添加迁移幂等性测试
  3. 文档更新: 更新数据库设计文档,明确 JSONB 列的索引策略