Trantor
很多同学在使用 Trantor 的过程, 或多或少都会有问题, 我们也有在收集各方所问过的问题, 使用 Trantor 的反馈等等.
Trantor 团队在 Review 这些问题, 特别是一些看起很没有营养的问题的时候, 经常会提出一个结论是 这位同学没理解 Trantor 机制. 很多时候类似问题就被放过去了, 归为已处理.
但新同学 如何理解 Trantor 机制, 其实是 Trantor 并没有做好的地方, 更多是说你这么做, 就能达到什么效果, 方式上没问题, 但是使用的同学想更进一步则会很难.
所以本文就是从概念到过程的, 相对体系化的介绍一下 Trantor 的机制, 尝试说明白, 到底什么是 Trantor 机制, 我们又怎么来理解它.
Trantor 的定位
要理解一个东西是什么, 首先要了解的是, 他的定位是什么, 要解决的问题是什么.
下面就是 Trantor 的定位:
Trantor 是一个中后台业务应用快速研发和交付体系,同时也是公司大战略的可定制化 SaaS 平台的核心机制部分.
我们发现里面有很多关键字, 但其实最核心的关键词, 就三个, 分别是:
- 中后台: 点明了 Trantor 的核心场景, 选择中后台是因为其场景相对模式化, 模式化的相对好抽象
- 快速研发: Trantor 的核心需求, 就是通过场景抽象, 二开机制来整体提升我们的软件研发以及交付的效率
- SaaS 平台: 这个是未来的大战略,指明了 Trantor 未来的方向, 这里暂不展开
所以说回来, Trantor 最核心的需求, 还是软件行业万年不变的老台词, 提升 研发效率.
研发效率
其实提升研发效率这条路, 一直在走, 我们目前使用的现代变成语言也好, 常用的框架也好, 本质上都是为了提升研发效率.
我们不再需要直接处理二进制指令, 因为有了汇编语言, 使用助记符对二进制指令做了抽象.
我们不再需要直接针对机器写程序, 因为有了操作系统, 提供了接口对硬件的操作做了抽象.
我们不再需要写很多重复性的代码, 因为有了业务模板, 通过固化重复代码的方式做了抽象.
我们不再需要写代码手动管理内存, 因为有了高级语言, 通过管理对象的生命周期做了抽象.
大家发现了么? 说自古以来可能有点过分, 但是基本也差不多, 就是计算机诞生以来, 软件的效率提升就没停过, 现在也没有停下脚步, 但是说回来基本上都是两个字: 抽象.
抽象
下面官方一些的描述:
抽象: 从众多的具体事物中,抽取共同的、本质的属性,舍弃个别的、非本质的属性,从而形成概念.
重点: 抽取共同的, 舍弃个别的.
回过头来再看 Trantor 的关键字中, 为什么会有一个 中后台 应用, 正式因为中后台有大量共同的, 可以被抽象的逻辑. 如果是一些界面各异, 逻辑迥然的产品, 本身可抽象的部分就不是很多, 相对效能提升的收益也不大.
Trantor 目前主要有两个层面的抽象, 分别是研发和交付.
研发的抽象
研发的抽象就是指, 为了提升软件研发效率所做的抽象. Trantor 在研发方面的抽象也做了很多, 最核心的就是 元信息 的抽象. 当然, 元信息的抽象我们又分了一些不同的类型.
其中最核心的就是一下三个类型:
- 模型
- 视图
- 行为
模型
模型: 是指对于某个实际问题或客观事物、规律进行抽象后的一种形式化表达方式.
在 Trantor 的体系内, 模型是描述了一个业务实体, 以及该实体有那些属性字段和业务关系, 相当于一个数据容器, 一个持久化的表结构. 大多数情况下, 模型约等于一个 Java 类加一些注解, 我们可以看一个模型的例子:
@TModel( name = “公司”, mainField = Company.name_field, indexes = { @Index(columns = Company.code_field, unique = true), @Index(columns = Company.outerCode_field) })public class Company extends BaseModel<Long> {
@TModelField(name = "公司编码”) @TextMeta(rule = “STRING(CO)+TIMES(yyyy-MM-dd)+INCRE(1,6,0,0)”) private String code; @TModelField(name = “外部编码”) private String outerCode; @TModelField(name = “公司名称”, nullable = false) @NotNull(message = “公司名称不能为空”) private String name; @TModelField(name = “上级公司”) @RelationMeta(name = “CompanyParent”) private Company parent; @TModelField(name = “地址”) private Address address; @TModelField(name = “公司详情”, type = TModelFieldType.RichText) @TextMeta(length = 2000) private String remark; @TModelField(name = “状态”, defaultValue = “Enable”, nullable = false) private CompanyStatus status;}上述代码就是一个 Trantor Model, 描述了一个公司所需要的属性以及业务关系. 我们看看这个模型描述了哪些事情:
- 首先声明了一个 Java 类, 以及相关的字段
- 每个字段上, 有该字段相关的声明, 比如字段的名字, 校验, 默认值, 是否可为空等等
- 一些业务关系, 就是通过
@RelationMeta声明的字段, 比如parent字段, 就指明了该模型是一个自关联的树模型 - 一些特殊的字段类型, 比如
remark声明了类型为RichText, 字面意思就是富文本 - 其他还有一些索引之类的声明
有了上述声明之后, 我们能做什么呢?
- 首先有了一个承载数据的容器, 就是声明的 Java 类
- 可以生成这个模型对应的数据库结构, 因为我们有每个字段的类型声明, 非空校验, 关联关系
- 因为我们有字段的类型, 字段的 Label, 默认值之类的声明, 所以我们也可以基于这些信息, 生成这个模型对应的前端界面, 至少标准的增删改查是没问题的
那么, 我们是怎么做到的呢? 还是我们一直在聊的, 性能提升的核心方式: 抽象. 抽象可以让我们只编写变化的部分, 共同的部分, 全部被 Trantor 处理掉了.
首先我们先看一下数据库结构是怎么映射出来的:
- Trantor 抽象了 Java 常用数据类型对应的 Trantor 数据类型
- Trantor 抽象了 Trantor 数据类型对应的数据库类型, 所以不同字段映射到的数据库类型是什么.
- Trantor 抽象了模型以及字段上, 对于数据库信息声明的方式, 比如上例的 @Index, @TextMeta, 所以字段的长度, 是否可为空, 甚至索引的声明 Trantor 都可以获取.
- 根据 @RelationMeta 的声明, Trantor 知道了模型之间的关系, 无论是一对一, 一对多, 还是多堆多, 需要声明的数据外键 Trantor 就可以根据规则声明.
上例生成出来的 MySQL 表结构如下:
CREATE TABLE `base__company` ( `id` bigint(20) NOT NULL, `code` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `remark` mediumtext COLLATE utf8mb4_unicode_ci, `outerCode` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `address` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `name` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, `status` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL, `CompanyParent` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '上级公司 ', `isDeleted` tinyint(4) NOT NULL DEFAULT '0', `deletedAt` bigint(20) NOT NULL DEFAULT '0', `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, `CreatedBy` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人 ', `UpdatedBy` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '最后修改人 ', PRIMARY KEY (`id`), UNIQUE KEY `uk_code` (`code`), KEY `CreatedBy` (`CreatedBy`), KEY `CompanyParent` (`CompanyParent`), KEY `UpdatedBy` (`UpdatedBy`), KEY `uk_outerCode` (`outerCode`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;Trantor 数据类型
因为计算机语言提供的数据类型, 会考虑到性能等方面, 同时也没有业务含义. 比如整形就分了 byte, short, int, long 四种. Trantor 将其抽象成了 Int 一种, 浮点类型也类似. 同时 Trantor 提供了一些业务类型, 比如 Phone, Email, Address, Image, Attachment 等.
视图
我们看一个视图的例子:
<View title=“公司管理”> <Table key=“table” model=“base_Company”> <Search> <Fields> <Field name=“name"/> <Field name=“code”/> <Field name=“parent”/> <Field name=“status”/> </Fields> </Search> <RecordActions label="操作"> <Action label=“详情” action=“base_Company_toDetail”/> <Action label=“编辑” action=“base_Company_toEdit” /> <Action label=“启用” :show=“this.record.status == ‘Disable’” action=“base_Company_CompanyAction::enable” after=“Refresh” /> <Action label=“禁用” :show=“this.record.status == ‘Enable’” action=“base_Company_CompanyAction::disable” after=“Refresh” /> </RecordActions> <Fields> <Field name=“code”/> <Field name=“name”/> <Field name=“parent”/> <Field name=“createdAt”/> <Field name=“status”/> </Fields> <Actions> <Action type=“Create” action=“base_Company_toEdit”/> </Actions> </Table></View>我们可以看到, 这个视图的内容非常少, 没有写任何 Dom 或者样式, 逻辑代码. 只声明了一个 <Table>, 绑定模型之后, 声明了那些字段用作搜索, 那些字段用来展示, 有那些 Action 操作.
我们的模板只保留了变化部分, 比如我们要展示的是什么模型, 需要展示那些字段. 其他非变化部分, 都被
和大致理解了模型的抽象之后, 再看界面是如何映射出来的, 就比较好理解了:
- Table 绑定了一个模型, 而之前已经将模型的 Java 类型抽象成了 Trantor 的数据类型
- Trantor 抽象的数据类型都有对应前端组件存在, 会自动映射到前端界面控件, 在搜索, 展示, 表单, 列表中的行为会自适应, 包括
地址,附件这种复杂组件也一样. - 字段上声明的字段名, 长度限制, 是否非空以及其校验失败的提示, 都会被抽取出来, 变成界面上的字段标题, Validation 等.
- @RelationMeta 的声明会让前端理解这是一个关联关系, 所以相关的选择器, 展示组件都会特殊处理
一个字段的历程
我们以一个字段为例, 就是上例中的一个公司地址字段, 我们在 Java 类里面, 只会写如下内容, 其实就是一个 Java 字段, 外加一个注解.
@TModelField(name = “地址”)private Address address;Trantor 基于这个 Java 字段, 映射出了 Trantor 的模型结构, 就是 Trantor 的元数据. 示例如下:
{ “name”: “地址”, “originalKey”: “address”, “resourceKey”: “base_Company_address”, “type”: “Text”, “javaType”: ”java.lang.String“, “typeMeta”: { "length": 256, "type": "Text" }, // …}DataStore 基于该字段的元数据, 可以生成后端表结构, 以 MySQL 为例如下:
`address` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL,而前端则同样基于该字段的元数据, 会自动声明成为一个地址组件, 且会根据数据容器不同, 选择不同的组件, 比如列表态, 搜索态, 编辑态, 只读态等, 还有相关的前后端校验逻辑, 都由 Trantor 前端自动完成了.
所以回头再看看我们写的这个字段, 因为我们写的, 是完全变化的部分, 就是这个业务模型需要一个什么字段, 以及前端是否需要这个字段. 剩下的所有行为, 都已经被 Trantor 抽象所覆盖了, 让我们的开发人员, 真的可以只关注业务逻辑, 尽可能少的关注前后端细节.
行为
前面讲述了模型定义了一个业务实体,视图定义了页面展示页面的样式。那么视图如何跳转,视图页面数据如何加载,触发一个按钮如何实现功能呢?行为就登场了。行为分为主要分为三类视图行为(ViewAction)、服务端行为(ServerAction)以及数据加载/处理行为。
从上面视图定义中,可以看出,视图跳转使用action=“base_Company_toDetail”,而业务端功能使用action=“base_Company_CompanyAction::enable”,之所以能够这么实现,是因为每个资源都对应一个唯一key,trantor通过key就能找到对应资源。
<RecordActions label="操作"> <Action label=“详情” action=“base_Company_toDetail”/> <Action label=“编辑” action=“base_Company_toEdit” /> <Action label=“启用” :show=“this.record.status == ‘Disable’” action=“base_Company_CompanyAction::enable” after=“Refresh” /> <Action label=“禁用” :show=“this.record.status == ‘Enable’” action=“base_Company_CompanyAction::disable” after=“Refresh” /></RecordActions>视图页面数据获取的方式则通过在视图定义确定,如下所示是一个典型的通过代码实现的获取列表数据的过程。当然trantor支持用户不用写代码只需要配置一些条件即可实现数据获取,无非是从通过一些查询条件直接从模型对应的表中获取数据。
[ { "forModel": "base_Company", "key": "toList", "name": "列表", "type": "View", "extra": { "dataSource": "Action", "openViewType": "Self", "target": "base_Company_toList", "actionKey": "base_Company_CompanyMultiDataAction" } }]交付的抽象
研发的抽象可以提升我们在研发过程的效率, 让我们用更少的代码表达更多的业务. 但是我们在真正做软件交付过程中, 更多的是在大软件上做修改, 所以我们提供了一套二开机制来支撑.