项目实训个人工作博客(一):数据库关系模式及Models层设计
在本次空间转录组学数据分析平台项目开发过程的初期阶段,我负责了后端数据库关系模式设计,以及backend/app/models目录下整个 Models 层的数据模型设计与实现工作。
models层解决的就是系统内部如何组织核心业务数据、如何描述实体之间的关系、如何保证数据在持久化与业务流转中的一致性。为了保证项目后续的文件管理、项目编排、服务调用、知识管理、分析流程追踪、智能体对话与代码生成等功能可以稳定扩展,我在设计这一层时,重点围绕以下几个目标展开:
1. 保证核心实体划分清晰,避免不同模块的数据边界混乱。
2. 保证模型字段约束明确,在进入数据库前就完成尽可能充分的校验。
3. 保证实体间关系可追踪,尤其是用户-项目-文件-服务执行这条主链路必须闭环。
4. 兼顾 MongoDB 文档存储的灵活性与业务关系模式的可维护性。
5. 为后续 API、Service、Repository 层提供统一、稳定、可复用的数据结构基础。


一、数据库设计的整体思路
本项目后端采用 MongoDB 作为主要数据存储,因此底层并不是传统意义上的严格关系型数据库表设计,而是以文档数据库为底座、以逻辑关系模式为核心的设计思路。
从业务上看,系统中的核心主线非常明确:
用户→项目文件→服务执行/分析流程→输出文件。
除此之外,还存在Analysis模块,用于组织文件分析树、算法节点和流程状态。
| 实体名称 | 主要标识字段 | 核心作用 | 关联对象 |
| User | user_id | 用户身份与账户信息 | Project、File、ServiceExecution |
| Project | project_id | 项目组织与资源聚合 | User、File、ServiceExecution |
| File | file_id | 文件资源统一抽象 | User、Project、FileType、ServiceExecution |
| FileType | file_type_id | 文件类型规范 | File、Service |
| ServiceInfo | service_id | 算法服务注册信息 | FileType、ServiceExecution、Project |
| ServiceExecution | execution_id | 服务调用执行记录 | ServiceInfo、File、Project、User |
二、核心关系模式设计
1. 用户与项目
整个系统中,User是最顶层的业务主体,而Project是承载业务资源的一级容器。一个用户可以创建多个项目,形成了非常明确的一对多关系:一个User对应多个Project,每个Project只属于一个User。
同时,项目本身不是孤立对象,而是一个资源聚合容器,因此我在Project模型中进一步维护了:file_id、knowledge_id、service_id、execution_id,也就是说,项目不仅有名称和描述,还能够聚合文件、关联服务并串起执行历史。
2. 项目与文件
文件是空间转录组平台里最重要的基础资源之一,很多后续分析、可视化、算法服务调用都依赖文件作为输入。我在关系模式上将File设计为独立实体,而不是直接嵌入项目文档中,原因主要有三点:
(1)文件本身生命周期独立,上传、查看、分析、删除都需要单独管理。
(2)文件元数据比较丰富,不适合简单挂在项目中。
(3)文件可能被多个流程复用,例如既参与分析树,又参与服务执行,还可能是其他文件的父文件或子文件。
因此这里采用了双向弱关联的设计:File.project_id用于记录文件当前归属项目,Project.file_id用于从项目侧快速聚合文件列表,这实际上是一种逻辑外键+聚合引用的折中方案,既符合MongoDB的灵活存储特性,又满足业务查询效率需求。
同时,为了支持文件派生链路,我又设计了FileRelation模型,用parent_file_id和child_file_id描述文件之间的父子关系。这一点非常适合空间转录组学分析场景,因为一个原始文件经过质控、筛选、归一化、可视化转换之后,往往会生成多个派生文件。
| 关系 | 表达字段 | 含义 |
| 用户拥有文件 | File.user_id | 文件归属哪个用户 |
| 项目包含文件 | File.project_id | 文件与项目的挂接关系 |
| 文件类型归类 | File.file_type_id | 文件属于哪种类型 |
| 文件派生关系 | FileRelation.parent_file_id、child_file_id | 记录分析链路中的上下游文件关系 |
3. 文件与文件类型
为了避免文件类型字段在不同模块中被随意写成字符串,导致后续判断混乱,我专门设计了FileType模型,将文件类型抽象为独立实体。FileType中保存了file_type_id、name、isplay_name、cription、category、extensions。
这样做的意义原因如下:
(1)文件类型定义与文件实例解耦,便于统一维护。
(2)服务模块可以通过accepted_files直接声明支持哪些file_type_id。
(3)后续前端展示时,可以基于display_name和category输出更友好的类型信息。
(4)文件上传校验可以统一依据extensions完成。
这使得系统不再是上传任意文件再临时判断,而是先有类型规范,再有文件实例,整体结构更清晰。
4. 服务与执行记录
在本项目中,算法服务并不是写死在代码里的,而是通过ServiceInfo进行统一注册和描述。这是我在Models层设计中比较重要的一部分,因为它直接决定了平台能否支持后续服务扩展。
ServiceInfo负责描述一个服务“是什么”,如:
- 服务地址baseurl
- 访问路径service_suffix
- 下载路径 download_suffix
- 参数模板parameter_template
- 参数定义parameter_schema
- 可接收文件类型 accepted_files
- 输出结构output_config
- 可见性visibility
- 状态status。
而ServiceExecution负责描述一次调用“做了什么、结果如何”,如:
- 输入文件input_file_id
- 输出文件 output_file_id
- 调用用户user_id
- 所属项目project_id
- 执行状态status
- 执行参数parameters
- 响应数据response_data
- 错误信息error_message
- 开始、完成时间与耗时
于是就形成了非常清晰的一对多关系:一个ServiceInfo对应多次ServiceExecution,一次ServiceExecution可以关联多个输入文件和多个输出文件,一次ServiceExecution也可以挂接到某个Project。这一设计非常适合分析平台的运行方式,因为算法服务是模板化定义+多次实例化执行的结构。
class ServiceInfo(BaseModel):
model_config = ConfigDict(extra="allow")
service_id: str = Field(default_factory=lambda: str(uuid4()))
name: str = Field(..., min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=1000)
version: str = Field(default="1.0.0", max_length=50)
baseurl: str = Field(..., min_length=1)
service_suffix: str = Field(..., min_length=1)
download_suffix: Optional[str] = None
request_config: TaskRequestConfig = Field(default_factory=TaskRequestConfig)
parameter_template: TaskParameterTemplate = Field(default_factory=TaskParameterTemplate)
parameter_schema: Optional[Dict[str, ParameterDefinition]] = None
accepted_files: Optional[Dict[str, AcceptedFileConfig]] = None
output_config: Optional[ServiceOutputConfig] = None
visibility: ServiceVisibility = Field(default=ServiceVisibility.PRIVATE)
status: ServiceStatus = Field(default=ServiceStatus.ACTIVE)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
created_by: str = Field(..., min_length=1)
class ServiceExecution(BaseModel):
model_config = ConfigDict(extra="allow")
execution_id: str = Field(default_factory=lambda: str(uuid4()))
service_id: str = Field(..., min_length=1)
service_name: Optional[str] = None
user_id: str = Field(..., min_length=1)
input_file_ids: List[str] = Field(..., min_length=1)
output_file_ids: List[str] = Field(default_factory=list)
project_id: Optional[str] = None
status: ServiceExecutionStatus = Field(default=ServiceExecutionStatus.PENDING)
parameters: Dict[str, Any] = Field(default_factory=dict)
response_data: Optional[Dict[str, Any]] = None
error_message: Optional[str] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
duration_seconds: Optional[float] = None
5. 项目、服务与文件执行链路闭环
从整个系统主链路来看,最关键的并不是某一个单独模型,而是下面这条闭环:
User→Project→File→ServiceExecution→Output File→Project
在这条链路中:
(1)用户在项目中上传文件。
(2)项目中的文件作为输入交给服务模块执行。
(3)服务执行产生输出文件。
(4)输出文件重新回流到项目资源池中。
(5)后续分析树、可视化和再次执行都可以继续复用这些结果。
这保证了平台不是零散功能的堆叠,而是有完整数据流闭环的系统。
三、分析流程相关模型设计
1.面向分析流程的树形结构
空间转录组学分析往往不是一条简单线性链路,而是输入文件→若干中间结果→不同算法节点→ 输出文件的树形过程。
因此我设计了AnalysisTreeModel、FileNodeModel和AlgorithmEdgeModel三层结构:FileNodeModel表示文件节点,AlgorithmEdgeModel表示算法边,AnalysisTreeModel表示整棵分析树。
这种设计比用平铺表记录流程更适合前端展示分析树,也更便于追踪每个节点的状态、摘要、错误信息和元数据。
四、Models 层设计中的几个关键原则
1. 统一采用Pydantic V2进行模型定义
整个models层以Pydantic为基础,这具有以下优点:
(1)可以通过Field明确字段类型、长度、默认值和说明。
(2)可以通过field_validator、model_validator在模型层提前进行校验。
也就是说,很多本该在业务层手动if-else判断的数据问题,已经前移到了模型层解决,减少了后续服务层的混乱。
2. 枚举类统一管理状态字段
像文件状态、服务状态、执行状态、可见性、参数类型、代码语言等字段,我都尽量使用Enum而不是裸字符串。这样一来,状态值更规范、代码可读性更强、前后端联调时更不容易出现拼写错误。
例如服务执行状态统一由ServiceExecutionStatus管理,文件分析状态统一由AnalysisStatus管理。
class ServiceExecutionStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class AnalysisStatus(str, Enum):
PENDING = "pending"
UPLOADED = "uploaded"
PROCESSING = "processing"
COMPLETED = "completed"
ERROR = "error"
3. 提供to_dict与from_dict,打通模型与MongoDB文档之间的转换
由于MongoDB返回的是字典文档,而业务层更希望使用强类型模型,所以我在多个模型中设计了to_dict()、xxx_from_dict()这两个方向的转换接口。这样做的目的是:
(1)Repository 层和Service层的职责边界更清晰。
(2)时间字段、枚举字段、嵌套字段可以统一转换。
(3)对MongoDB 的_id字段做了适当隔离,不让它污染业务模型。
4. 兼顾严格约束与文档型数据库扩展性
在不少模型里,我使用了ConfigDict(extra="allow"),而在另一些更严格的模型里使用 extra="forbid"。这并不是随意设置,而是根据业务场景做区分。
核心输入参数、类型定义、服务参数这类结构稳定的对象,尽量forbid。面向 MongoDB 扩展、可能带额外字段的业务对象,适度allow。
这样既保证了核心边界不会失控,也保留了文档数据库在业务快速演进中的灵活性。
五、数据库表一览
1、用户相关表
User表
| 字段 | 类型 | 描述 |
|
user_id |
String |
用户唯一标识 |
|
username |
String |
用户名 |
|
password |
String |
密码(加密存储) |
|
|
String |
邮箱地址 |
|
created_at |
DateTime |
创建时间 |
|
updated_at |
DateTime |
更新时间 |
2、文件相关表
FileType表
| 字段 | 类型 | 描述 |
|
file_type_id |
String |
文件类型ID |
|
name |
String |
唯一名称 |
|
display_name |
String |
对用户展示的名称 |
|
description |
String |
类型描述 |
|
category |
String |
类型分类 |
|
extensions |
List<String> |
允许的扩展名 |
|
created_at |
DateTime | 创建时间 |
|
updated_at |
DateTime |
更新时间 |
FileInfo表
| 字段 | 类型 | 描述 |
|
user_id |
String |
用户ID |
|
file_id |
String |
文件ID |
|
filename |
String |
文件名 |
|
file_type_id |
String |
文件类型ID |
|
file_type_name |
String |
文件类型名称 |
|
file_type_display_name |
String |
文件类型展示名称 |
|
file_path |
String |
文件路径 |
|
upload_time |
DateTime |
上传时间 |
|
last_viewed_time |
DateTime |
最后查看时间 |
|
analysis_status |
Enum |
分析状态 |
|
description |
String |
文件描述 |
|
metadata |
Dict |
文件元数据 |
|
generated_by |
Dict |
生成信息 |
FileRelation表
| 字段 | 类型 | 名称 |
|
parent_file_id |
String |
父文件ID |
|
child_file_id |
String |
子文件ID |
|
project_id |
String |
项目ID |
|
description |
String |
关系描述 |
|
created_at |
DateTime |
创建时间 |
3、项目相关表
Project表
| 字段 | 类型 | 名称 |
|
project_id |
String |
项目ID |
|
user_id |
String |
所属用户ID |
|
name |
String |
项目名 |
|
description |
String |
项目描述 |
|
file_ids |
List<String> |
项目包含的文件ID列表 |
|
service_ids |
List<String> |
项目关联的服务ID列表 |
|
execution_ids |
List<String> |
项目关联的执行ID列表 |
|
created_at |
DateTime |
创建时间 |
|
updated_at |
DateTime |
更新时间 |
ProjectServiceConfig表
| 字段 | 类型 | 名称 |
|
mode |
Enum |
配置模式 |
|
whitelist |
List<String> |
服务ID白名单 |
|
blacklist |
List<String> |
服务ID黑名单 |
4、服务相关表
ServiceInfo表
| 字段 | 类型 | 名称 |
|
service_id |
String |
Service ID |
|
name |
String |
Service名称 |
|
description |
String |
Service描述 |
|
version |
String |
Service版本 |
|
baseurl |
String |
基础URL |
|
service_suffix |
String |
Service后缀 |
|
download_suffix |
String |
Download后缀 |
|
request_config |
Object |
请求配置 |
|
parameter_template |
Object |
参数模板 |
|
parameter_schema |
Object |
参数定义schema |
|
accepted_files |
Object |
接受的文件类型配置 |
|
output_config |
Object |
输出结果配置 |
|
visibility |
Enum |
Service可见性 |
|
created_at |
DateTime |
创建时间 |
|
updated_at |
DateTime |
更新时间 |
|
created_by |
String |
创建者用户ID |
ServiceExecution表
| 字段 | 类型 | 名称 |
|
execution_id |
String |
执行ID |
|
service_id |
String |
Service ID |
|
service_name |
String |
Service 名称 |
|
user_id |
String |
用户ID |
|
input_file_ids |
List<String> |
输入文件ID列表 |
|
output_file_ids |
List<String> |
输出文件ID列表 |
|
project_id |
String |
项目ID |
|
status |
Enum |
执行状态 |
|
parameters |
Dict |
执行参数 |
|
response_data |
Dict |
远程服务器响应数据 |
|
error_message |
String |
错误信息 |
|
created_at |
DateTime |
创建时间 |
|
started_at |
DateTime |
开始执行时间 |
|
completed_at |
DateTime |
完成时间 |
|
duration_seconds |
Float |
执行耗时(秒) |
六、具体收获与思考
这次负责数据库关系模式和 Models 层设计,让我对数据结构先行这件事有了更深的体会。很多时候,后端开发最难的并不是写接口,而是先想清楚系统里到底有哪些实体、谁依赖谁、哪些字段应该固化、哪些关系应该显式保留。
如果关系模式没有设计清楚,后续会出现如同一业务概念在不同模块中字段命名不一致,数据归属关系混乱,后续权限和删除逻辑很难维护,文件、项目、服务执行之间无法形成完整追踪链,新功能接入时只能不断打补丁,而不是稳定扩展等各类棘手的问题。
总的来说,我负责的数据库关系模式设计和 Models 层设计工作,本质上是在为整个后端系统搭建稳定的数据骨架。这部分工作虽然不像界面展示那样直观,但它决定了系统后续能否持续扩展、不同功能之间能否顺畅协同、数据链路能否完整闭环。
通过这次实践,我不仅进一步熟悉了Pydantic模型设计、MongoDB文档建模和后端分层协作方式,也更深刻地理解了模型设计不是简单列字段,而是对整个业务世界进行抽象。后续在项目继续推进过程中,我也会继续结合实际功能迭代,对这套关系模式和模型体系进行完善与优化。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)