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
5.0 KiB
UUID 生成迁移到应用层
日期: 2026-01-27
类型: 架构变更
影响: 数据库迁移、Model 定义
变更概述
将 UUID v7 生成从数据库层迁移到应用层,移除数据库 gen_uuid_v7() 函数和所有 server_default 设置。
变更原因
架构一致性
与"禁止物理外键"的架构理念保持一致,所有业务逻辑集中在应用层。
技术优势
- 应用层控制 - UUID 生成逻辑在 Python 代码中,便于测试和 Mock
- 数据库独立 - 减少对 PostgreSQL 特定功能的依赖,提高可移植性
- 简化迁移 - 数据库迁移脚本更简洁,无需管理自定义函数
- 统一管理 - 所有 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. 移除物理外键
同时移除了所有模型中的 ForeignKey 和 foreign_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.pyserver/app/models/ai_quota.pyserver/app/models/ai_usage_log.pyserver/app/models/folder.pyserver/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)
迁移指南
新项目
直接使用更新后的迁移脚本和模型定义。
现有项目
-
清理数据库:
./start_docker.sh -c -b # 清理并重建 -
验证迁移:
docker exec jointo-server-app alembic current # 输出: 3a3a2a1417de (head) -
验证无外键:
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)
注意事项
- 应用层必须验证引用完整性 - 在 Service 层检查关联记录是否存在
- 所有关联字段必须有索引 - 保证查询性能
- UUID 生成器已封装 - 使用
app/utils/id_generator.py的generate_uuid() - 测试时可 Mock -
generate_uuid函数便于单元测试
相关文档
总结
这次变更将 UUID 生成和外键约束从数据库层完全移除,实现了"应用层控制、数据库层简化"的架构目标,为后续的分库分表和微服务化奠定了基础。