2025-07-28 19:17:52 +08:00
< TOCO知识库 >
2025-07-28 19:18:21 +08:00
-----------------------------------------------------------------------------
2025-06-18 15:28:20 +08:00
### **1. TOCO 平台概览:**
2025-08-19 18:51:41 +08:00
- **1.1 平台简介:** TOCO是软件设计和代码自动生成平台。基于DDD、分层架构、CQRS理论, 覆盖数据库到API的完整开发链路, 提升开发效率和代码质量
- **1.2 核心价值/目标用户:** 提供软件设计能力,设计成果直接生成标准代码,提高编码一致性和效率
- **1.3 主要特性概览:** 可视化设计、模型关联、多人协作、代码生成器
2025-06-18 11:10:27 +08:00
2025-06-18 15:28:20 +08:00
### **2. TOCO 设计元素:**
#### **2.1 模块 (Module)**
2025-06-18 11:10:27 +08:00
- **定义与用途:** 在TOCO中, 我们将系统领域细分为具体的模块, 映射为Java工程中的module。这些模块代表了系统的叶子子域, 每个模块负责特定的功能。模块划分有助于系统的可维护性和可扩展性, 并能提高开发效率和代码质量
2025-08-19 18:51:41 +08:00
- **关键配置:** 名称(小写英文+下划线, 如meeting,user_detail, 禁止后缀, 全局唯一),描述
- **与其他元素关系:** 下列所有设计元素都属于某个模块
- **代码产物:** 生成独立Java Module: 项目路径/modules/模块名, 包含entrance、service、manager、persist、common分层
2025-06-22 14:59:22 +08:00
2025-06-18 15:28:20 +08:00
#### **2.2 枚举 (Enum)**
2025-08-19 18:51:41 +08:00
- **定义与用途:** 表示常量值集合,可跨模块使用,可作为字段类型
- **关键属性/配置:** 名称(以_enum结尾, 全局唯一),枚举值列表(全大写+下划线)
- **与其他元素关系:** 可作为Entity、Dto、Vo、Bto、Qto、Eo的字段类型
2025-06-23 16:01:21 +08:00
- **Enum设计元素的表达:**
2025-08-19 18:51:41 +08:00
- Json格式, schema如下:
2025-06-19 16:07:18 +08:00
```json
{
"type": "object",
2025-08-19 18:51:41 +08:00
"properties": {
"name": { "type": "string","description": "名称,英语表达, 下划线分割, 不超过32字符"},
"uuid": { "type": "string","description": "唯一标识符,创建时为空,更新时必填"},
"description": { "type": "string", "description": "枚举含义和用途, 不超过128字符"},
"moduleName": { "type": "string", "description": "所属模块,创建时必填,更新时可空"},
2025-06-19 16:07:18 +08:00
"values": {
2025-06-24 14:29:45 +08:00
"type": "array","description": "枚举值列表",
2025-08-19 18:51:41 +08:00
"items": {"type": "string","description": "枚举值, 英语表达, 下划线分割, 不超过32字符"}
2025-06-19 16:07:18 +08:00
}
2025-08-19 18:51:41 +08:00
},
2025-06-19 16:07:18 +08:00
"required":["name","description"]
}
```
2025-06-22 14:59:22 +08:00
* **代码产物和修改建议**
2025-08-19 18:51:41 +08:00
* **生成产物:** common模块中生成Java类
* **职责:** 表达Enum数据结构
* **命名规则:** 类名以Enum结尾
* **类路径:** 位于 < code > **.common.enums</ code > 包下
* **唯一标识符位置:** 类注解@AutoGenerated中指定 , uuid规则: ${Enum的uuid}|ENUM|DEFINITION
- **生成代码:** 在common层生成Enum文件, 如StatusEnum
- **修改建议:** 不建议修改
2025-08-11 18:19:46 +08:00
#### **2.3 值对象 (EO)**
2025-08-19 18:51:41 +08:00
- **定义与用途:** EO是可复用的POJO数据结构, 可跨模块使用, 可作为实体字段类型。
- **关键属性/配置:** 名称以_eo结尾, 全局唯一。字段类型限制: 基本类型、List、EO、Enum。
- **与其他元素关系:** 可作为Entity、Dto、Vo、Bto、Qto的字段类型, 支持EO嵌套。
- **EO设计元素的表达: **
- 使用Json格式表达, Schema定义如下:
2025-06-19 16:07:18 +08:00
```json
{
"type":"object",
"properties": {
2025-08-19 18:51:41 +08:00
"name":{ "type": "string", "description": "名称, 英文下划线分割, 不超过32字符"},
"description": { "type": "string","description": "描述EO含义和用途, 不超过128字符"},
"uuid":{ "type": "string", "description": "唯一标识符,创建时为空,更新时必填"},
"moduleName":{ "type": "string", "description": "所属模块,创建时必填,更新时可空"},
2025-06-19 16:07:18 +08:00
"fieldList":{
2025-08-19 18:51:41 +08:00
"type":"array", "description": "EO属性字段列表",
"items":{
2025-06-19 16:07:18 +08:00
"type": "object",
"properties":{
2025-08-19 18:51:41 +08:00
"name": { "type": "string","description": "字段名称, 英文下划线分割, 不超过32字符"},
"uuid":{ "type": "string","description": "字段类型为Enum或Eo时必填, 对应的Enum或Eo标识符" } ,
"type":{ "type": "string","description": "字段类型: String,Integer,Long,Float,Double,Boolean,Date,Eo,Enum,BigDecimal,List" },
"innerUuid":{"type": "string", "description": "当innerType为Eo或Enum时, 对应的标识符"},
"innerType": { "type": "string", "description": "List元素类型: String,Integer,Long,Float,Double,Boolean,Date,Eo,Enum,BigDecimal"}
2025-06-19 16:07:18 +08:00
},
"required":[ "name","type"]
}
2025-08-19 18:51:41 +08:00
}
2025-06-19 16:07:18 +08:00
},
"required":["name","description"]
}
```
2025-06-22 14:59:22 +08:00
* **代码产物和修改建议**
2025-08-19 18:51:41 +08:00
* **生成产物:** persist层生成结构定义类, 如AddressEo
* **职责:** 表达POJO数据结构
* **命名规则:** 类名以Eo结尾
* **类路径:** 位于 `**.persist.eo` 包下
* **唯一标识符位置:** 类注解@AutoGenerated中指定 , uuid规则: ${Eo的uuid}|EO|DEFINITION
2025-07-02 15:07:32 +08:00
- **修改建议:** 不建议修改
2025-08-19 18:51:41 +08:00
2025-06-18 15:28:20 +08:00
#### **2.4 实体关系 (ER / Entity)**
2025-08-19 18:51:41 +08:00
- **定义与用途:** 实体对应数据库表,关系为实体间的外键依赖
- **关键属性/配置:** 包含名称、字段、字段类型、主键、索引, 关系分为1:1和1:N
- **与其他元素关系:** 实体是聚合的基础, 也是DTO和VO的派生基础
2025-06-22 14:59:22 +08:00
- **代码产物和修改建议**
2025-07-02 15:07:32 +08:00
- 结构定义
2025-08-19 18:51:41 +08:00
* **生成产物**: 按Mybatis-plus要求生成的Java类
* **职责:** 生成Mybatis-plus结构定义类文件
* **类路径:** 位于 `**.persist.dos` 包下
* **唯一标识符位置:** 类注解@AutoGenerated中指定 , uuid规则: ${Entity的uuid}|ENTITY|DEFINITION
2025-07-02 15:07:32 +08:00
- Mapper
2025-08-19 18:51:41 +08:00
* **生成产物**: persist层的Mybatis-plus Mapper类
* **职责:** 为Mybatis-plus框架提供Mapper
2025-07-02 15:07:32 +08:00
* **命名规则**: 类名以Mapper结尾(${entityName}Mapper)
2025-08-19 18:51:41 +08:00
* **类路径:** 位于 `**.persist.mapper.mybatis` 包下
* **唯一标识符位置:** 类注解@AutoGenerated中指定 , uuid规则: ${Entity的uuid}|ENTITY|MAPPER
2025-07-02 15:07:32 +08:00
- Dao接口
2025-08-19 18:51:41 +08:00
* **生成产物**: persist层的Dao接口
* **职责:** 提供Entity数据查询接口, 为service层提供数据访问入口
2025-07-02 15:07:32 +08:00
* **命名规则**: 类名以Dao结尾(${entityName}Dao)
2025-08-19 18:51:41 +08:00
* **类路径:** 位于 `**.persist.mapper` 包下
* **唯一标识符位置:** 类注解@AutoGenerated中指定 , uuid规则: ${Entity的uuid}|ENTITY|IDAO
2025-07-02 15:07:32 +08:00
- Dao实现
2025-08-19 18:51:41 +08:00
* **生成产物**: persist层的Dao接口实现类
* **职责:** 通过调用Mapper实现Dao接口
2025-07-02 15:07:32 +08:00
* **命名规则**: 类名以DaoImpl结尾(${entityName}DaoImpl)
2025-08-19 18:51:41 +08:00
* **类路径:** 位于 `**.persist.mapper` 包下
* **唯一标识符位置:** 类注解@AutoGenerated中指定 , uuid规则: ${Entity的uuid}|ENTITY|DAO
2025-07-02 15:07:32 +08:00
- **修改建议:** 不建议修改
2025-08-19 18:51:41 +08:00
2025-06-18 15:28:20 +08:00
#### **2.5 聚合对象 (BO/业务对象)**
2025-08-19 18:51:41 +08:00
- **定义与用途:** 聚合对象封装一组关联实体。从聚合根实体开始,按层级关系组装其他实体,形成树形结构。提供内存一致性视图和数据操作入口。一个实体只能属于一个聚合对象,聚合对象只能在单一模块中组合。
- **包含元素:** 聚合根实体 + 子实体对象。例如: ProductBO包含商品基本信息实体(聚合根)、商品SKU实体、商品库存实体(子对象)。
- **关键配置:** 名称(${EntityName驼峰}BO, 如StaffBO),聚合根实体,子对象实体。每个聚合必须包含一个聚合根。
- **与其他元素关系:** 聚合是写方案的基础。
2025-06-22 14:59:22 +08:00
- **代码产物和修改建议**
2025-07-02 15:07:32 +08:00
- 综述
2025-08-19 18:51:41 +08:00
- 业务对象包含多个Entity, 通过嵌套组合表达Entity关系。有子对象时生成BO和BaseBO, BaseBO封装实体属性和关系, BO用于业务扩展。无子对象时只生成BO类。
2025-07-02 15:07:32 +08:00
- BO
2025-08-19 18:51:41 +08:00
* **生成产物**: Manager层生成聚合对象类, 符合Hibernate标准
* **职责:** 定义聚合对象,组合成层级结构实现充血模型,支持写链路数据变更、监听变更、数据校验
* **命名规则**: ${entityName}BO
* **类路径:** `**.manager.bo` 包下
* **唯一标识符位置:** 类注解@AutoGenerated中 , uuid规则: ${Entity的uuid}|BO|DEFINITION
- **修改建议:** 建议修改BO中的validateAggregate或valid方法进行业务不变性校验。不建议修改校验方法以外的代码。校验方法由框架自动调用, 不需要业务代码显式调用。
2025-07-02 15:07:32 +08:00
- BaseBO
2025-08-19 18:51:41 +08:00
* **生成产物**: 存在子BO时生成, 封装不变代码部分
* **职责:** 定义聚合对象,组合成层级结构实现充血模型,支持写链路数据变更、监听变更、数据校验
* **命名规则**: ${entityName}BO
* **类路径:** `**.manager.bo` 包下
* **唯一标识符位置:** 类注解@AutoGenerated中 , uuid规则: ${Entity的uuid}|BO|DEFINITION
- **修改建议:** 建议修改BO中的validateAggregate或valid方法进行业务不变性校验。不建议修改校验方法以外的代码。
2025-06-18 15:28:20 +08:00
#### **2.6 数据传输对象 (DTO)**
2025-08-19 18:51:41 +08:00
- **定义与用途:** DTO基于某个Entity构建, 通过外键关系关联多个Entity的数据结构。DTO隐含数据组装逻辑, 符合外键关系。分为BaseDTO和普通DTO: BaseDTO直接派生自Entity, 包含Entity所有字段, 每个Entity只有一个BaseDTO; 普通DTO基于BaseDTO创建, 包含BaseDTO所有字段, 可增加扩展字段或自定义字段。DTO不能作为接口参数, 不能作为HTTP API返回值。
- **创建方式:** 每个Entity自动生成一个BaseDTO, 命名为${Entity名字}BaseDto, 如UserBaseDto, 包含Entity全部字段。其他DTO需手动基于BaseDTO创建。判断是否为BaseDTO: 是则通过Entity名称获取; 否则通过DTO要表达的信息创建, 如会议及议程信息。
- **关键配置:** 名称( BaseDTO以BaseDto结尾, 其他DTO以Dto结尾, 全局唯一) 、根Entity、字段列表。字段分三种: a.继承Entity或BaseDTO的字段, 类型相同; b.扩展字段, 包含正向替换和反向注入字段, 类型为DTO或List< DTO > ; c.自定义字段, 类型为基本类型、Eo、Enum、DTO。BaseDTO包含Entity全部字段, DTO包含BaseDTO全部字段, 不裁剪字段, 可通过外键关系扩展其他Entity, 无法扩展时可增加自定义字段。
- **字段扩展方式:** DTO通过外键关系替换/注入对应Entity信息。满足条件可扩展: a.正向替换: 当前实体有指向其他实体的外键字段; b.反向注入:其他实体有指向当前实体的外键字段。
例如: 两个Entity
```
2025-06-18 11:10:27 +08:00
MeetingRoom{ //会议室
Long id;// 会议室id,主键
String name;// 会议室名称
}
Meeting { //会议
Long id;// 会议id,主键
2025-08-19 18:51:41 +08:00
Long roomid; //会议室id外键, 到MeetingRoom的n:1关系
Long backupRoomid; //备用会议室id外键, 到MeetingRoom的n:1关系
2025-06-18 11:10:27 +08:00
String title; //会议标题
2025-08-19 18:51:41 +08:00
DateTime startTime; //开始时间
DateTime endTime; //结束时间
2025-06-18 11:10:27 +08:00
}
2025-08-19 18:51:41 +08:00
```
Meeting和MeetingRoom是n:1关系。多个会议占用同一个会议室。
组装对象以某Entity为根时, 首先拥有该Entity相同数据结构, 通过"正向替换"、"反向注入"行为, 递归组装有外键关系的Entity信息。
**正向替换:** 选定表存在到另一表的外键。选择相关的外键属性,将该外键替换为另一表为根的组装对象。获取基于外键且包含另一表详细属性的数据。
例如: 需要会议和占用会议室时, 将Meeting表中roomid外键替换为以MeetingRoom为根的组装对象, backupRoomid不替换:
```
2025-06-18 11:10:27 +08:00
MeetingWithRoomDto {
Long id;// 会议id
2025-08-19 18:51:41 +08:00
MeetingRoomDto room { //正向替换会议室信息,以会议室为根的对象
Long id; //会议室ID
2025-06-18 11:10:27 +08:00
String name;// 会议室名称
}
2025-08-19 18:51:41 +08:00
Long backupRoomid; // 不变化
2025-06-18 11:10:27 +08:00
String title; //会议标题
2025-08-19 18:51:41 +08:00
DateTime startTime; //开始时间
DateTime endTime; //结束时间
2025-06-18 11:10:27 +08:00
}
2025-08-19 18:51:41 +08:00
```
**反向注入:** 选定表, 如有其他表到该表有外键, 选择相关的外键属性在选定表中增加以另一表为根的组合对象( 1:1时) 或组合对象列表( n:1时) 。
需求:"获取会议室和占用它的会议信息", 选定MeetingRoom, 基于Meeting表存在roomid字段为到MeetingRoom的n:1外键。将List< MeetingBaseDto > 反向注入到MeetingRoom中:
```
2025-06-18 11:10:27 +08:00
MeetingRoomWithMeetingDto {
Long id;// 会议室id
String name;// 会议室名称
2025-08-19 18:51:41 +08:00
List< MeetingBaseDto > meetingList { //反向注入使用该会议室的会议信息
2025-06-18 11:10:27 +08:00
Long id;// 会议id
String title; //会议标题
2025-08-19 18:51:41 +08:00
DateTime startTime; //开始时间
DateTime endTime; //结束时间
2025-06-18 11:10:27 +08:00
}
}
2025-08-19 18:51:41 +08:00
```
"正向替换"和"反向注入"可按需递归调用,组装多个有外键关系的对象。
- **JSON结构描述:** DTO用json结构表示, 用于理解含义或作为创建、更新DTO工具的参数。字段含义: dto的uuid为唯一标识, 创建时设置为null, 复用时填入uuid。expandList为正向替换, reverseExpandList为反向注入, customFieldList为自定义字段。expandList中, foreignKeyInThisEntity为本表外键字段名, dtoFieldName为替换后字段名; reverseExpandList中, foreignKeyInOtherEntity为他表外键字段名, dtoFieldName为注入后字段名; customFieldList中, uuid为自定义字段UUID, 创建时不填入, 更新时需传入用于定位; typeUuid为类结构UUID, type为Enum、Eo时包含; innerType为List内部类型, type为List时包含; innerUuid为List内部类结构UUID, type为List且innerType为Enum、Eo时包含。
示例:
2025-06-19 16:52:57 +08:00
- meeting_with_room_dto
2025-08-19 18:51:41 +08:00
```json
2025-06-19 16:52:57 +08:00
{
2025-06-29 17:24:56 +08:00
"dto": {
"uuid": null,
"name": "meeting_with_room_dto",
2025-08-19 18:51:41 +08:00
"description": "会议详情,包含会议室信息",
2025-06-29 17:24:56 +08:00
"fromEntity": "meeting",
"expandList": [
{
"foreignKeyInThisEntity": "room_id",
"dtoFieldName": "meeting_room",
"dto": {
"uuid": "d05c7b3d-1c92-45a1-2113-a01b245813c1",
"name": "meeting_room_with_meetings_dto",
"fromEntity": "meeting_room",
"description": "会议室信息,包含会议列表"
}
}
],
"customFieldList":[
{
2025-08-19 18:51:41 +08:00
"uuid": "自定义字段唯一标识, 更新DTO时需传入",
2025-06-29 17:24:56 +08:00
"name": "status",
"type": "Enum",
"typeUuid": "对应Enum的uuid",
"description": "当前状态"
}
]
}
2025-06-19 16:52:57 +08:00
}
2025-08-19 18:51:41 +08:00
```
2025-06-19 16:52:57 +08:00
- meeting_room_with_meetings_dto
2025-08-19 18:51:41 +08:00
```json
2025-06-19 16:52:57 +08:00
{
2025-06-29 17:24:56 +08:00
"dto": {
"uuid": "d05c7b3d-1c92-45a1-2113-a01b245813c1",
"name": "meeting_room_with_meetings_dto",
2025-08-19 18:51:41 +08:00
"description": "会议室详情,包含会议信息",
"fromEntity": "meeting_room",
2025-06-29 17:24:56 +08:00
"reverseExpandList": [
{
"foreignKeyInOtherEntity": "room_id",
"dto": {
"uuid": "ffeec02d-2a32-1531-1ce1-b9bfc1993765",
"name": "meeting_base_dto",
"description": "会议基本信息",
"fromEntity": "meeting"
},
"dtoFieldName": "meeting_list"
}
]
}
2025-06-19 16:52:57 +08:00
}
2025-08-19 18:51:41 +08:00
```
meeting_with_room_dto无uuid, 为待创建DTO。meeting_base_dto和meeting_room_with_meetings_dto为已存在DTO, 带uuid。
2025-06-19 16:52:57 +08:00
2025-08-19 18:51:41 +08:00
- **预定义方法:** 每个DTO基于根Entity的唯一索引自动生成预定义RPC方法及实现, 预定义方法获取根Entity数据, 通过RPC自动获取所有扩展字段数据并拼装。如实体user有唯一索引username, 则为UserDto生成UserDto UserDtoService.getByUserName(String userName)和List< UserDto > UserDtoService.getByUserNames(List< String > userNames)。预定义方法内部基于外键关系自动生成复杂DTO数据的递归、Join拼装能力, 直接返回DTO内部所有继承字段和扩展字段数据。自定义字段数据获取不自动生成, 需在对应convert方法中编写代码。
- **公开性:** DTO可设置公开性, DTO为公开时生成的预定义RPC方法也为公开RPC, 可被其他模块订阅调用; DTO为非公开时生成的预定义RPC方法也为非公开RPC, 其他模块不可见。
- **跨模块依赖:** 如DTO内引用其他模块DTO, 需订阅其他模块RPC方法( getBy${PrimaryKey}、getBy${PrimaryKey}s、getBy${foreignKey}等) , 用于获取对应DTO。
- **复杂嵌套DTO获取流程:** 获取DTO有2种方式, 都可直接获取复杂嵌套DTO数据:
- 第1种: 通过预定义方法获取DTO
- 第2种: 通过读方案获取DTO
2025-06-18 11:10:27 +08:00
2025-08-19 18:51:41 +08:00
判断使用方式的步骤:
- a. 查询DTO条件为主键或唯一索引的值或列表, 使用第1种方式
- b. 通过其他复杂查询条件, 采用第2种方式
判断使用方式时, 只能根据查询条件判断是否使用读方案, 禁止使用返回值是否需要数据拼装来判断! 如现实代码和TOCO定义有冲突, 只能使用TOCO定义!
2025-06-18 11:10:27 +08:00
2025-06-22 14:59:22 +08:00
- **代码产物和修改建议**
2025-07-02 15:07:32 +08:00
- 结构定义
2025-08-19 18:51:41 +08:00
* **生成产物**: Java类
* **职责:** 表达DTO数据结构
2025-07-02 15:07:32 +08:00
* **命名规则**: 类名以Dto结尾
2025-07-17 11:00:51 +08:00
* **禁止** 修改该类
2025-08-19 18:51:41 +08:00
* **类路径:** `**.manager.dto` 包路径下
* **唯一标识符位置:** 类注解@AutoGenerated中指定 , uuid规则: ${DTO在TOCO中的uuid}|DTO|DEFINITION
2025-07-02 15:07:32 +08:00
- **Manager**
2025-08-19 18:51:41 +08:00
* **生成产物:** Java接口及实现类
* **命名规则:** 接口类名以Manager结尾、实现类名以ManagerImpl结尾(${DtoName}Manager)、基类名以ManagerBaseImpl结尾(${DtoName}ManagerImpl)
* **职责:** 提供DTO数据获取接口, 包括根据id单个、id列表批量获取、根据DTO对应实体的数据库索引获取
* **类路径:** `**.manager` 包路径下
* **禁止** 删除该类中任何系统自动生成函数
* **唯一标识符位置:** 类注解@AutoGenerated中指定 , uuid规则: ${DTO在TOCO中的uuid}|DTO|MANAGER
2025-07-02 15:07:32 +08:00
- **Converter**
2025-08-19 18:51:41 +08:00
* **生成产物:** Java实现类及基类
* **禁止** 删除该类中任何系统自动生成函数
2025-07-02 15:07:32 +08:00
* **命名规则:** 实现类名以Converter结尾(${DtoName}Converter)、基类名以BaseConverter结尾(${DtoName}BaseConverter)
2025-08-19 18:51:41 +08:00
* **职责:** Entity转换到BaseDTO或BaseDTO转换为普通DTO: 从Entity转为BaseDTO的方法命名为convert${EntityName}To${DtoName}; 从BaseDTO转换为DTO的方法命名为convert${BaseDtoName}To${DtoName}
* **类路径:** `**.manager.converter` 包路径下
2025-07-02 15:07:32 +08:00
* **唯一标识符位置:**
2025-08-19 18:51:41 +08:00
* 实现类Converter类注解@AutoGenerated中指定 , uuid规则: ${DTO在TOCO中的uuid}|DTO|CONVERTER
* 基类BaseConverter类注解@AutoGenerated中指定 , uuid规则: ${DTO在TOCO中的uuid}|DTO|BASE_CONVERTER
2025-07-02 15:07:32 +08:00
- **例子:**
2025-08-19 18:51:41 +08:00
* 如UserDto、UserDtoManager、UserDtoConverter extends UserDtoBaseConverter、UserDtoService( 名称为${DtoName}Service, 内部包含getBy${PrimaryKey}、getBy${PrimaryKey}s等方法) 。如Dto为UserBaseDto, 则生成类名为UserBaseDtoService
2025-07-02 15:07:32 +08:00
- **修改建议:**
2025-08-19 18:51:41 +08:00
- 建议在Service与BaseConverter中扩展代码, 不建议修改结构定义文件和Manager文件。DTO的自定义字段不直接派生自Entity, 一般对应取数逻辑代码。涉及数据获取、计算和拼装, 批量处理性能最好, 代码位置必须放在BaseConverter中已自动生成的列表转换方法中, 批量取数组装, 如UserBaseDtoBaseConverter.convertUserToUserBaseDto(List< User > )或UserDtoBaseConverter.convertUserBaseDtoToUserDto(List< UserBaseDto > )
2025-06-22 14:59:22 +08:00
2025-06-18 15:28:20 +08:00
#### **2.7 视图对象 (VO)**
2025-08-20 10:28:34 +08:00
- **定义与用途:** VO基于BaseDTO( 或指定的其他DTO) 派生, 通过外键关系关联多个BaseDTO的数据结构。用于视图层与前端数据传输, 作为HTTP API返回值或读方案返回值。不能作为接口参数或RPC返回值。
- **关键配置:** 名称( 以Vo结尾, 全局唯一) 、根Entity、派生源、字段列表。字段分三种: a.继承DTO字段( 基础类型/EO/Enum保持类型不变, DTO类型转为对应VO类型) ; b.扩展字段( 正向替换和反向注入, 类型为VO或List< VO > ) ; c.自定义字段( 基本类型或VO类型) 。可裁剪无用字段, 可通过外键关系扩展其他BaseDto。无法扩展时可增加自定义字段。
- **继承字段类型转换:** VO只与派生源VO有转换关系。当VO派生自某DTO, 且继承了派生源DTO中的DTO类型字段( 假设DTO-A) , 则VO中对应字段类型必须为VO( 假设VO-A) , 且**VO-A必须派生自DTO-A**,以维持转换关系。
- **与DTO的区别:** DTO用于服务层传输, 通常作RPC返回值, 与数据模型更近, 复用性强; VO用于视图层传输, 通常作API返回值, 与UI展示更近, 可裁剪冗余字段, 复用性弱。
- **创建方式:** VO通常基于某个BaseDTO及外键关系派生, 也可直接创建与DTO无关的全自定义字段VO( 尽量少用, 仅用于特殊页面组装无关数据的场景) 。
- **根VO和子VO:** VO分两种: 1.根VO: 最外层VO结构, 需通过TOCO创建, 有uuid唯一标识, 可被其他根VO或子VO引用; 2.子VO: 根VO内部嵌套VO, 通过外键关系关联BaseDTO后自动创建, 仅附属于一个根VO, 无uuid。需描述VO字段和扩展关系, 通过**创建根VO**使TOCO**自动创建**子VO或引用其他根VO。
- **字段扩展方式:** 同DTO字段扩展方式, 通过任意**一个**派生源BaseDTO, 经外键扩展成复杂嵌套VO。满足条件即可扩展: 正向替换( 当前实体有指向其他实体的外键字段) ; 反向注入( 其他实体有指向当前实体的外键字段) 。
例如: 两个BaseDTO
```
2025-06-18 11:10:27 +08:00
MeetingRoomBaseDto{ //会议室
Long id;// 会议室id,主键
String name;// 会议室名称
}
MeetingBaseDto { //会议
Long id;// 会议id,主键
Long roomid; //占用的会议室id, 到MeetingRoom的外键, n:1关系
Long backupRoomid; //备用的会议室id, 到MeetingRoom的外键, n:1关系
String title; //会议标题
DateTime startTime; //会议开始时间
DateTime endTime; //会议结束时间
}
2025-08-20 10:28:34 +08:00
```
Meeting和MeetingRoom是n:1关系。多个会议占用同一个会议室。
通过"正向替换"组装出:
```
MeetingWithRoomVo { //根VO, 需通过TOCO创建
2025-06-18 11:10:27 +08:00
Long id;// 会议id
2025-08-20 10:28:34 +08:00
MeetingRoomVo room { //正向替换会议室信息, TOCO自动生成的内部VO, 派生自MeetingRoomBaseDto
2025-06-18 11:10:27 +08:00
Long id; //会议室ID
String name;// 会议室名称
}
2025-08-20 10:28:34 +08:00
Long backupRoomid; // 不变
2025-06-18 11:10:27 +08:00
String title; //会议标题
DateTime startTime; //会议开始时间
DateTime endTime; //会议结束时间
}
2025-08-20 10:28:34 +08:00
```
通过"反向注入"生成:
```
2025-06-18 11:10:27 +08:00
MeetingRoomWithMeetingVo{
Long id;// 会议室id
String name;// 会议室名称
2025-08-20 10:28:34 +08:00
List< MeetingVo > meetingList{ //反向注入使用该会议室的会议信息, TOCO自动生成的内部VO, 派生自MeetingBaseDto
2025-06-18 11:10:27 +08:00
Long id;// 会议id
String title; //会议标题
DateTime startTime; //会议开始时间
DateTime endTime; //会议结束时间
}
}
2025-08-20 10:28:34 +08:00
```
- **JSON结构描述:** VO用json结构表示, 用于理解含义或作为创建、更新VO工具参数。字段含义: expandList为正向替换, reverseExpandList为反向注入, extendFieldList为来自派生源DTO的字段, customFieldList为自定义字段。expandList中, foreignKeyInThisEntity为本表外键字段名, voFieldName为替换后字段名; reverseExpandList中, foreignKeyInOtherEntity为他表外键字段名, voFieldName为注入后字段名; customFieldList中, uuid为自定义字段UUID( 创建时不填, 更新时需传入定位) ; typeUuid为类结构UUID( type为List且innerType=Enum、Eo时包含) ; extendFieldList中, name为继承字段名, 若DTO字段非DTO类型则VO字段类型与DTO一致, 若DTO字段为DTO或List< DTO > 类型, 由于VO字段不能为DTO, **必须**将DTO转换为VO, 所以extendFieldList会有**vo结构**表示该字段DTO类型派生出的VO定义, 注意: 该VO字段中的VO类型必须派生自继承的DTO字段类型!
示例: 系统中存在meeting_detail_dto
```
2025-06-25 15:23:05 +08:00
{
"uuid": "cd55c96b-aa67-bfb2-7614-70b503a8f8bf",
"name": "meeting_detail_dto",
"fromEntity": "meeting",
"description": "会议详情",
"expandList": [
{
2025-06-26 15:33:23 +08:00
"foreignKeyInThisEntity": "create_user_id",
2025-06-26 15:23:08 +08:00
"voFieldName": "create_user",
2025-06-25 15:23:05 +08:00
"dto": {
"uuid": "53bb59cf-1ed2-6fb3-9f61-895b638903d8",
"name": "user_base_dto",
"fromEntity": "user",
"description": "用户基本信息"
}
}
]
}
2025-08-20 10:28:34 +08:00
```
- meeting_with_room_vo由meeting_detail_dto派生, 继承id和create_user字段
```
2025-06-19 16:52:57 +08:00
{
"vo": {
"uuid": null,
"name": "metting_with_room_vo",
"description": "会议详情,包含会议室信息,以及会议室禁用列表",
2025-06-25 17:29:57 +08:00
"rootVo": "metting_with_room_vo",
2025-06-19 16:52:57 +08:00
"fromEntity": "meeting",
"fromDto": "meeting_detail_dto",
"fromDtoUuid": "cd55c96b-aa67-bfb2-7614-70b503a8f8bf",
"extendFieldList":[
{
"name": "id"
},
{
2025-06-24 19:15:04 +08:00
"name": "create_user",
"vo": {
2025-06-25 17:00:41 +08:00
"uuid": null,
2025-06-24 19:15:04 +08:00
"name": "user_base_vo",
"description": "用户信息",
2025-06-25 17:29:57 +08:00
"rootVo": "metting_with_room_vo",
2025-06-24 19:15:04 +08:00
"fromEntity": "user",
"fromDto": "user_base_dto",
"fromDtoUuid": "53bb59cf-1ed2-6fb3-9f61-895b638903d8",
"reverseExpandList": [],
"customFieldList":[],
"extendFieldList":[
{
"name": "name"
},
{
"name": "gender"
}
]
}
2025-06-19 16:52:57 +08:00
}
],
"expandList": [
{
2025-06-26 15:33:23 +08:00
"foreignKeyInThisEntity": "room_id",
2025-06-26 15:23:08 +08:00
"voFieldName": "meeting_room",
2025-06-19 16:52:57 +08:00
"vo": {
"name": "meeting_room_with_meetings_vo",
"description": "会议室信息,包含会议列表",
2025-06-25 17:29:57 +08:00
"rootVo": "metting_with_room_vo",
2025-06-19 16:52:57 +08:00
"fromEntity": "meeting_room",
"fromDto": "meeting_room_base_dto",
"fromDtoUuid": "88437212-6370-99a6-1e7a-fe1469082d08",
"reverseExpandList": [
{
2025-06-26 15:33:23 +08:00
"foreignKeyInOtherEntity": "room_id",
2025-06-19 16:52:57 +08:00
"vo": {
2025-06-25 17:00:41 +08:00
"uuid": null,
2025-06-19 16:52:57 +08:00
"name": "meeting_base_vo",
"description": "会议基本信息",
2025-06-25 17:29:57 +08:00
"rootVo": "metting_with_room_vo",
2025-06-19 16:52:57 +08:00
"fromEntity": "meeting",
"fromDto": "meeting_base_dto",
"fromDtoUuid": "1a768c5e-b449-db5d-fe55-9d572d64332a",
"extendFieldList":[
{
"name": "startTime"
},
{
"name": "endTime"
}
]
},
2025-06-26 15:23:08 +08:00
"voFieldName": "meeting_list"
2025-06-19 16:52:57 +08:00
}
],
"customFieldList":[
{
2025-06-20 13:52:02 +08:00
"uuid": "自定义字段的唯一标识, 更新DTO的时候需要传入",
2025-06-19 16:52:57 +08:00
"name": "occupied",
"type": "Boolean",
"description": "是否被占用"
},
{
2025-06-20 13:52:02 +08:00
"uuid": "自定义字段的唯一标识, 更新DTO的时候需要传入",
2025-06-19 16:52:57 +08:00
"name": "custom_eo",
2025-06-20 13:52:02 +08:00
"type": "Eo",
"typeUuid": "uuid of an eo"
2025-06-19 16:52:57 +08:00
},
{
2025-06-20 13:52:02 +08:00
"uuid": "自定义字段的唯一标识, 更新DTO的时候需要传入",
2025-06-19 16:52:57 +08:00
"name": "status_list",
"type": "List",
"innerType": "Enum",
"innerUuid": "uuid of an enum"
},
{
2025-06-20 13:52:02 +08:00
"uuid": "自定义字段的唯一标识, 更新DTO的时候需要传入",
2025-06-19 16:52:57 +08:00
"name": "custom_string_list",
"type": "List",
"innerType": "String"
}
],
"extendFieldList":[
{
"name": "id"
},
{
"name": "name"
}
]
}
}
]
}
}
2025-08-20 10:28:34 +08:00
```
2025-06-19 16:52:57 +08:00
2025-08-20 10:28:34 +08:00
meeting_with_room_vo为根VO, 无uuid, 为待创建根VO。meeting_room_with_meetings_vo和meeting_base_vo为meeting_with_room_vo的子VO, 无法被其他根VO引用, 无uuid。注意: metting_with_room_vo的extendFieldList中create_user结构含vo, 因为派生源meeting_detail_dto中create_user字段为user_base_dto类型, 所以metting_with_room_vo中该继承字段类型需变为由user_base_dto派生出的VO结构, 注意metting_with_room_vo中create_user对应的user_base_vo**必须派生自**DTO字段的user_base_dto类型。
2025-06-19 16:52:57 +08:00
2025-08-20 10:28:34 +08:00
- **派生源默认使用BaseDTO:** 除非用户指定VO的派生源DTO, 否则创建VO时只可用**BaseDTO**为派生源。
- **与DTO的转换关系:** 创建**有派生源**的VO后, TOCO生成代码时自动生成2种convert方法: 1.基础convert方法, 从DTO转换为VO, 仅转换结构及基本类型字段的get/set, 方法命名为convertTo${VoName}、convertTo${VoName}List、convertTo${VoName}Map, 其中**Map转换方法**为底层批量方法, 通常是自定义字段逻辑编写位置( 复用性好, 被其他convert方法调用) , 单个和列表convert方法都通过**调用Map方法**实现; 2.带数据拼装逻辑的convert方法, 内部**自动**调用基础convert方法从DTO转换为VO并设置基本类型字段数据, 然后根据外键**自动**获取**扩展字段**数据拼装最终数据, 方法命名为convertAndAssembleData、convertAndAssembleDataList, 即这两个方法已**自动**获取所有**继承字段**和**扩展字段**数据。这2种方法代码生成在VO对应Converter类中。
- **字段数据获取:** 继承自DTO的字段及扩展字段, TOCO在convert方法中自动生成数据获取代码, 无需手动实现获取和拼装逻辑。自定义字段则**必须**在最底层convertTo${VoName}Map方法中实现对应获取和拼装逻辑, 便于其他convert方法**复用**这段逻辑。**禁止**在其他convert方法中实现自定义字段逻辑, 会导致某些场景下数据拼装不完整。
- **跨模块依赖:** VO内存在由其他模块DTO派生出的子VO, 需订阅其他模块RPC(getBy${PrimaryKey},getBy${PrimaryKey}s,getBy${ForeignKey}s等)方法, 获取对应DTO, 再转换为子VO。
- **复杂嵌套VO获取流程:** 获取VO通常有3种方式, 3种方式都可直接获取复杂嵌套VO数据:
- 第1种: 先通过预定义方法获取VO派生源BaseDTO, 再通过convertAndAssembleData或convertAndAssembleDataList方法转换成VO
- 第2种: 先通过读方案获取VO派生源BaseDTO, 再通过convertAndAssembleData或convertAndAssembleDataList方法转换成VO
- 第3种: 通过读方案直接获取VO
2025-06-18 11:10:27 +08:00
2025-08-20 10:28:34 +08:00
TOCO中判断使用方式的步骤:
- a. 查询VO条件为主键或唯一索引的值或列表, 使用第1种方式
- b. 通过其他复杂查询条件, 可采用第2种或第3种方式
- c. 用户上下文中有指定返回DTO的读方案符合条件, 使用第2种, 否则使用第3种
2025-06-18 11:10:27 +08:00
2025-08-20 10:28:34 +08:00
注意判断使用方式时,只能根据查询条件判断是否使用读方案,绝对禁止使用返回值是否需要数据拼装来判断!**现实代码编写方式和TOCO步骤冲突时, 只能使用TOCO定义! **
2025-06-22 14:59:22 +08:00
- **代码产物和修改建议**
2025-07-02 15:07:32 +08:00
- **结构定义**
2025-08-20 10:28:34 +08:00
* **生成产物:** controller层生成Java类
2025-07-02 15:07:32 +08:00
* **命名规则:** 类名以Vo结尾
2025-08-20 10:28:34 +08:00
* **职责:** 表达VO数据结构
* **类路径:** `**.entrance.web.vo` 包路径下
* **唯一标识符位置:** 类注解@AutoGenerated中指定 , uuid规则: ${VO在TOCO中的uuid}|VO|DEFINITION
2025-07-02 15:07:32 +08:00
- **Converter**
2025-08-20 10:28:34 +08:00
* **生成产物:** controller层生成Java类( **有派生源**的VO才有Converter) 和基类
2025-07-02 15:07:32 +08:00
* **命名规则:** 实现类名以Converter结尾(${VoName}Converter), 基类名以BaseConverter结尾(${VoName}BaseConverter)
2025-08-20 10:28:34 +08:00
* **类路径:** `**.entrance.web.converter` 包路径下
* **职责:** 把DTO转换成VO; Converter含2种convert方法: 1.基础convert方法, 从DTO转换为VO, 仅转换结构, 方法命名为convertTo${VoName}、convertTo${VoName}List、convertTo${VoName}Map, 其中**Map转换方法**为底层批量方法, 单个和列表convert方法都通过**调用Map方法**实现; 2.带数据拼装逻辑的convert方法, 内部调用基础convert方法从DTO转换为VO, 然后根据外键获取拼装最终数据, 方法命名为convertAndAssembleData、convertAndAssembleDataList
2025-07-02 15:07:32 +08:00
* **唯一标识符位置:**
2025-08-20 10:28:34 +08:00
* 实现类Converter类注解@AutoGenerated中指定 , uuid规则: ${VO在TOCO中的uuid}|VO|CONVERTER
* 基类BaseConverter类注解@AutoGenerated中指定 , uuid规则: ${DTO在TOCO中的uuid}|DTO|BASE_CONVERTER
2025-07-02 15:07:32 +08:00
* **例子:**
2025-08-20 10:28:34 +08:00
* 如UserDetailVo、UserDetailVoConverter( 含convertToUserDetailVo、convertToUserDetailVoList、convertToUserDetailVoMap、convertAndAssembleData、convertAndAssembleDataList方法)
2025-07-02 15:07:32 +08:00
- **修改建议:**
2025-08-20 10:28:34 +08:00
- 建议在Converter中扩展代码, 不建议修改结构定义文件。VO的**自定义字段**不直接派生自DTO, 一般对应取数逻辑代码。涉及数据获取、计算和拼装时, 批量处理性能最好, 自定义字段对应代码位置**必须**放在Converter的**Map**基础转换方法convertTo${VoName}Map中, 批量取数组装, 如UserVoConverter.convertToUserVoMap
2025-06-22 16:58:06 +08:00
#### **2.8 查询对象(WO)**
2025-08-20 10:36:53 +08:00
- **定义与用途:** WO基于Entity通过外键关系关联多个Entity的数据结构。WO表达数据取数拼装逻辑, 符合外键关系。WO作为ReadPlan查询上下文, 创建ReadPlan前需先创建WO对象。理解ReadPlan语义时需要WO作为上下文。
当返回DTO|VO时需要对列表属性进行过滤, 则根据DTO|VO结构定义, 扩展对应WO对象( 过滤字段名需与DTO|VO字段名一致) 。例如:
2025-06-29 17:24:56 +08:00
查询DTO
2025-08-20 10:36:53 +08:00
```java
class MeetingDto {
String meetingId;
String meetingName;
List< MeetingAgenda > agendaList;
}
```
需求:根据会议名称查询会议列表,根据议程信息过滤部分议程。定义查询对象时需包含议程信息,扩展字段名定义为**agendaList**
- **查询对象设计元素表达**
以json格式表达, json schema如下:
```json
{
"type": "object","description": "查询对象定义", "required": ["dtoOrVoId","name","fromEntity"],
"properties": {
"name": {"type": "string", "description": "查询对象名称, 英语表达, 下划线分割, 不超过32字符"},
"uuid": {"type": "string", "description": "查询对象uuid, 更新时必传( 只有根节点) , 创建时不传"},
"dtoOrVoId":{"type":"string","description":"返回数据对象(VO或DTO)的uuid, 创建时必填, 更新时不传"},
"moduleName": {"type": "string", "description": "查询对象所属模块名称,创建时必传"},
"fromEntity": {"type": "string", "description": "查询对象对应的实体"},
"expandList": {
"type": "array", "description": "正向扩展列表",
"items": {
"type": "object","description": "正向扩展定义","required": ["field","wo","fieldName"],
"properties": {
"field": {"type": "string", "description": "fromEntity扩展字段, 必须是fromEntity外键字段"},
"wo": {"$ref": "#"},
"fieldName": {"type": "string", "description": "扩展字段名称"}
}
}
},
"reverseExpandList": {
"type": "array", "description": "反向扩展列表",
"items": {
"type": "object","description": "反向扩展定义","required": ["field","wo","fieldName"],
"properties": {
"field": {"type": "string", "description": "fromEntity扩展字段, 必须是fromEntity外键字段"},
"wo": {"$ref": "#"},
"fieldName": {"type": "string", "description": "扩展字段名称"}
2025-06-22 16:58:06 +08:00
}
}
}
2025-08-20 10:36:53 +08:00
}
2025-06-22 16:58:06 +08:00
}
2025-08-20 10:36:53 +08:00
```
- **创建/生成方式:**
- **创建思路** 按查询返回的DTO|VO结构, 构建同构WO对象( 扩展和反向扩展字段名保持一致) , 根据查询需求和过滤需求对WO二次裁剪或扩展:
- 去掉过滤和查询都不需要的扩展
- 补全查询或字段过滤需要的扩展
- **关键配置:** WO字段分三种: a.继承Entity字段, 类型与Entity相同; b.扩展字段, 含正向替换和反向注入字段, 类型为WO或List< WO >
- **字段扩展方式:** WO通过外键关系替换/注入对应Entity信息, 对象化表达有外键关系的Entity信息。存在外键关系且满足条件即可扩展: a.正向替换: 当前实体有指向其他实体的外键字段; b.反向注入:其他实体有指向当前实体的外键字段。
例如: 两个Entity
```java
MeetingRoom{ //会议室
Long id;// 会议室id,主键
String name;// 会议室名称
}
Meeting { //会议
Long id;// 会议id,主键
Long roomid; //占用会议室id, 到MeetingRoom外键, n:1关系
Long backupRoomid; //备用会议室id, 到MeetingRoom外键, n:1关系
String title; //会议标题
DateTime startTime; //会议开始时间
DateTime endTime; //会议结束时间
}
```
Meeting和MeetingRoom是n:1关系。多个会议占用同一个会议室。
组装对象以某Entity为根时, 首先拥有与该Entity相同数据结构, 通过"正向替换"、"反向注入"行为, 递归组装有外键关系的Entity信息。
**正向替换**:选定表,该表有到另一表的外键。选择需求相关的外键属性,将外键属性替换为另一表为根的组装对象。获取基于外键且包含另一表详细属性的数据。
例如: 需要会议和占用会议室时, 将Meeting表中roomid外键替换为以MeetingRoom为根的组装对象, backupRoomid不替换:
```
MeetingWithRoomWo {
Long id;// 会议id
MeetingRoomWo room { //正向替换会议用的会议室信息,以会议室为根的对象
Long id; //会议室ID
String name;// 会议室名称
}
Long backupRoomid; // 不变化
String title; //会议标题
DateTime startTime; //会议开始时间
DateTime endTime; //会议结束时间
}
```
**反向注入**:选定表,其他表到该表有外键,选择需求相关的外键属性在选定表中增加以另一表为根的组合对象(1:1关系时)或组合对象列表(n:1关系时)。
需求:"获取会议室和占用它的会议信息", 选定MeetingRoom, 基于Meeting表存在roomid字段为到MeetingRoom的n:1关系外键。将List< MeetingBaseDto > 反向注入到MeetingRoom中:
```
MeetingRoomWithMeetingWo {
Long id;// 会议室id
String name;// 会议室名称
List< MeetingBaseWo > meetingList { //反向注入使用该会议室的会议信息,以会议为根的对象
Long id;// 会议id
String title; //会议标题
DateTime startTime; //会议开始时间
DateTime endTime; //会议结束时间
}
}
```
"正向替换"和"反向注入"可按需递归调用, 组装多个有外键关系的对象。例如, MeetingAgenda到Meeting有n:1外键, 组装以Meeting开始, 包含MeetingRoom、MeetingAgenda的查询对象, MeetingRoom可正向扩展到Meeting, 可反向注入MeetingAgenda。
meeting_with_room_and_agenda_wo示例:
```json
2025-06-22 16:58:06 +08:00
{
"wo": {
"uuid": null,
"name": "meeting_with_room_and_agenda_wo",
2025-06-30 11:26:24 +08:00
"dtoOrVoId":"96eedc80-2c29-44ab-883f-031efdba43e8",
2025-06-22 16:58:06 +08:00
"description": "会议详情,包含会议室信息,以及其中的会议列表",
"fromEntity": "meeting",
"expandList": [
{
"field": "room_id",
"fieldName": "meeting_room",
"wo": {
"name": "meeting_room_wo",
"fromEntity": "meeting_room",
"description": "会议室信息"
}
}
],
"reverseExpandList": [
{
"field":"meeting_id",
"filedName": "meeting_agenda_list",
"wo": {
"fromEntity": "meeting_agenda",
"name": "meeting_agenda_wo",
"description": "会议议程信息"
}
}
]
}
}
2025-08-20 10:36:53 +08:00
```
2025-06-22 16:58:06 +08:00
#### **2.9 读方案 (ReadPlan)**
2025-08-20 10:44:36 +08:00
- **定义与用途:** 读方案描述如何从数据库获取DTO和VO列表数据, 提供三个能力:
- 根据查询条件返回符合条件的DTO或VO的id列表
- 根据查询条件返回符合条件的DTO或VO的id数量
- 根据字段过滤条件对DTO和VO的列表字段数据进行过滤
2025-08-19 18:51:41 +08:00
- **注意** 不派生自DTO的VO不能创建读方案
2025-08-20 10:44:36 +08:00
- **读方案能力边界**
- 支持sql的count、exists、left join、not、in、like、between、and、or、括号、order by、limit、offset
- 不支持group by、having语法, 不支持其他函数
- 1:N外键关系只能使用exists, 1:1外键关系可使用left join和exists
例如: 实体t和t1, t1有t_id外键指向t, 若为1:N关系, 则`select * from t where exists (select * from t1 where t.id=t1.id)` 合法,`select * from t left join t1 on t.id=t1.id` 非法
- exists只能选择后表字段, left join可使用前表和后表字段
例如: 实体t(id,name), 实体t1(id,name,t_id), `select * from t where exists (select * from t1 where t.id=t1.id and t.name like ?)` 非法,`select * from t left join t1 on t.id=t1.id and t1.name like ?` 合法
- 需求超出读方案能力时,必须使用自定义查询
- **特别注意** 根据主键获取单个或批量DTO/VO且不需过滤列表字段数据的需求, 必须使用DTO自动生成的预定义方法, 禁止使用读方案
- **关键配置:** 名称(小写字母+下划线, 不以read_plan结尾, 全局唯一)、返回结构(DTO/VO, 一个读方案只能返回一种类型)、查询条件自然语言描述、是否生成计数方法、排序字段、过滤字段及过滤条件
- **与RPC、代码的关系:** 返回DTO的读方案自动生成RPC方法, 参数为QTO, 返回值为DTO列表; 返回VO的读方案自动生成Java方法, 参数为QTO, 返回值为VO列表, 方法内部逻辑由TOCO完全实现
- **生成的读方案RPC使用:** 读方案RPC属于对应模块, 其他模块需先订阅该RPC, 用adapter调用; 当前模块直接调用对应Service
- **创建方式:** 先创建或选择一种DTO或VO作为返回值类型, 然后定义查询条件自然语言描述、分页方式、是否生成计数方法、排序字段等
- **排序** 支持两种方式:
- **默认排序**:指定默认排序字段(不需入参指定排序字段)
- **自定义排序**:指定排序字段(需入参指定排序字段),满足列表头动态排序需求
- **排序字段来源**: 只能来源自根WO(不包括列表WO属性), 以及根WO扩展出的非列表属性的WO字段。排序字段是从根节点到当前属性的路径, 例如: ADto包含BDto bDto, BDto有name属性, 排序字段路径为: bDto.name
- 提取需求中的查询信息,以输入的查询对象作为上下文,构建查询语句
- 如有列表属性过滤需求:提取需求中的过滤信息,针对可过滤字段(列表属性)分别创建过滤条件。过滤条件不能使用列表属性作为查询条件属性(不能使用contains语法)。属性字段是从根节点属性到当前属性的路径
- **查询语法和过滤语法**
- 基本语法格式:属性名 操作符 变量或常量,变量格式为#变量名 ;枚举类型常量用单引号包围,例如:'MALE'
- 数值、时间类型属性操作符:!=, ==, >, < , < =, >=, in, notIn, isNullOrNot
- 文本类型属性操作符: like, isNotNull, isNull, !=, ==, in, notIn, isNullOrNot
- isNullOrNot是三元操作符, 根据入参判断过滤某值是否为null。例如: gender isNullOrNot #genderIsNullOrNot , 若入参为true等价于where gender is null, 若为false等价于where gender is not null
- 对象属性或列表对象属性操作符: isNull, isNotNull, isNullOrNot。本对象不能直接用此操作符
- 对象属性可对其子属性进行上述查询
2025-07-02 15:07:32 +08:00
- 查询变量不能作为条件属性
2025-08-20 10:44:36 +08:00
- 查询条件间可用AND, OR连接
- 可插入括号()对条件分组
- 列表对象属性只能使用contains、isNull、isNotNull操作符: wo列表类型可用contains(子查询), 表示列表属性中需包含至少一个满足子查询条件的对象; 其他列表类型只能用isNull或isNotNull
- 通过and, or, not连接符拼装子查询完成查询。不要使用未提及的操作符号、连接符
- 查询条件中的入参可在运行时传入或不传入值, 不传入值表示该参数相关条件不起作用; 基于这种动态效果, 多条件联合查询可优先使用AND
- 使用点号(.)访问当前对象的单值对象类型子属性,可多个点号组合访问嵌套单值对象属性
- 查询条件中的属性必须是当前查询对象的属性或单值对象属性或单值对象子属性
2025-07-02 15:07:32 +08:00
- 禁止使用filter语法
- 禁止使用has语法
- 语法定义:使用 lezer 定义了如下语法
2025-08-14 11:07:23 +08:00
< code >
2025-06-29 17:24:56 +08:00
@top Program { expression? }
@skip { spaces | newline | LineComment }
@precedence {
member,
and @left ,
or @left
}
kw< term > { @specialize [@name ={term}]< identifier , term > }
boolean { @specialize [@name =Boolean]< identifier , " true " | " false " > }
@skip {} {
String[isolate] {
'"' (stringContentDouble | Escape)* ('"' | "\n") |
"'" (stringContentSingle | Escape)* ("'" | "\n")
2025-06-22 14:59:22 +08:00
}
2025-06-29 17:24:56 +08:00
}
commaSep< content > {
content ("," content)*
}
List {
"[" commaSep< Value > ~destructure "]"
}
2025-08-19 18:51:41 +08:00
Value { String | Number | List | boolean }
2025-06-29 17:24:56 +08:00
Field { identifier ~arrow }
Member { Field !member "." (Member | Field) }
Input { "#" identifier ~arrow }
expression {
TupleExpression | BinaryExpression | NotExpression | ParenthesizedExpression | ListExpression
}
TupleExpression { TwoTupleExpression | ThreeTupleExpression }
TwoTupleExpression { (Field | Member) TwoOperator }
ThreeTupleExpression { (Field | Member) ThreeOperator (Input | Value) }
ListExpression { (Field | Member) ListOperator ParenthesizedExpression }
BinaryExpression {
expression !and (kw< 'AND'> | kw< 'and'>) expression |
expression !or (kw< 'OR'> | kw< 'or'>) expression
}
NotExpression { (kw< 'NOT'> | kw< 'not'>) ParenthesizedExpression }
ParenthesizedExpression { "(" expression ")" }
@tokens {
spaces[@export ] { $[\u0009 \u000b\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]+ }
newline[@export ] { $[\r\n\u2028\u2029] }
identifierChar { @asciiLetter | $[_$\u{a1}-\u{10ffff}] }
word { identifierChar (identifierChar | @digit )* }
identifier { word }
hex { @digit | $[a-fA-F] }
stringContentSingle { ![\\\n']+ }
stringContentDouble { ![\\\n"]+ }
@precedence { spaces, newline, identifier }
Escape {
"\\" ("x" hex hex | "u" ("{" hex+ "}" | hex hex hex hex) | ![xu])
2025-06-22 14:59:22 +08:00
}
2025-06-29 17:24:56 +08:00
Number {
(@digit ("_" | @digit )* ("." ("_" | @digit )*)? | "." @digit ("_" | @digit )*)
(("e" | "E") ("+" | "-")? ("_" | @digit )+)? |
@digit ("_" | @digit )* "n" |
"0x" (hex | "_")+ "n"? |
"0b" $[01_]+ "n"? |
"0o" $[0-7_]+ "n"?
2025-06-22 14:59:22 +08:00
}
2025-08-19 18:51:41 +08:00
2025-06-29 17:24:56 +08:00
@precedence { Number "." }
ThreeOperator { "in" | "notIn" | "!=" | "==" | ">" | ">=" | "< " | "< =" | "isNullOrNot" | "like" | "has" }
TwoOperator { "isNull" | "isNotNull" }
ListOperator { "contains" | "filter" }
LineComment[isolate] { "//" ![\n]* }
"(" ")"
"[" "]"
"."
}
@detectDelim
2025-08-14 11:07:23 +08:00
< / code >
2025-06-18 11:10:27 +08:00
- **读方案设计元素的表达**
2025-07-02 15:07:32 +08:00
- 以json格式表达, json schema 定义如下
2025-06-18 11:10:27 +08:00
```json
{
2025-06-24 15:41:46 +08:00
"type":"object","description":"读方案定义",
2025-06-18 11:10:27 +08:00
"properties":{
"qto": {
"type":"object",
"properties":{
2025-06-24 15:41:46 +08:00
"name": {"type":"string","description":"读方案名称, 表达了查询的意图; 英语描述, 单词之间使用下划线分割, 长度限制在32个字符内"},
"uuid": {"type":"string","description":"读服务设计元素的uuid, 创建时不传入, 在更新的时候必须传入"},
"woId": {"type":"string","description":"查询对象的uuid, 作为查询服务的上下文, 创建的时候必须指定, 更新的时候不传递"},
"description": {"type":"string","description":"描述读方案的功能, 长度限制在256个字符内"},
"generateCountApi": {"type":"boolean", "description":"是否需要生成计数接口"},
"supportPaginate": {"type":"boolean", "description":"是否需要分页"},
2025-06-18 11:10:27 +08:00
"supportUnPage":{"type":"boolean", "description":"如果不需要分页, 一次性返回部数据, 则返回true"},
2025-06-24 15:41:46 +08:00
"supportWaterfall":{"type":"boolean","description":"是否需要瀑布流"},
"query":{"type":"string","description":"查询语句,符合前述语法定义"},
2025-06-29 17:24:56 +08:00
"voOrDtoId":{"type":"string","description":"返回数据对象(VO或DTO)的uuid,创建的时候必须指定,更新的时候不传递"},
"outOrder":{"type":"array","description":"入参排序方式定义",
"items":{"type":"object","description":"定义字段排序方式","required":["fieldPath", "direction"],
"properties":{"fieldPath": {"type":"string","description":"字段名路径"},"direction": {"type":"string","description":"排序方向, 可以是ASC(升序)或者DESC(降序)"}}}
},
"defaultOrder": {"type":"array","description":"默认排序方式定义",
"items": {"type":"object","description":"定义字段排序方式","required":["fieldPath", "direction"],
"properties":{"fieldPath": {"type":"string","description":"字段名"},"direction": {"type":"string","description":"排序方向, 可以是ASC(升序)或者DESC(降序)"}}}
2025-08-19 18:51:41 +08:00
},
2025-06-29 17:24:56 +08:00
"filters": {"type": "array", "description":"过滤条件定义",
"items": {"type":"object","description":"定义过滤条件","required":["fieldPath", "filter"],
"properties":{
"fieldPath": {"type":"string","description":"需要过滤的列表字段路径"},
"filter": {"type":"object","description":"过滤语法,符合前述的语法定义,不能使用contains语法"}
2025-08-19 18:51:41 +08:00
}
}
2025-06-18 11:10:27 +08:00
},
"required":["name","description","query"]
}
},
"required":["qto"]
2025-06-29 17:24:56 +08:00
}
2025-06-18 11:10:27 +08:00
}
```
- **举例**
2025-07-02 15:07:32 +08:00
- 上下文
2025-08-14 10:45:02 +08:00
< code >
2025-08-19 18:51:41 +08:00
public class meeting_room {
2025-08-20 10:44:36 +08:00
storey storey; // 楼层对象
2025-07-02 15:07:32 +08:00
String name; // 名称
room_type_enum room_type; // 会议室类型
2025-08-20 10:44:36 +08:00
List< equipment_enum > equipment; // 设备列表
2025-07-02 15:07:32 +08:00
Long id; // 主键
Long seat_number; // 座位数
String description; // 说明
Boolean enable_indicator; // 是否启用
String input_code; // 输入码
Long storey_id; // 楼层id
Long contact_staff_id; // 联系员工ID
2025-08-20 10:44:36 +08:00
List< meeting > meeting; // 会议列表,可添加过滤条件
class storey { // 楼层
location location; // 位置对象
String name; // 楼层名
2025-07-02 15:07:32 +08:00
Long id; // 主键
Long sort_number; // 排序号
Long location_id; // 位置id
}
2025-08-19 18:51:41 +08:00
class meeting {
2025-07-02 15:07:32 +08:00
Long id; // 主键
String title; // 标题
Date start_time; // 开始时间
Date end_time; // 结束时间
Long room_id; // 会议室id
String description; // 描述
Long create_user_id; // 创建人id
}
2025-08-20 10:44:36 +08:00
class location { // 位置,如楼栋
2025-07-02 15:07:32 +08:00
String name; // 名称
Long id; // 主键
String description; // 描述
Long sort_number; // 排序号
}
2025-08-20 10:44:36 +08:00
enum room_type_enum {
2025-08-19 18:51:41 +08:00
SMALL, //小会议室
MEDIUM, //中会议室
2025-07-02 15:07:32 +08:00
LARGE
}
2025-08-20 10:44:36 +08:00
enum equipment_enum {
2025-08-19 18:51:41 +08:00
TV, //电视
WHITEBOARD, //白板
PROJECTOR, //投影仪
2025-07-02 15:07:32 +08:00
VIDEO
}
2025-06-18 11:10:27 +08:00
}
2025-08-14 10:45:02 +08:00
< / code >
2025-07-02 15:07:32 +08:00
- 用户需求
2025-08-20 10:44:36 +08:00
获取一段时间未使用的会议室(包含当前会议已选择的会议室),根据会议标题过滤会议信息,分页返回。
- 读方案定义
2025-08-14 11:07:23 +08:00
< code >
2025-07-02 15:07:32 +08:00
{
"name": "get_unused_meeting_room_list",
"description": "获取未使用的会议室(包含当前会议已选择的会议室)",
"woId":"759bedd4-4540-4de6-b65d-d44912fb0991",
"dtoOrVoId":"e50c02c3-f336-4dc6-88e1-24ffe3dea1e2",
"generateCountApi": true,
"supportPaginate": true,
"supportUnPage": false,
"supportWaterfall": false,
"query": "enable_indicator == true AND ( id == #idIs OR meeting isNull OR NOT ( meeting contains ( start_time < = #meetingEndTime AND end_time >= #meetingStartTime ) ) )",
"filters": [
{
"fieldPath": "meeting",
"filter": "title like #meetingTitleLike "
}
]
2025-06-18 11:10:27 +08:00
}
2025-08-14 11:07:23 +08:00
< / code >
2025-06-22 14:59:22 +08:00
- **代码产物和修改建议**
2025-08-20 10:44:36 +08:00
- **QTO查询参数对象**
* **生成产物:** Service层生成Java类
2025-07-02 15:07:32 +08:00
* **命名规则:** 类名以Qto结尾(${ReadPlanName}Qto)
2025-08-20 10:44:36 +08:00
* **类路径:** `**.persist.qto` 包下
* **职责:** 读方案的查询参数结构, 可作为API参数, 直接透传给RPC调用
* **唯一标识符位置:** 类注解@AutoGenerated中指定 , uuid规则: ${ReadPlan的uuid}|QTO|DEFINITION
* **内部结构:** QTO包含分页相关字段:
2025-08-14 11:07:23 +08:00
< code >
2025-08-20 10:44:36 +08:00
private String scrollId; //瀑布流游标
private Integer size; //每页记录数
private Integer from; //开始记录位置
2025-08-14 11:07:23 +08:00
< / code >
2025-07-02 15:07:32 +08:00
- **Dao**
2025-08-20 10:44:36 +08:00
- **生成产物:** Dao层生成Java类
2025-07-02 15:07:32 +08:00
- **命名规则:** 类名以Dao结尾(${ReadPlanName}Dao)
2025-08-20 10:44:36 +08:00
- **类路径:** `**.persist.mapper` 包下
- **职责:** 读方案对应的数据库查询方法
- **唯一标识符位置:** 类注解@AutoGenerated中指定 , uuid规则: ${ReadPlan的uuid}|QTO|DAO
2025-07-02 15:07:32 +08:00
- **QueryExecutor**
2025-08-20 10:44:36 +08:00
- **生成产物** 返回**VO**的查询方案, 在controller层生成Java类, 包含独立函数:
- 分页查询: **Paged函数, 返回VSQueryResult< XxxVo >
- 不分页全量: query**函数, 返回List< XxxVo >
- 瀑布流: **Waterfall函数, 返回VSQueryResult< XxxVo >
- 查询数量: **Count函数, 返回Integer
2025-08-05 12:05:20 +08:00
- **命名规则:** 类名以QueryExecutor结尾(${VoName}QueryExecutor)
2025-08-20 10:44:36 +08:00
- **类路径:** `**.entrance.web.query.executor` 包下
- **职责:** 提供VO查询入口, 将QtoService返回的id数据转化为目标**VO**
2025-07-02 15:07:32 +08:00
- **QueryService**
2025-08-20 10:44:36 +08:00
- **生成产物** 返回**DTO**的查询方案, 在service层生成Java类, 包含独立函数:
- 分页查询: **Paged函数, 返回VSQueryResult< XxxDto >
- 不分页全量: query**函数, 返回List< XxxDto >
- 瀑布流: **Waterfall函数, 返回VSQueryResult< XxxDto >
- 查询数量: **Count函数, 返回Integer
2025-08-05 12:05:20 +08:00
- **命名规则:** 类名以QueryService结尾(${DtoName}QueryService)
2025-08-20 10:44:36 +08:00
- **类路径:** `**.service.index.entity` 包下
- **职责:** 提供DTO查询入口, 将QtoService返回的id数据转化为目标**DTO**,或返回符合条件的记录数量
- **VSQueryResult说明:**
- VSQueryResult是固定类, 全路径为com.vs.es.query.VSQueryResult, 存在于jar中, 直接使用, 禁止创建新的VSQueryResult类!
- VSQueryResult代码结构:
2025-08-14 11:07:23 +08:00
< code >
2025-08-08 12:10:23 +08:00
package com.vs.es.query;
2025-08-05 14:05:45 +08:00
2025-08-08 12:10:23 +08:00
import lombok.Data;
2025-08-05 14:05:45 +08:00
2025-08-08 12:10:23 +08:00
import java.util.List;
2025-08-05 14:05:45 +08:00
2025-08-08 12:10:23 +08:00
@Data
public class VSQueryResult< T > { //T为DTO或VO类型
private int count;
private List< T > result;
2025-08-05 14:05:45 +08:00
2025-08-20 10:44:36 +08:00
private int from; //页码分页起始条数
private int size; //每页记录数量
2025-08-08 12:10:23 +08:00
private String scrollId; //瀑布流查询游标
2025-08-20 10:44:36 +08:00
private boolean hasMore; //瀑布流查询是否有下一页
2025-08-08 12:10:23 +08:00
}
2025-08-14 11:07:23 +08:00
< / code >
2025-07-02 15:07:32 +08:00
- **例子:**
2025-08-20 10:44:36 +08:00
* 根据用户名称查询用户列表返回UserDTO, 生成UserNameQto、UserNameQtoService、UserNameQtoDao、UserNameQueryService; UserNameQueryService调用UserNameQtoService, UserNameQtoService调用UserNameQtoDao
- **修改建议:**
- 如有结果数据二次处理, 建议在QueryService和QueryExecutor中扩展代码, 不建议修改QTO文件
2025-08-20 11:02:31 +08:00
2025-06-22 16:58:06 +08:00
#### **2.10 查询传输对象(QTO)**
2025-08-20 11:02:31 +08:00
- **定义与用途:** QTO是读方案的查询参数结构。每个读方案对应一个QTO。调用方使用QTO结构传入查询字段值, 完成数据库查询
- **如何创建/生成:** 创建读方案后, TOCO自动生成对应的QTO。无需单独创建
- **关键配置:** 名称(${ReadPlanNameQto}, 驼峰命名) , 查询字段列表( 如idIs, nameLike, schoolNameLike等)
- **与API的关系:** QTO可作为API参数。API接收参数后直接透传给内部RPC调用。QTO只用于读操作参数, **禁止用于写参数结构**
2025-08-20 10:36:53 +08:00
2025-07-28 18:26:03 +08:00
#### **2.11 写方案 (WritePlan, 单聚合操作)**
2025-08-20 11:02:31 +08:00
- **定义与用途:** 写方案是数据库写操作的唯一方式, 每个写方案只能变更一个聚合的数据。写方案可以一次操作聚合内的多张表。例如: location(父对象/聚合根)和storey(子对象)是1:N关系, 可创建写方案同时更新location信息和操作storey列表(新增/删除/修改); 也可创建写方案单独更新单个storey对象。
- **关键配置:** 名称(小写+下划线, 不以write_plan结尾, 全局唯一),操作的聚合,聚合内的实体和字段,每个实体的操作类型:
- CREATE: 创建单个实体
- UPDATE: 更新单个实体
- DELETE: 删除单个实体
- CREATE_ON_DUPLICATE_UPDATE: 创建或更新单个实体
- FULL_MERGE: 批量更新列表数据, 执行CREATE_ON_DUPLICATE_UPDATE操作, 删除不在传入列表中的旧数据
- PARTIAL_MERGE: 批量更新列表数据, 执行CREATE_ON_DUPLICATE_UPDATE操作
- **与RPC关系: ** 每个写方案自动生成一个RPC方法, 参数为对应BTO, 返回值为聚合根实体主键值, 只实现当前聚合的数据库操作。
- **生成RPC使用: ** 写方案RPC属于所属模块, 其他模块需先订阅该RPC, 用adapter调用; 当前模块直接调用对应Service。
- **与聚合关系:** 每个聚合可定义多个写方案,但每个写方案只能操作一个聚合内的表。
- **创建方式:** 先选定聚合和要操作的实体,再确定每个实体的操作类型
- 提取需求中的更新需求,根据上下文构建写方案
- 输入备选聚合对象定义,每个聚合定义多个实体的嵌套关系
- 明确聚合内实体的父子层级关系
- 提取需求相关的变更名称和描述
- 选择最合适的聚合对象(最多只能选一个)
- 按对象粒度选出需变更对象
- 选定变更对象的字段(每个对象至少选一个字段,无法确定时默认选全部,字段必须明确指定名称)
- 为每个对象设置操作类型
- 列表属性可批量操作:
- 批量更新列表(FULL_MERGE/PARTIAL_MERGE):需包含父对象
- 批量删除列表: 需包含父对象, 父对象设为UPDATE
- 批量创建列表: 需包含父对象, 父对象设为UPDATE或CREATE
- 示例: meeting对象有List< meeting_agenda > agendaList, 批量创建agenda需同时设置meeting和meeting_agenda对象, meeting_agenda设为CREATE; 只设置meeting_agenda的CREATE则一次只能创建一条数据
- FULL_MERGE和PARTIAL_MERGE只用于列表属性, 单值属性用CREATE_ON_DUPLICATE_UPDATE
- UPDATE/DELETE/CREATE_ON_DUPLICATE_UPDATE/FULL_MERGE/PARTIAL_MERGE操作必须指定一个唯一键(包括主键), 用于确定数据记录, 主键字段也要包含在fields中
- 唯一键:实体中的唯一索引,由一个或多个字段组成
- 选定对象层级不能跳跃,发生不连续时去掉不连续的叶子对象
- 父对象和子对象操作类型限制:
- 父对象DELETE: 子对象不能选择任何操作
- 父对象CREATE: 子对象只能CREATE
- 父对象CREATE_ON_DUPLICATE_UPDATE: 子对象只能CREATE/CREATE_ON_DUPLICATE_UPDATE/FULL_MERGE/PARTIAL_MERGE
- 父对象FULL_MERGE: 子对象只能FULL_MERGE/PARTIAL_MERGE/CREATE
- 增量更新字段: Long/BigDecimal/Float/Integer类型字段可设为增量更新, 设置在incrFields中, 配合UPDATE/CREATE_ON_DUPLICATE_UPDATE/PARTIAL_MERGE/FULL_MERGE使用。例如: A实体count字段设为增量更新, 效果是A.count = A.count + ?,传入负值可减值
- **写方案设计元素表达:**
2025-07-02 15:07:32 +08:00
```json
{
"type":"object",
"description":"写方案定义",
"properties":{
2025-08-20 11:02:31 +08:00
"uuid":{"type":"string","description":"写方案唯一标识,创建时不传,更新时必传"},
"name":{"type":"string","description":"写方案名称,英文+下划线, 32字符内"},
"description":{"type":"string","description":"写方案描述, 256字符内"},
"bo": {"type":"string","description":"聚合根名称"},
2025-07-02 15:07:32 +08:00
"operations": {
2025-08-20 11:02:31 +08:00
"type":"array","description":"实体操作定义,包括操作类型和字段",
2025-07-02 15:07:32 +08:00
"items":{
2025-08-20 11:02:31 +08:00
"type": "object", "description":"具体实体操作定义",
2025-07-02 15:07:32 +08:00
"properties":{
2025-08-20 11:02:31 +08:00
"bo":{"type":"string","description":"操作的实体"},
"action":{"type":"string","description":"实体操作类型"},
2025-07-02 15:07:32 +08:00
"uniqueKey": {
2025-08-19 18:51:41 +08:00
"type":"array",
2025-08-20 11:02:31 +08:00
"description":"唯一键字段列表,用于确定数据记录",
"items":{"type":"string","description":"字段名称"}
2025-07-02 15:07:32 +08:00
},
"fields":{
"type":"array",
"items":{"type":"string","description":"字段名称, 必须来自bo对象"},
2025-08-20 11:02:31 +08:00
"description":"操作字段列表"
2025-07-02 15:07:32 +08:00
},
"incrFields":{
"type":"array",
"items":{ "type":"string", "description":"增量字段名称"},
2025-08-20 11:02:31 +08:00
"description":"增量操作字段列表"
2025-07-02 15:07:32 +08:00
},
"required":["bo","action","fields"]
}
}
}
},
"required":["name","description","operations","bo"]
2025-06-18 11:10:27 +08:00
}
2025-07-02 15:07:32 +08:00
```
2025-08-20 11:02:31 +08:00
- **示例:**
- **上下文:**
```java
2025-06-18 11:10:27 +08:00
public class meeting {//会议聚合
//唯一键:(id)、(meeting_name)
2025-08-20 11:02:31 +08:00
Long id; //主键
2025-06-18 11:10:27 +08:00
String meeting_name; //会议名称
Date start_time; //开始时间
Date end_time;//结束时间
List< meeting_agenda > meetingAgendaList;
class meeting_agenda {
Long id;//主键
Date start_time;//开始时间
Date end_time;//结束时间
String title;//议程名称
List< agenda_vs_user > agendaVsUserList;
}
class agenda_vs_user {
Long id; //主键
Long user_id; //用户id
Long agenda_id; //议程id
Date enroll_time; //注册时间
Long meeting_id;//会议id
}
}
public class user {//用户聚合
//唯一键:(id)、(name,gender)
Long id;//主键
String name;//名字
GenderEnum gender;//性别
String nickname;//昵称
Long friend_count;//好友数量
}
2025-08-20 11:02:31 +08:00
```
- **需求:** 创建会议
- **对应写方案定义:**
```json
2025-06-18 11:10:27 +08:00
{
"name": "create_meeting",
"description": "创建会议",
"bo": "meeting",
"operations": [
{
"bo": "meeting",
"action": "CREATE",
"fields": [
"meeting_name",
"start_time",
"end_time"
]
}
]
}
2025-08-20 11:02:31 +08:00
```
- **代码产物和修改建议:**
- **BOService: **
* **生成产物:** 在聚合服务BOService中生成一个函数
* **函数命名:** 与写方案同名
* **职责:** 转换入参, 调用BaseBOService函数完成聚合对象操作, 实现数据库写操作
* **返回值:** 聚合根实体记录主键值
* **类路径:** `**.service`
* **唯一标识:** 函数注解@AutoGenerated中指定 , uuid规则: ${WritePlan的uuid}
- **BaseBOService: **
- **生成产物:** 在聚合BaseBOService中生成系列函数, 根据入参完成聚合对象变更
- **职责:** 为每个实体生成增删改函数,根据参数结构和聚合结构构建嵌套调用逻辑,完成聚合对象变更,记录并返回变更情况
- **类路径:** `**.service.base`
- **禁止:** 修改该类
- **BTO (业务变更传输对象): **
* **生成产物:** Java类, 以内部类方式表示层级结构, 注意BTO只能由写方案生成, 不能凭空创建
* **命名规则:** 以Bto结尾(${WritePlanName}Bto, 驼峰命名)
* **禁止:** 修改该类
* **类路径:** `**.service.bto` 包下
* **职责:** 写方案的参数结构, 每个写方案对应一个BTO, 调用方按BTO结构向写方案RPC方法传入实体字段值, 完成数据库变更
* **唯一标识:** 类注解@AutoGenerated中指定 , uuid规则: ${WritePlan的uuid}|BTO|DEFINITION
- **示例:**
- 创建用户及设置的写方案create_user_and_setting, 生成CreateUserAndSettingBto, 在UserBOService中生成函数createUserAndSetting, 该函数调用BaseUserBOService中生成的createUserAndSetting, 其中BaseUserBOService还生成createUser和createSetting函数, 一起完成用户创建和设置创建逻辑。
2025-06-22 14:59:22 +08:00
- **修改建议:**
2025-08-20 11:02:31 +08:00
- 不能修改BaseBOService函数, 不建议修改BTO文件。建议在BOService中扩展代码, 处理可能被复用的前后逻辑, 如数据库前后值对比、常被复用的校验逻辑(业务不变性校验除外)、需要在一个事务内执行的其他写操作等。
- 父类函数返回BoResult类, 记录各Bto和Bo实例对应关系及Bto实例操作结果, 可通过以下接口获取。例如: 创建用户的写方案中, BoService入参为CreateUserBto, 用户id由数据库生成, 需要返回创建用户id时, 通过boResult.getAddedResult(createUserBto).getBo().getId()返回新建用户id
```java
2025-07-23 09:58:30 +08:00
/**
* 获取更新成功的bto结果
*/
public UpdatedBto getUpdatedResult(final Object bto)
/**
2025-08-20 11:02:31 +08:00
* 获取成功插入的Bto结果
2025-07-23 09:58:30 +08:00
*/
2025-08-19 18:51:41 +08:00
public AddedBto getAddedResult(final Object btoObj)
2025-07-23 09:58:30 +08:00
/**
* 获取Bto对应的删除结果
*/
2025-08-19 18:51:41 +08:00
public DeletedBto getDeletedResult(final Object btoObj)
2025-08-20 11:02:31 +08:00
```
```java
2025-07-23 09:58:30 +08:00
public class UpdatedBto< Bto , Entity , BO > {
2025-08-20 11:02:31 +08:00
//Bto入参
private Bto bto;
//Bto对应的Entity, 前项
private Entity entity;
//bo, 后项
private BO bo;
}
```
```java
//记录删除记录情况
2025-07-23 09:58:30 +08:00
@Setter
@Getter
public class DeletedBto< Bto , Entity > {
2025-08-20 11:02:31 +08:00
//Bto入参
private Bto bto;
//Bto对应的Entity, 前项
private Entity entity;
2025-08-19 18:51:41 +08:00
}
2025-08-20 11:02:31 +08:00
```
```java
2025-07-23 09:58:30 +08:00
//记录Bto创建的前后值
@Setter
@Getter
public class AddedBto< Bto , BO > {
2025-08-20 11:02:31 +08:00
//Bto入参
private Bto bto;
//bo后项
private BO bo;
2025-08-19 18:51:41 +08:00
}
2025-08-20 11:02:31 +08:00
```
2025-06-22 14:59:22 +08:00
2025-08-20 11:02:31 +08:00
#### **2.12 业务变更传输对象(BTO)**
- **定义与用途:** 在TOCO中, BTO为写方案自动生成的参数结构, 每个写方案会生成一个BTO。BTO为写方案选定的操作实体根据关系形成的树形集合, 最外层为聚合根。写方案调用方按照BTO的结构向写方案生成的RPC方法传入需要操作的实体字段值, 完成对数据库的写操作
- **如何创建/生成:** BTO只能由写方案自动创建, 不能单独新建任何BTO。在创建写方案后, TOCO会自动生成一个BTO作为该写方案传入的参数结构。
- **关键配置:** 名称(${WritePlanName}Bto, 驼峰展示) , 嵌套的树形实体和字段列表, BTO内部的字段全部都来自Entity。以下为一个示例:
< code >
class CreateUserBto { //对应实体user
Long id; //来自于user.id
String name; //来自于user.name
List< PictureBto > pictureList;
class PictureBto { //对应实体picture
String url; //来自于picture.url
}
}
< / code >
- **与API的关系:** BTO通常可作为API的参数, API接收到参数后可直接透传给内部的RPC进行调用, 注意BTO只用作写操作的参数, **禁止用作查询参数结构**
- **复杂场景处理:** 对于涉及多个写方案的复杂API, 可以:
a. 如果接口有一个主要的写场景, 则创建一个主要的写方案, 让其自动生成的BTO作为API参数, 然后再增加其他基本类型的参数或EO
b. 使用基本类型参数或EO作为参数, 在接口执行流程中将基本类型参数转换为各个写方案自动生成的BTO, 再调用多个写方案
c. 如果流程过于复杂, 优先考虑使用基本类型参数或EO
#### **2.13 服务层方法 (RPC)**
- **定义与用途:** 在TOCO中, RPC为服务层的方法。RPC按照可见性可以分为两种, 一种是公开RPC, 可以被其他模块订阅, 订阅后可以通过RPC适配器进行调用; 另一种是非公开RPC, 只能被当前模块调用。非公开RPC可以被公开, 从而被其他模块订阅并调用
- **如何创建/生成:** RPC有4种创建方式: a.DTO创建后会自动创建RPC, RPC的公开性与DTO的公开性保持一致; b.返回DTO的读方案会根据分页情况、以及是否生成计数函数的配置自动生成非公开的RPC c.写方案创建后会自动生成非公开的RPC, 返回值为操作的聚合根实体记录的主键字段的值 d.如果上述三种RPC无法满足需求, 则可以通过TOCO创建自定义RPC完整指定功能, 需指定具体的参数和返回值以及公开性等。
- **优先复用:** 当用户需要创建一个RPC时, 如果用户有明确要求创建的方式, 则按照用户的要求来创建。如果没有明确要求, 则通常先判断是否可以通过创建读方案、写方案、DTO来使TOCO自动创建出对应的RPC, 最后再考虑通过TOCO创建自定义RPC
- **自定义RPC和代码中手写方法的关系: ** 二者都可以通过手动的方式实现一个服务层的方法, 应用场景的区别在于: 如果一个方法需要被其他模块订阅, 则通常使用自定义RPC; 如果一个方法只是某个API私有调用, 不需要给外部模块开放, 则可以使用代码手写方法
- **关键配置:** 类名(驼峰, 首字母大写, 以Service结尾)、是否公开、方法名(驼峰,首字母小写)、请求参数、返回值。注意如果一个RPC是分页查询且参数为Qto类型, 则Qto中已经包含了分页所需的from、size、scrollId等属性, 无需额外为RPC增加类似参数。
- **TOCO中RPC的存储:** 注意RPC在TOCO中只存储了方法签名, 不存储内部的执行流程逻辑, 如果需要了解其内部的实现逻辑, 则需要通过阅读RPC对应的代码。
- **参数类型:** RPC的参数**只能**为QTO、BTO、Enum、基本类型, 可为单值或列表。注意如果是对象类型, 则优先使用QTO、BTO作为参数, **禁止使用**VO和自定义结构如Object
- **返回值类型:** RPC的返回值**只能**为DTO、Enum、基本类型,可为单值或列表,**禁止使用**VO、QTO、BTO、自定义结构如Object作为返回值。注意如果是对象类型, 则优先使用DTO作为返回值
- **TOCO中json结构描述:** 在TOCO中, DTO使用一个json结构表示, 示例如下:
```json
{
"methodName": "saveUsers",
"className": "UserSaveService",
"requestParams":[
{
"name": "saveUserBtoList",
"description": "批量保存用户参数",
"type": "List",
"innerType": "Bto",
"innerUuid": "dbvvc4d4-0063-442f-abd7-vrfded656988"
}
],
"response": {
"type": "Boolean"
}
}
```
结构中一些关键字段描述如下:
requestParams为请求参数列表, response为返回结构, requestParams中每个参数和response的结构相同, 其中: name为参数名;type为参数类型, 参数类型取值范围为Boolean,String,Integer,Long,Float,Double,BigDecimal,Date,ByteArray,Enum,Eo,Dto,Qto,Bto,List,PageResult,Void, 其中参数不能为Void和PageResult, 如果不需要返回值则type设置为Void, 如果返回值为分页查询的结果则type设置为PageResult, 且innerType必为Dto, 对应代码中的VSQueryResult< XxxDto > ; description为描述; typeUuid为参数对应类结构的UUID, 当type为Enum、Eo、Dto时传入该对象的uuid, 当type为Qto时传入对应读方案的uuid、当type为Bto时传入对应写方案的uuid; innerType为List内部类型, 当type为List或PageResult时包含该字段; innerUuid为List内部类结构的UUID, 当type为List或PageResult且innerType为Enum、Eo、Dto时传入该对象的uuid, 当innerType为Qto时传入对应读方案的uuid、当innerType为Bto时传入对应写方案的uuid。
- **生成代码:** RPC会在service层中生成类文件及实现函数, 包含DTO自动生成的RPC如UserDtoService.getById(主键为id)或StaffBaseDtoService.getByStaffId(主键为staff_id)、读写方案自动生成的RPC如UserDtoQueryService.queryByListQto、UserBOService.createUser、自定义RPC如UserCustomService.customMethod。特别注意公开的RPC才可被其他模块使用, RPC被订阅后会生成RpcAdapter适配器, 其他模块通过RpcAdapter才可调用该方法。如Order模块订阅了User模块的UserDtoService.getById, 则会在Order模块中生成UserDtoServiceInOrderRpcAdapter.getById方法, Order模块中的代码必须通过@Resource private UserDtoServiceInOrderRpcAdapter userDtoServiceInOrderRpcAdapter;注入适配器后才可进行方法调用。这里**必须要注意**:变量的命名必须是类名的首字母小写,禁止使用其他变量名。
- **修改建议:** 建议修改RPC方法, 不建议修改RPC方法签名、适配器中的内容
#### **2.14 应用程序接口 (API)**
- **定义与用途:** 在TOCO中, API用于定义对外暴露的HTTP接口
- **如何创建/生成:** API一般为通过TOCO创建, 需指定具体的参数和返回值等。
- **TOCO中API的存储:** 注意API在TOCO中只存储了其URI和方法签名, 不存储内部的执行流程逻辑, 如果需要了解其内部的实现逻辑, 则需要通过阅读API对应的代码。
- **关键配置:** uri(加粗展示,一般为/api/${moduleName}/xxx, 如/api/user/create,全局唯一。如果用户有特殊命名规则的话以用户要求为准)、类名(以Controller结尾)、方法名(驼峰,首字母小写)、请求参数、返回值。注意如果一个API是分页查询且参数为Qto类型, 则Qto中已经包含了分页所需的from、size、scrollId等属性, 无需额外为API增加类似参数。
- **参数类型:** API的参数**只能**为读方案自动生成的QTO、写方案自动生成的BTO、EO、Enum、基本类型, 可为单值或列表。注意如果是对象类型, 则优先使用读方案自动生成的QTO、写方案自动生成的BTO作为参数。**禁止使用**DTO和自定义结构如Object作为参数
- **返回值类型:** TOCO的API运行在自己的Java脚手架中, 脚手架会自动对API的返回值做一层对象包装( code、message、data) 。所以在TOCO中, API的返回值无需考虑返回码和错误信息, 只需考虑返回的数据本身。TOCO中API的返回值**只能**为VO、Enum、基本类型,可为单值或列表,**禁止使用**DTO、QTO、BTO、自定义结构如Object作为返回值。注意如果是对象类型, 则优先使用VO作为返回值。
- **TOCO中json结构描述:** 在TOCO中, API使用一个json结构表示, 示例如下:
```json
{
"methodName": "getUserMeetingList",
"className": "MeetingQueryController",
"usageScenario": "xxx",
"requestParams":[
{
"name": "getMeetingListByUserId74Qto",
"description": "查询参数",
"type": "Qto",
"uuid": "6351a4d4-0063-442f-abd7-a2df6d656988"
},
{
"name": "test",
"description": "是否为测试请求",
"type": "Boolean"
}
],
"response": {
"type": "List",
"innerType": "Vo",
"innerUuid": "1d58352b-5333-4509-aec2-1abc3fac9122"
}
}
```
结构中一些关键字段描述如下:
requestParams为请求参数列表, response为返回结构, requestParams中每个参数和response的结构相同, 其中: name为参数名;type为参数类型, 参数类型取值范围为Boolean,String,Integer,Long,Float,Double,BigDecimal,Date,ByteArray,Enum,Eo,List,PageResult,Vo,Qto,Bto,Void, 其中参数不能为Void和PageResult, 如果不需要返回值则type设置为Void, 如果返回值为分页查询的结果则type设置为PageResult, 且innerType必为Vo, 对应代码中的VSQueryResult< XxxVo > ; description为描述; typeUuid为参数对应类结构的UUID, 当type为Enum、Eo、Vo时传入该对象的uuid, 当type为Qto时传入对应读方案的uuid、当type为Bto时传入对应写方案的uuid; innerType为List内部类型, 当type为List时包含该字段; innerUuid为List或PageResult内部类结构的UUID, 当type为List或PageResult且innerType为Enum、Eo、Vo时传入该对象的uuid, 当innerType为Qto时传入对应读方案的uuid、当innerType为Bto时传入对应写方案的uuid。
- **代码产物和修改建议**
- **生成代码:** API会在entrance层生成Controller以及对应的API方法
- **修改建议:** 建议修改API方法的实现内容, 禁止直接修改API方法签名、URI, **注意**: 如需修改API定义( 名称、出入参数、URL) 需要通过修改API设计元素实现
#### **2.15 流程服务( FunctionFlow)**
- **定义与用途:** TOCO针对复杂业务拆解, 定义了流程服务, 把一个复杂的业务过程, 根据业务逻辑的内聚性, 合并逻辑功能, 把流程分解成流程节点, 最终构造出一个类似工作流的逻辑流程; 最终实现复杂业务流程分解, 提升代码的可维护性。TOCO内嵌了流程引擎, 在Function_Flow生成代码后, 可以在流程引擎中执行
- **何时使用:**
- 如果一个API/RPC中涉及的写服务超过3个, 则**必须**使用流程服务
- 当用户要求使用流程服务
- **节点的封装:**
- 因为TOCO是一个面向数据处理的系统, 所以数据内聚为首要考虑因素; 一个节点通常围绕一个核心写服务, 包括取数的读服务, 为写服务的入参进行数据处理和转换, 以及该写服务完成后的一些附属功能, 除了条件节点,入参校验,最终数据返回节点,每个节点至少包括一个写服务。
- **关键配置:** 名称(小写字母+下划线),拆解复杂业务逻辑,如果业务流程比较简单,则不需要使用流程服务
- **如何创建/生成:**
- 流程不是逻辑的伪代码,不需要表达全部逻辑细节,相关有内聚性,相似性的功能逻辑需要被封装到一个流程节点内;例如用户注册:创建用户需要,需要创建账号信息,需要创建用户信息,并且发送一个通知消息,这几个功能都属于创建用户相关信息,而且没有逻辑分叉,因此可以内聚在一个逻辑节点内。
- 流程节点之间的参数和返回值的传递通过一个统一的上下文Context传递( 在写代码的时候按需修改Context文件, 添加所需的字段)
- 定义了JSON结构, 用于表达逻辑流。流程节点分为 “顺序节点”、“条件节点”,“选择节点”,"开始节点“ ,节点之间通过有向边连接,表示逻辑的执行方向;从开始节点开始,可达各个节点。
- 节点的边的定义如下: 顺序节点可以有多条入边, 可以有多条出边( 一个出边表示下一个执行的节点, 多条出边表示几个分支并发执行) ; 条件节点可由多条入边, 可以有2条出边, 表示条件为True和False两种逻辑分支; 选择节点可以有多条入边, 可以有多条出边, 每条出边表示一种选择路径; 开始节点只能有一条出边, 不能有入边;
- ”条件节点“只封装条件判断逻辑, 返回TRUE或者FALSE; ”循环节点“只封装条件判断逻辑, 返回TRUE或者FALSE; ”选择节点“只封装分支选择逻辑, 返回下游分支节点的名称;
- 终止流程:直接抛出异常
- 一般校验逻辑放在一个“顺序节点"中,如果校验失败,以异常的方式抛出
- 节点可以复用,同样的功能可以抽取出来封装到一个节点中
- **流程服务设计元素的表达:**
- 以Json表达, Json Schema 定义如下:
```json
{
"type": "object","description": "流程服务设计元素", "required": [ "description","name","nodes","edges"],
"properties":
{
"name": { "type": "string", "description": "流程名称, 英语描述, 单词之间使用下划线分割, 总称不超过32个字符" },
"moduleName": {"type": "string", "description": "模块名称,创建流程时传入,指定流程服务所属的模块"},
"uuid": {"type": "string", "description": "流程服务设计元素的uuid, 创建流程不传入, 在更新的时候必须传入"},
"description": {"type": "string", "description": "流程服务描述, 总长度控制在200个字符内"},
"nodes": {
"type": "array", "description": "流程服务的节点列表",
"items": {
"type": "object","description": "流程服务节点对象", "required": ["name", "type", "description"],
"properties": {
"name": {"type": "string", "description": "节点名称, 英语描述, 单词之间使用下划线分割, 总称不超过32个字符"},
"type": {"type": "string", "description": "节点 类型,可以是 PROCESS_NODE(顺序节点) 、SWITCH_NODE(选择节点)、CONDITION_NODE(条件节点),START_NODE(开始节点)"},
"description": {"type": "string", "description": "描述节点的功能, 英语描述, 单词之间使用下划线分割, 总称不超过200个字符" }
2025-08-19 18:51:41 +08:00
}
2025-08-20 11:02:31 +08:00
}
},
"edges": {
"type": "array", "description": "流程服务的边列表",
"items": {
"type": "object","description": "流程服务的边", "required": ["fromNode", "toNode"],
"properties": {
"fromNode": {"type": "string", "description": "边的开始节点名称"},
"toNode": {"type": "string", "description": "边的结束节点名称"},
"value": {"type": "boolean", "description": "可选, 作为条件节点的出边, true值表示条件匹配的分支,false表示条件不匹配的分支; 作为循环节点的出边, true值表示进入循环, false值表示退出循环"}
2025-08-19 18:51:41 +08:00
}
2025-08-20 11:02:31 +08:00
}
}
}
}
```
- 例子:用户注册流程
< code >
2025-07-02 15:07:32 +08:00
{
2025-08-20 11:02:31 +08:00
"moduleName":"user",//该流程所属的模块
"name":"user_register",//定义该流程的功能
"description":"注册用户",//描述该流程的详细功能
"nodes": [ // 定义该流程包含的节点
{
"name":"start",
"type":"START_NODE",
"description": "起始节点"
},
{
"name":"check_user_registed",
"type":"CONDITION_NODE",
"description":"校验入参合法性;判断昵称是否重名;判断用户是否已注册"
},
{
"name": "create_user",
"descripiton":"创建用户,发送'用户创建'消息, 给用户发送欢迎通知",
"type": "PROCESS_NODE"
},
{
"name":"return_user_info",
"descripiton":"返回用户信息,返回推荐给用户可能感兴趣的好友",
"type":"PROCESS_NODE"
}
],
"edges": [
{
"fromNode": "start",
"toNode":"check_user_registed"
},
{
"fromNode":"check_user_registed",
"toNode":"create_user",
"value":false
},
{
"fromNode":"check_user_registed"
"toNode":"return_user_info",
"value":true
},
{
"fromNode":"create_user",
"toNode":"return_user_info"
}
]
2025-07-02 15:07:32 +08:00
}
2025-08-20 11:02:31 +08:00
< / code >
2025-06-22 14:59:22 +08:00
- **代码产物和修改建议**
2025-07-02 15:07:32 +08:00
- **FlowConfig**
* **生成产物:** 每个模块在service层的生成一个Java类, 负责注册模块下的所有流程到执行器
* **命名规则:** 类名为${moduleName}FlowConfig
* **职责:** 在应用启动的时候注册模块内的所有的流程服务到执行器
2025-08-14 11:07:23 +08:00
* **类路径:** < code > **.service.flow</ code >
2025-07-02 15:07:32 +08:00
- **Service**
* **生成产物:** 在service层的以模块名为类名前缀的${moduleName}FlowService中生成一个流程的入口函数
* **函数命名规则:** 流程名为方法名为后缀: pubic void invoke${functionFlowName}(${functionFlowName}Context context)
* **职责:** 在代码逻辑中,使用该流程需要以该函数作为调用入口
2025-08-14 11:07:23 +08:00
* **类路径:** < code > **.service</ code >
2025-07-02 15:07:32 +08:00
* **唯一标识符位置:** 其对应的标识符在函数的注解@AutoGenerated中指定 , uuid规则: ${FunctionFlow在TOCO中的uuid}|FLOW|METHOD
- **FlowNode**
* **生成产物:** 在service层生成一个Java类, **注意** 每个FunctionFlow的开始节点(StartNode)不生成
* **类命名规则:** ${nodeName}Node
* **入口函数命名:** pubic void process()
* **职责:** 用于封装内聚性的业务逻辑
2025-08-14 11:07:23 +08:00
* **类路径:** < code > service.flow.node.${functionFlowName}</ code >
2025-07-02 15:07:32 +08:00
- **FlowContext**
* **函数命名规则:** {nodeName}Node
* **职责:** 作为流程节点之间的参数传递(包括出参和入参),在实现业务逻辑的时候,按需在这个上下文类中添加所需的字段
2025-08-14 11:07:23 +08:00
* **类路径:** < code > **.service.flow.context</ code >
2025-07-02 15:07:32 +08:00
* **唯一标识符位置:** 其对应的标识符在类注解@AutoGenerated中指定 , uuid规则: ${FunctionFlow在TOCO中的uuid}|FLOW|CONTEXT
- **例子:**
- 用户登录, 在UserFlowService中生成一个函数invokeLoginFlow, 该函数通过流程框架根据流程定义调用LoginNode, LoginNode中封装了用户登录的逻辑, LoginFlowContext中封装了用户登录的参数和结果。
- **修改建议:** 不修改 service 中的函数, 不修改FlowConfig, 可以修改FlowContext, 添加/修改出入参数, 修改FlowNode中的具体业务逻辑。
2025-08-20 10:36:53 +08:00
2025-08-20 11:02:31 +08:00
#### **2.16 自定义查询**
2025-07-25 10:16:43 +08:00
- 在读方案无法满足需求的情况下,可以使用自定义查询
2025-07-25 16:16:23 +08:00
- 自定查询使用复杂的sql实现业务功能
- 自定义查询的数据访问层使用MyBatis、MyBatisPlus实现
2025-07-25 15:59:06 +08:00
- 自定查询的时候框架不自动生成任何代码(需要模型编写全部代码)
2025-07-25 10:16:43 +08:00
- 各层的代码位置严格遵守章节:**3.2 项目结构与导航**, 必须有mapper层、service层、必须有DO对象、必须有DTO对象, 如果是API返回数据, 必须有VO对象
2025-08-20 10:36:53 +08:00
2025-06-22 14:59:22 +08:00
### **3 生成代码产物补充说明**
2025-07-24 14:50:16 +08:00
- **3.1 支持的语言/框架**
Java、SpringBoot、MyBatis-plus(读)、Hibernate(写)
2025-08-19 18:51:41 +08:00
- **3.2 项目结构与导航**
2025-07-23 10:17:23 +08:00
TOCO生成的项目是一个多模块的SpringBoot项目,包括主模块和子模块,它的子模块位于根目录下的/modules目录中, **注意** 获取子模块代码文件路径的时候需要从根目录开始, 所以必须从modules节点开始, 例如: 子模块module1下的的java类路径应该是`modules/module1/src/main/java...`
2025-08-14 11:07:23 +08:00
< dir >
2025-07-02 15:07:32 +08:00
|──main_module
│ ├── common # 项目级别公共的基础模块
│ │ ├──config/ # 中间件配置
│ │ ├──constants/ # 项目级别常量
│ │ ├──enums/ # 项目级别枚举
│ │ ├──redis/ # Redis配置
│ │ ├──response/ # 返回结果封装
2025-08-08 17:40:19 +08:00
│ │ └──utils/ # 项目级别utils
2025-07-02 15:47:19 +08:00
└── └── entrance/ # 项目入口
│ └──AppApplication.java # 项目启动类
└── modules # 子模块列表
└── module1/ # 子模块1
2025-08-19 18:51:41 +08:00
├── common/
2025-07-02 15:47:19 +08:00
│ ├──constants/ # 模块常量
2025-08-08 17:40:19 +08:00
│ ├──utils/ #模块级别utils
2025-07-02 15:47:19 +08:00
│ └──enums/ # 枚举
2025-08-19 18:51:41 +08:00
├── entrance/web/src/main/java/com/{project_name}/{module_name}/entrance/web/
2025-08-01 18:30:31 +08:00
│ ├──controller/ # api 定义
2025-07-02 15:47:19 +08:00
│ ├──converter/ # 把DTO转化成VO
│ ├──vo/ # VO结构定义
│ └── query/ # 把读方案返回的数据转成成VO
│ ├── assembler/ # VO的数据填充
│ ├── collector/ # 读方案返回的id数据展开成完成对象数据
│ └── executor/ # 调用Service的度方案实现, 同时调用collector和assembler, 返回最终的VO
2025-08-19 18:51:41 +08:00
├── manager/src/main/java/com/{project_name}/{module_name}/manager/
2025-07-02 15:47:19 +08:00
│ ├── bo/ # 聚合对象定义
2025-07-02 18:48:49 +08:00
│ │ └── base/ # 聚合对象的基类
2025-07-04 15:51:32 +08:00
│ ├── dto/ # 数据传输对象定义
2025-07-02 15:47:19 +08:00
│ ├── converter/ # 复杂Dto( 非BaseDto)组装
│ ├── facade/ # 调用其他模块的RPC适配器, 包含RpcAdapter, 如UserDtoServiceInMeetingRpcAdapter, 表示从meeting模块调用user模块中方法
│ └── impl/ # Dto的查询接口的实现
2025-08-19 18:51:41 +08:00
├── persist/src/main/java/com/{project_name}/{module_name}/persist/
2025-07-02 18:45:52 +08:00
│ ├── eo/ # 值对象(Eo)的结构定义
2025-07-02 15:47:19 +08:00
│ ├── dos/ # 数据库单表结构的映射
│ ├── qto/ # 读方案的数据库查询实现
│ └── mapper/ # MyBatis的Mapper定义
2025-08-01 18:30:31 +08:00
└── service/src/main/java/com/{project_name}/{module_name}/service/ # BOService( 包含某个聚合下所有写方案生成的方法) 、 DtoService( 包含DTO生成的预定义方法)
2025-07-02 15:47:19 +08:00
├── bto/ # 写方案入参的定义
├── converter/ # 对返回的BaseDto按需进行二次扩展
├── query/ # 查询方案的service层入口, 调用persist层的查询实现
2025-07-20 15:53:28 +08:00
└── base/ # 每个BOService对应的基类
2025-07-02 15:07:32 +08:00
在一个子模块的内, 它的依赖层级为entrance -> service -> manager -> persist, 同时各个层都依赖 common
2025-08-14 11:07:23 +08:00
< / dir >
2025-07-24 14:50:16 +08:00
- **3.3 标准查找流程**
1. **API查找** → `modules/{模块名}/entrance/web/controller/`
2. **DTO查找** → `modules/{模块名}/manager/dto/`
3. **Service查找** → `modules/{模块名}/service/`
4. **数据层查找** → `modules/{模块名}/persist/
- **3.4 特殊注解及含义**
2025-06-22 14:59:22 +08:00
TOC自动生成的类和方法会带有@AutoGenerated注解 , 注解中有2个属性:locked为boolean类型, 如果locked=true, 则代表该文件或方法不建议修改;uuid为String类型, 表示该类或方法的唯一标识, 如果uuid中包含|字符, 则说明该uuid为特殊格式, 由不同类型的数据拼装而成(见**[3.2 设计元素到代码的映射规则及修改建议]**中每种设计元素的代码说明)。
2025-08-11 12:13:28 +08:00
- **3.5 代码严格分层**
Service层方法不能返回VO, 不能调用任何Controller层的方法, 如VoQueryExecutor、VoConverter等
2025-06-22 14:59:22 +08:00
2025-06-18 15:28:20 +08:00
### 4. TOCO 最佳实践
2025-07-31 10:22:29 +08:00
#### 4.1 接口参数类型和返回值选择(Interface Parameter & Return Type Definition):
2025-07-14 19:27:15 +08:00
在做TOCO接口(API、RPC)设计时, 通常会先判断接口的主要功能是读数据库或写数据库, 并分析相关的读写方案以及对应的QTO和BTO。如果是读场景, 则参数会**优先使**用相关的QTO; 如果是写场景, 则参数会**优先**使用相关的BTO, 如果BTO和QTO无法满足要求, 则可以再增加基本类型或Enum、EO等类型参数。参数类型选择时必须遵循以下要求: a.DTO、VO不能作为参数类型; b.QTO、BTO不能作为返回值类型。
2025-08-19 18:51:41 +08:00
**重要说明:**
2025-07-14 19:27:15 +08:00
- 本章节描述的是参数选择的**优先级原则**,不是绝对限制
- API参数的绝对限制请参考2.14章节
- "优先使用QTO/BTO"意思是在满足规范的前提下,根据场景选择最合适的参数类型
2025-08-15 18:03:21 +08:00
#### 4.2 在处理先读, 后更新的场景的时候, 为了避免并发引起的数据脏写, 应该充分利用BoService的校验功能。 例如: 在账户扣钱的情况, 为了避免账户在并发扣除余额不足, 在用raw sql实现中, 我们一般会 通过 update account set balance = balance - amount where user_id = xxx and blance > amount 的 where 条件保护; 在toco中,
2025-08-15 18:08:07 +08:00
应该1、在写方案中使用incr字段。 2、 在boService中, 添加类似保护代码 if(userBo.getBalance >= 0) 进行保护; 或则通过userBo的业务不变性( 聚合校验) , 添加balance>=0的校验
2025-07-28 19:18:21 +08:00
-----------------------------------------------------------------------------
2025-07-28 19:17:52 +08:00
< / TOCO知识库 >