Skip to content

整体架构

数据建模能力横跨四个模块:forge-metadata 定义「模型长什么样」,forge-data 负责「怎么读写」,forge-moduleforge-boot 负责「何时把这些定义装配起来、落到数据库」。理解这四层的边界与依赖方向,是看懂后续所有篇章的前提。

本篇只解释「为什么这样分层」。如果你想直接写出第一个宠物商店模型,请看 模型与字段定义指南

四层职责一览

模块一句话职责关键产物
forge-metadata把注解解析为运行时中立的元数据ModelDefinitionFieldDefinitionModelRegistry
forge-data基于元数据提供 CRUD 与关系读写门面ModelsRepositoryOriginQuery<T>
forge-module模块编排与启动钩子,触发模型解析ModelMetaBootstrapPostProcessor
forge-boot生命周期编排、元数据落库、DDL 链路LifecycleOrchestratorSystemMetaManager

这四层的核心约束只有一句:上层依赖下层,下层永不反向感知上层。元数据是最底座,运行时读写、装配、落库都建立在它之上。

模块依赖图

依赖箭头全部朝向 forge-metadata,没有任何一条反向。forge-data 单向依赖 forge-metadata,因为读写时需要查 ModelDefinition 才能知道某个字段的列名、是否落库、关系如何关联;反过来,元数据解析时完全不需要知道有没有人会去读写它。

元数据为什么独立成层

把「模型的形态描述」从「读写实现」里抽出来,是这套架构最根本的一刀。

注解 @Model@Field@Field.Relation 都属于 forge-metadata,它们经 ModelParser 解析后产出 ModelDefinition,注册进 ModelRegistryModelDefinition 里只有运行时中立的描述:tableNamefieldslogicDeleteindexes,没有一行 MyBatis-Plus、没有一行 JDBC。

一份元数据,三个消费方各取所需:forge-data 读它来翻译字段名到列名、解析关系;forge-bootSystemMetaManager 读它来落 sys_model / sys_field 三张系统表;DDL 链路读它来生成建表语句。如果元数据和读写耦合在一起,落库与建表就不得不绕开读写门面去硬抠注解,三个消费方各写一套解析逻辑,约束(如「枚举必须实现 ValueEnum<T>」「关系字段 store=false 不落库」)也会散落各处难以统一校验。

设计取舍:元数据与运行时分层

选项选择理由
注解直接驱动读写(无中间模型)落库、建表、读写三方各自解析注解,约束校验无法集中,关系语义重复实现
引入 ModelDefinition 中间层一次解析、多方复用;元数据运行时中立,落库与建表都不依赖具体 ORM
元数据层反向调用读写做校验破坏单向依赖;启动期 fail-fast 校验应在解析阶段完成,无需读库

TIP

运行时中立意味着 ModelDefinition 不绑定具体方言。列类型映射发生在更上层——SystemMetaManagersys_field 时调用 dialect.mapColumnType(field),关系字段因无数据库列而映射为 null

forge-data 为什么直接持有 Models / Repository

forge-data 提供了静态门面 Models 与底层仓储 Repository<T>,对外暴露 Models.origin(Class)Models.of(entity)Models.saveWith(entity, fieldNames) 等入口。它没有再为「跨层调用」单独抽一个聚合模块,因为这里根本不存在跨层鸿沟。

读写所需的一切,都在它的下游 forge-metadata 里。要把 PetcategoryId 字段翻译成 category_id 列,查 ModelDefinition.getColumnName(fieldName) 即可;要做 M2O 关系填充,查 FieldDefinition 上的 relationFields / referenceFields 即可。forge-dataforge-metadata 是干净的单向向下依赖,没有任何「上层持能力、下层够不到」的情形,自然不需要聚合模块来架桥。

设计取舍:data 是否需要聚合模块

选项选择理由
为读写单独抽聚合模块架桥读写依赖的元数据全在下游 forge-metadata,是顺向依赖,无鸿沟可填
Models 直接持 Repository 并下钻元数据单向依赖天然成立,少一层包装即少一份维护成本,符合「至简至上」
关系读写另起独立模块RelationReadApi / RelationWriteApi 是纯应用层实现,仅靠 IN 查询拼装,归入 forge-data 内聚最强

关系读写值得单独说明:DefaultRelationReadApiDefaultRelationWriteApi 都是纯应用层实现——填充靠 queryByColumnInWHERE column IN (...),M2M 写维护靠对中间表做 diff 增删。它们不引入新的存储层概念,只是 Models 读写能力的组合,因此天然属于 forge-data,无需独立成模块。关系读写的完整链路见 关系字段设计

装配链路:从启动到落库

四层在运行时的协作,由 forge-module 的启动钩子触发、forge-boot 的生命周期编排串起。

时序里有几处值得对照真实日志确认装配是否生效:

  • 拓扑排序后打印模块顺序:[lifecycle] Module topology order: {...}
  • 生命周期收尾统计:Lifecycle={}, 模块 {} 个, 扫描模型 {} 个, 耗时 {}ms
  • 元数据落库收尾:[sys_meta] 元数据落库完成,耗时 {} ms(模块 {} 个、模型 {} 个)

启动钩子涉及包扫描与反射解析,其前置机制见 包扫描与启动钩子。生命周期各阶段(含 DDL、删表归档语义)的细节见 生命周期与 DDL 设计

设计取舍:装配为何下沉到 boot

选项选择理由
解析与落库都放在 forge-module落库、DDL、生命周期编排是重量级职责,与「模块编排」关注点不同
解析在 module、落库与 DDL 在 bootmodule 只负责触发解析钩子,boot 统一掌管运行期生命周期,职责单一
落库逻辑直接写在 forge-dataSystemMetaManager 依赖 dialect 做列类型映射、依赖生命周期阶段判定,属运行期编排而非通用读写

一张图收束依赖与数据流

记住三条边界,后续篇章就不会迷路:

  1. 依赖单向朝下,终点是 forge-metadata
  2. 元数据运行时中立,方言映射发生在更上层。
  3. forge-data 自给自足,读写所需元数据全在下游,无需聚合模块。

延伸阅读