搜索 K
Appearance
Appearance
数据建模能力横跨四个模块:forge-metadata 定义「模型长什么样」,forge-data 负责「怎么读写」,forge-module 与 forge-boot 负责「何时把这些定义装配起来、落到数据库」。理解这四层的边界与依赖方向,是看懂后续所有篇章的前提。
本篇只解释「为什么这样分层」。如果你想直接写出第一个宠物商店模型,请看 模型与字段定义指南。
| 模块 | 一句话职责 | 关键产物 |
|---|---|---|
forge-metadata | 把注解解析为运行时中立的元数据 | ModelDefinition、FieldDefinition、ModelRegistry |
forge-data | 基于元数据提供 CRUD 与关系读写门面 | Models、Repository、OriginQuery<T> |
forge-module | 模块编排与启动钩子,触发模型解析 | ModelMetaBootstrapPostProcessor |
forge-boot | 生命周期编排、元数据落库、DDL 链路 | LifecycleOrchestrator、SystemMetaManager |
这四层的核心约束只有一句:上层依赖下层,下层永不反向感知上层。元数据是最底座,运行时读写、装配、落库都建立在它之上。
依赖箭头全部朝向 forge-metadata,没有任何一条反向。forge-data 单向依赖 forge-metadata,因为读写时需要查 ModelDefinition 才能知道某个字段的列名、是否落库、关系如何关联;反过来,元数据解析时完全不需要知道有没有人会去读写它。
把「模型的形态描述」从「读写实现」里抽出来,是这套架构最根本的一刀。
注解 @Model、@Field、@Field.Relation 都属于 forge-metadata,它们经 ModelParser 解析后产出 ModelDefinition,注册进 ModelRegistry。ModelDefinition 里只有运行时中立的描述:tableName、fields、logicDelete、indexes,没有一行 MyBatis-Plus、没有一行 JDBC。
一份元数据,三个消费方各取所需:forge-data 读它来翻译字段名到列名、解析关系;forge-boot 的 SystemMetaManager 读它来落 sys_model / sys_field 三张系统表;DDL 链路读它来生成建表语句。如果元数据和读写耦合在一起,落库与建表就不得不绕开读写门面去硬抠注解,三个消费方各写一套解析逻辑,约束(如「枚举必须实现 ValueEnum<T>」「关系字段 store=false 不落库」)也会散落各处难以统一校验。
| 选项 | 选择 | 理由 |
|---|---|---|
| 注解直接驱动读写(无中间模型) | 否 | 落库、建表、读写三方各自解析注解,约束校验无法集中,关系语义重复实现 |
引入 ModelDefinition 中间层 | 是 | 一次解析、多方复用;元数据运行时中立,落库与建表都不依赖具体 ORM |
| 元数据层反向调用读写做校验 | 否 | 破坏单向依赖;启动期 fail-fast 校验应在解析阶段完成,无需读库 |
TIP
运行时中立意味着 ModelDefinition 不绑定具体方言。列类型映射发生在更上层——SystemMetaManager 落 sys_field 时调用 dialect.mapColumnType(field),关系字段因无数据库列而映射为 null。
forge-data 提供了静态门面 Models 与底层仓储 Repository<T>,对外暴露 Models.origin(Class)、Models.of(entity)、Models.saveWith(entity, fieldNames) 等入口。它没有再为「跨层调用」单独抽一个聚合模块,因为这里根本不存在跨层鸿沟。
读写所需的一切,都在它的下游 forge-metadata 里。要把 Pet 的 categoryId 字段翻译成 category_id 列,查 ModelDefinition.getColumnName(fieldName) 即可;要做 M2O 关系填充,查 FieldDefinition 上的 relationFields / referenceFields 即可。forge-data → forge-metadata 是干净的单向向下依赖,没有任何「上层持能力、下层够不到」的情形,自然不需要聚合模块来架桥。
| 选项 | 选择 | 理由 |
|---|---|---|
| 为读写单独抽聚合模块架桥 | 否 | 读写依赖的元数据全在下游 forge-metadata,是顺向依赖,无鸿沟可填 |
Models 直接持 Repository 并下钻元数据 | 是 | 单向依赖天然成立,少一层包装即少一份维护成本,符合「至简至上」 |
| 关系读写另起独立模块 | 否 | RelationReadApi / RelationWriteApi 是纯应用层实现,仅靠 IN 查询拼装,归入 forge-data 内聚最强 |
关系读写值得单独说明:DefaultRelationReadApi 与 DefaultRelationWriteApi 都是纯应用层实现——填充靠 queryByColumnIn 拼 WHERE column IN (...),M2M 写维护靠对中间表做 diff 增删。它们不引入新的存储层概念,只是 Models 读写能力的组合,因此天然属于 forge-data,无需独立成模块。关系读写的完整链路见 关系字段设计。
四层在运行时的协作,由 forge-module 的启动钩子触发、forge-boot 的生命周期编排串起。
时序里有几处值得对照真实日志确认装配是否生效:
[lifecycle] Module topology order: {...}Lifecycle={}, 模块 {} 个, 扫描模型 {} 个, 耗时 {}ms[sys_meta] 元数据落库完成,耗时 {} ms(模块 {} 个、模型 {} 个)启动钩子涉及包扫描与反射解析,其前置机制见 包扫描与启动钩子。生命周期各阶段(含 DDL、删表归档语义)的细节见 生命周期与 DDL 设计。
| 选项 | 选择 | 理由 |
|---|---|---|
解析与落库都放在 forge-module | 否 | 落库、DDL、生命周期编排是重量级职责,与「模块编排」关注点不同 |
解析在 module、落库与 DDL 在 boot | 是 | module 只负责触发解析钩子,boot 统一掌管运行期生命周期,职责单一 |
落库逻辑直接写在 forge-data | 否 | SystemMetaManager 依赖 dialect 做列类型映射、依赖生命周期阶段判定,属运行期编排而非通用读写 |
记住三条边界,后续篇章就不会迷路:
forge-metadata。forge-data 自给自足,读写所需元数据全在下游,无需聚合模块。