# RFC 130: 枚举类型重构 - PostgreSQL ENUM 改为 SMALLINT **状态**: 已实施 **创建时间**: 2026-01-21 **作者**: System **类型**: 重构 --- ## 概述 将项目模块中所有 PostgreSQL ENUM 类型改为 SMALLINT 类型,并在后端 Python 代码中使用 IntEnum 进行映射。 ## 背景 ### 问题 PostgreSQL ENUM 类型存在以下问题: 1. **迁移复杂**: 修改 ENUM 值需要复杂的 ALTER TYPE 操作 2. **扩展困难**: 添加新值需要数据库迁移,无法动态扩展 3. **跨数据库兼容性差**: 不同数据库对 ENUM 的支持不一致 4. **可读性问题**: 数据库中存储字符串,占用空间较大 ### 目标 - 简化数据库结构,使用 SMALLINT 存储枚举值 - 在后端代码中使用 IntEnum 提供类型安全 - 保持 API 接口向后兼容(对外仍使用字符串) - 提升性能和扩展性 ## 设计方案 ### 数据库层 **修改前(PostgreSQL ENUM)**: ```sql CREATE TYPE project_type AS ENUM ('mine', 'collab'); CREATE TYPE project_content_type AS ENUM ('ad', 'movie', 'series', 'anime', 'short', 'concept'); CREATE TYPE aspect_ratio_type AS ENUM ('16:9', '9:16', '4:3', '21:9', '1:1', '2.35:1', '2.39:1'); CREATE TYPE share_permission AS ENUM ('viewer', 'editor'); CREATE TYPE member_role AS ENUM ('owner', 'editor', 'viewer'); CREATE TABLE projects ( type project_type NOT NULL DEFAULT 'mine', content_type project_content_type, aspect_ratio aspect_ratio_type, ... ); ``` **修改后(SMALLINT)**: ```sql CREATE TABLE projects ( type SMALLINT NOT NULL DEFAULT 1, -- 1=mine, 2=collab content_type SMALLINT, -- 1=ad, 2=movie, 3=series, 4=anime, 5=short, 6=concept aspect_ratio SMALLINT, -- 1=16:9, 2=9:16, 3=4:3, 4=21:9, 5=1:1, 6=2.35:1, 7=2.39:1 ... ); COMMENT ON COLUMN projects.type IS '项目类型:1=mine(个人项目), 2=collab(协作项目)'; COMMENT ON COLUMN projects.content_type IS '项目内容类型:1=ad(广告片), 2=movie(电影), 3=series(剧集), 4=anime(动画), 5=short(短视频), 6=concept(概念片)'; ``` ### Python 模型层 **修改前(str Enum)**: ```python class ProjectType(str, Enum): MINE = "mine" COLLAB = "collab" class Project(SQLModel, table=True): type: ProjectType = Field( sa_column=Column(SQLEnum(ProjectType), nullable=False) ) ``` **修改后(IntEnum + 转换方法)**: ```python class ProjectType(IntEnum): """项目类型枚举""" MINE = 1 COLLAB = 2 @classmethod def from_string(cls, value: str) -> int: """字符串转数字""" mapping = {'mine': cls.MINE, 'collab': cls.COLLAB} return mapping.get(value.lower(), cls.MINE) @classmethod def to_string(cls, value: int) -> str: """数字转字符串""" mapping = {cls.MINE: 'mine', cls.COLLAB: 'collab'} return mapping.get(value, 'mine') class Project(SQLModel, table=True): type: int = Field( sa_column=Column(SmallInteger, nullable=False, default=ProjectType.MINE) ) @property def type_str(self) -> str: """获取项目类型字符串""" return ProjectType.to_string(self.type) ``` ### 枚举值映射表 | 枚举类型 | 数值 | 字符串值 | 说明 | |---------|------|---------|------| | **project_type** | 1 | mine | 个人项目 | | | 2 | collab | 协作项目 | | **content_type** | 1 | ad | 广告片 | | | 2 | movie | 电影 | | | 3 | series | 剧集 | | | 4 | anime | 动画 | | | 5 | short | 短视频 | | | 6 | concept | 概念片 | | **aspect_ratio** | 1 | 16:9 | 标准宽屏 | | | 2 | 9:16 | 竖屏 | | | 3 | 4:3 | 传统电视 | | | 4 | 21:9 | 超宽屏 | | | 5 | 1:1 | 正方形 | | | 6 | 2.35:1 | 电影宽屏 | | | 7 | 2.39:1 | 电影宽屏 | | **share_permission** | 1 | viewer | 查看权限 | | | 2 | editor | 编辑权限 | | **member_role** | 1 | owner | 所有者 | | | 2 | editor | 编辑者 | | | 3 | viewer | 查看者 | ### 仓储层(Repository) ```python async def get_by_user( self, user_id: str, project_type: Optional[str] = None, # API 传入字符串 ... ) -> List[Project]: """获取用户的项目列表""" from app.models.project import ProjectType conditions = [...] # 字符串转数字进行查询 if project_type: type_int = ProjectType.from_string(project_type) conditions.append(Project.type == type_int) ... ``` ### 服务层(Service) ```python async def create_project( self, user_id: str, project_data: ProjectCreate # Schema 使用字符串 ) -> Project: """创建项目""" from app.models.project import ProjectType, ProjectContentType # 转换枚举值(字符串 → 数字) type_int = ProjectType.from_string(project_data.type.value) content_type_int = ProjectContentType.from_string( project_data.content_type.value ) if project_data.content_type else None project = Project( type=type_int, content_type=content_type_int, ... ) ... ``` ### API 层(Schema) **保持对外接口使用字符串**: ```python class ProjectCreate(BaseModel): type: str = Field(..., pattern="^(mine|collab)$") content_type: Optional[str] = None # 'ad', 'movie', etc. @field_validator('content_type') @classmethod def validate_content_type(cls, v: Optional[str]) -> Optional[str]: if v is not None: valid_types = ['ad', 'movie', 'series', 'anime', 'short', 'concept'] if v.lower() not in valid_types: raise ValueError(f'content_type must be one of {valid_types}') return v class ProjectResponse(BaseModel): type: str # 返回字符串 'mine' 或 'collab' content_type: Optional[str] # 返回字符串 'ad', 'movie' 等 @classmethod def from_orm(cls, obj): """从 ORM 对象转换,自动处理枚举转换""" return cls( type=obj.type_str, # 使用模型的 property 方法 content_type=obj.content_type_str, ... ) ``` ## 实施步骤 ### 1. 修改模型层 - ✅ 将 `str Enum` 改为 `IntEnum` - ✅ 添加 `from_string()` 和 `to_string()` 转换方法 - ✅ 修改模型字段类型为 `SmallInteger` - ✅ 添加 `_str` property 方法 ### 2. 修改数据库迁移 - ✅ 移除 `CREATE TYPE ... AS ENUM` 语句 - ✅ 修改字段类型为 `SMALLINT` - ✅ 更新注释说明数值映射 ### 3. 修改仓储层 - ✅ 添加枚举转换逻辑(字符串 → 数字) - ✅ 查询时处理枚举过滤 ### 4. 修改服务层 - ✅ 处理创建/更新时的枚举转换 - ✅ 确保返回数据使用字符串 ### 5. 修改 Schema 层 - ✅ 保持 API 使用字符串 - ✅ 添加 `field_validator` 验证 - ✅ 修改 `ProjectResponse.from_orm()` 处理枚举转换 ### 6. 数据库重建 - ⏳ 删除现有数据库 - ⏳ 运行新迁移脚本 - ⏳ 验证数据结构 ## 优势 ### 1. 简化数据库 - 无需 ENUM 类型定义 - 字段类型统一为 SMALLINT - 迁移脚本更简洁 ### 2. 扩展灵活 - 添加新值无需 ALTER TYPE - 可以动态扩展枚举值 - 便于版本管理 ### 3. 性能提升 - SMALLINT 比 TEXT 更高效 - 索引性能更好 - 存储空间更小 ### 4. 跨数据库兼容 - SMALLINT 是标准 SQL 类型 - 便于迁移到其他数据库 - 减少数据库特定依赖 ### 5. 向后兼容 - API 接口保持不变 - 对外仍使用字符串 - 客户端无需修改 ## 风险与缓解 ### 风险 1: 数据库可读性降低 **缓解**: - 添加详细的 COMMENT 注释 - 文档中维护完整的映射表 - 使用有意义的数值(1, 2, 3...) ### 风险 2: 代码复杂度增加 **缓解**: - 封装转换逻辑到枚举类 - 使用 property 方法简化访问 - 统一转换模式 ### 风险 3: 数据迁移 **缓解**: - 当前为开发阶段,可以删除重建 - 生产环境需要编写数据迁移脚本 - 提供回滚方案 ## 测试计划 ### 单元测试 - [ ] 枚举转换方法测试 - [ ] 模型 property 方法测试 - [ ] Schema 验证测试 ### 集成测试 - [ ] 项目 CRUD 操作测试 - [ ] 枚举筛选查询测试 - [ ] API 响应格式测试 ### 性能测试 - [ ] 查询性能对比 - [ ] 索引效率测试 - [ ] 存储空间对比 ## 后续工作 1. **其他模块迁移**: 将相同模式应用到其他模块(用户、文件夹等) 2. **生产环境迁移**: 编写数据迁移脚本,支持平滑升级 3. **监控优化**: 监控性能提升效果,优化查询 4. **文档完善**: 更新 API 文档和开发指南 ## 参考资料 - [PostgreSQL ENUM 类型文档](https://www.postgresql.org/docs/current/datatype-enum.html) - [Python IntEnum 文档](https://docs.python.org/3/library/enum.html#enum.IntEnum) - [SQLAlchemy SmallInteger 类型](https://docs.sqlalchemy.org/en/20/core/type_basics.html#sqlalchemy.types.SmallInteger) --- **实施完成时间**: 2026-01-21 **影响范围**: 项目模块(projects, project_members, project_shares) **向后兼容**: 是(API 接口保持不变)