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.0 KiB

UUID 生成迁移到应用层

日期: 2026-01-27
类型: 架构变更
影响: 数据库迁移、Model 定义

变更概述

将 UUID v7 生成从数据库层迁移到应用层,移除数据库 gen_uuid_v7() 函数和所有 server_default 设置。

变更原因

架构一致性

与"禁止物理外键"的架构理念保持一致,所有业务逻辑集中在应用层。

技术优势

  1. 应用层控制 - UUID 生成逻辑在 Python 代码中,便于测试和 Mock
  2. 数据库独立 - 减少对 PostgreSQL 特定功能的依赖,提高可移植性
  3. 简化迁移 - 数据库迁移脚本更简洁,无需管理自定义函数
  4. 统一管理 - 所有 ID 生成逻辑在 app/utils/id_generator.py 统一管理

具体变更

1. 移除数据库函数

之前

-- 迁移脚本中创建函数
CREATE OR REPLACE FUNCTION gen_uuid_v7() RETURNS uuid AS $$
DECLARE
  unix_ts_ms BIGINT;
  uuid_bytes BYTEA;
BEGIN
  unix_ts_ms = (EXTRACT(EPOCH FROM CLOCK_TIMESTAMP()) * 1000)::BIGINT;
  uuid_bytes = SUBSTRING(INT8SEND(unix_ts_ms) FROM 3 FOR 6) || gen_random_bytes(10);
  RETURN ENCODE(..., 'hex')::UUID;
END;
$$ LANGUAGE plpgsql VOLATILE;

之后

# 应用层生成
from app.utils.id_generator import generate_uuid

id = generate_uuid()  # 使用 uuid_utils 库

2. 移除 server_default

之前

# 迁移脚本
op.create_table('users',
    sa.Column('user_id', sa.UUID(), server_default=sa.text('gen_uuid_v7()'), nullable=False),
    ...
)

之后

# 迁移脚本
op.create_table('users',
    sa.Column('user_id', sa.UUID(), nullable=False),  # 无 server_default
    ...
)

3. Model 定义

之前

class User(SQLModel, table=True):
    user_id: UUID = Field(
        sa_column=Column(PG_UUID(as_uuid=True), primary_key=True)
        # 依赖数据库 server_default
    )

之后

from app.utils.id_generator import generate_uuid

class User(SQLModel, table=True):
    user_id: UUID = Field(
        sa_column=Column(
            PG_UUID(as_uuid=True),
            primary_key=True,
            default=generate_uuid  # ✅ 应用层生成
        )
    )

4. 移除物理外键

同时移除了所有模型中的 ForeignKeyforeign_key 定义:

之前

owner_id: UUID = Field(foreign_key="users.user_id", index=True)
# 或
owner_id: UUID = Field(
    sa_column=Column(PG_UUID(as_uuid=True), ForeignKey("users.user_id"), index=True)
)

之后

owner_id: UUID = Field(
    sa_column=Column(PG_UUID(as_uuid=True), nullable=False, index=True),
    description="所有者用户ID - 应用层验证"
)

影响的文件

Model 文件(移除 ForeignKey)

  • server/app/models/ai_job.py
  • server/app/models/ai_quota.py
  • server/app/models/ai_usage_log.py
  • server/app/models/folder.py
  • server/app/models/project.py

迁移脚本(移除 UUID 函数和 server_default)

  • server/alembic/versions/20260127_1931_3a3a2a1417de_initial_schema_初始化所有表结构.py

数据库初始化(改用 Alembic)

  • server/app/core/database.py - 移除 SQLModel.metadata.create_all(),改用 Alembic 迁移

验证结果

-- ✅ 无物理外键
SELECT * FROM information_schema.table_constraints 
WHERE constraint_type = 'FOREIGN KEY';
-- (0 rows)

-- ✅ 无 UUID 生成函数
SELECT proname FROM pg_proc WHERE proname = 'gen_uuid_v7';
-- (0 rows)

-- ✅ 所有 UUID 字段无默认值
SELECT table_name, column_name, column_default
FROM information_schema.columns
WHERE data_type = 'uuid' AND column_default IS NOT NULL;
-- (0 rows)

迁移指南

新项目

直接使用更新后的迁移脚本和模型定义。

现有项目

  1. 清理数据库

    ./start_docker.sh -c -b  # 清理并重建
    
  2. 验证迁移

    docker exec jointo-server-app alembic current
    # 输出: 3a3a2a1417de (head)
    
  3. 验证无外键

    docker exec jointo-server-postgres psql -U jointoAI -d jointo -c "
    SELECT table_name, constraint_name 
    FROM information_schema.table_constraints 
    WHERE constraint_type = 'FOREIGN KEY';
    "
    # 输出: (0 rows)
    

注意事项

  1. 应用层必须验证引用完整性 - 在 Service 层检查关联记录是否存在
  2. 所有关联字段必须有索引 - 保证查询性能
  3. UUID 生成器已封装 - 使用 app/utils/id_generator.pygenerate_uuid()
  4. 测试时可 Mock - generate_uuid 函数便于单元测试

相关文档

总结

这次变更将 UUID 生成和外键约束从数据库层完全移除,实现了"应用层控制、数据库层简化"的架构目标,为后续的分库分表和微服务化奠定了基础。