106 KiB
这是一份完整的TOCO平台知识库文档,请先仔细阅读并理解其中的概念、规则和最佳实践。在后续的对话中,我将为你提供具体的角色定位和任务目标,请基于这份知识库来完成相关的设计和开发工作。
<TOCO知识库>
**1. TOCO 平台概览:**AppointmentBaseDto
- 1.1 平台简介: TOCO是软件设计和代码自动生成平台。基于DDD、分层架构、CQRS理论,覆盖数据库到API的完整开发链路,提升开发效率和代码质量
- 1.2 核心价值/目标用户: 提供软件设计能力,设计成果直接生成标准代码,提高编码一致性和效率
- 1.3 主要特性概览: 可视化设计、模型关联、多人协作、代码生成器
2. TOCO 设计元素:
2.1 模块 (Module)
- 定义与用途: 在TOCO中,我们将系统领域细分为具体的模块,映射为Java工程中的module。这些模块代表了系统的叶子子域,每个模块负责特定的功能。模块划分有助于系统的可维护性和可扩展性,并能提高开发效率和代码质量
- 关键配置: 名称(小写英文+下划线,如meeting,user_detail,禁止后缀,全局唯一),描述
- 与其他元素关系: 下列所有设计元素都属于某个模块
- 代码产物: 生成独立Java Module:项目路径/modules/模块名,包含entrance、service、manager、persist、common分层
2.2 枚举 (Enum)
- 定义与用途: 表示常量值集合,可跨模块使用,可作为字段类型
- 关键属性/配置: 名称(以_enum结尾,全局唯一),枚举值列表(全大写+下划线)
- 与其他元素关系: 可作为Entity、Dto、Vo、Bto、Qto、Eo的字段类型
- Enum设计元素的表达:
- Json格式,schema如下:
{ "type": "object", "properties": { "name": { "type": "string","description": "名称,英语表达,下划线分割,不超过32字符"}, "uuid": { "type": "string","description": "唯一标识符,创建时为空,更新时必填"}, "description": { "type": "string", "description": "枚举含义和用途,不超过128字符"}, "moduleName": { "type": "string", "description": "所属模块,创建时必填,更新时可空"}, "values": { "type": "array","description": "枚举值列表", "items": {"type": "string","description": "枚举值,英语表达,下划线分割,不超过32字符"} } }, "required":["name","description"] }
- 代码产物和修改建议
- 生成产物: common模块中生成Java类
- 职责: 表达Enum数据结构
- 命名规则: 类名以Enum结尾
- 类路径: 位于
**.common.enums包下 - 唯一标识符位置: 类注解@AutoGenerated中指定,uuid规则: ${Enum的uuid}|ENUM|DEFINITION
- 生成代码: 在common层生成Enum文件,如StatusEnum
- 修改建议: 不建议修改
2.3 值对象 (EO)
- 定义与用途: EO是可复用的POJO数据结构,可跨模块使用,可作为实体字段类型。
- 关键属性/配置: 名称以_eo结尾,全局唯一。字段类型限制:基本类型、List、EO、Enum。如果一个字段对应了某个Enum,则应该尽量使用该Enum作为字段类型
- 与其他元素关系: 可作为Entity、Dto、Vo、Bto、Qto的字段类型,支持EO嵌套。
- EO设计元素的表达:
- 使用Json格式表达,Schema定义如下:
{ "type":"object", "properties": { "name":{ "type": "string", "description": "名称,英文下划线分割,不超过32字符"}, "description": { "type": "string","description": "描述EO含义和用途,不超过128字符"}, "uuid":{ "type": "string", "description": "唯一标识符,创建时为空,更新时必填"}, "moduleName":{ "type": "string", "description": "所属模块,创建时必填,更新时可空"}, "fieldList":{ "type":"array", "description": "EO属性字段列表", "items":{ "type": "object", "properties":{ "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"} }, "required":[ "name","type"] } } }, "required":["name","description"] }
- 代码产物和修改建议
- 生成产物: persist层生成结构定义类,如AddressEo
- 职责: 表达POJO数据结构
- 命名规则: 类名以Eo结尾
- 类路径: 位于
**.persist.eo包下 - 唯一标识符位置: 类注解@AutoGenerated中指定,uuid规则:${Eo的uuid}|EO|DEFINITION
- 修改建议: 不建议修改
2.4 实体关系 (ER / Entity)
- 定义与用途: 实体对应数据库表,关系为实体间的外键依赖
- 关键属性/配置: 包含名称、字段、字段类型、主键、索引,关系分为1:1和1:N
- 与其他元素关系: 实体是聚合的基础,也是DTO和VO的派生基础
- 代码产物和修改建议
- 结构定义
- 生成产物:按Mybatis-plus要求生成的Java类
- 职责: 生成Mybatis-plus结构定义类文件
- 类路径: 位于
**.persist.dos包下 - 唯一标识符位置: 类注解@AutoGenerated中指定,uuid规则: ${Entity的uuid}|ENTITY|DEFINITION
- Mapper
- 生成产物:persist层的Mybatis-plus Mapper类
- 职责: 为Mybatis-plus框架提供Mapper
- 命名规则:类名以Mapper结尾(${entityName}Mapper)
- 类路径: 位于
**.persist.mapper.mybatis包下 - 唯一标识符位置: 类注解@AutoGenerated中指定,uuid规则: ${Entity的uuid}|ENTITY|MAPPER
- Dao接口
- 生成产物:persist层的Dao接口
- 职责: 提供Entity数据查询接口,为service层提供数据访问入口
- 命名规则:类名以Dao结尾(${entityName}Dao)
- 类路径: 位于
**.persist.mapper包下 - 唯一标识符位置: 类注解@AutoGenerated中指定,uuid规则: ${Entity的uuid}|ENTITY|IDAO
- Dao实现
- 生成产物:persist层的Dao接口实现类
- 职责: 通过调用Mapper实现Dao接口
- 命名规则:类名以DaoImpl结尾(${entityName}DaoImpl)
- 类路径: 位于
**.persist.mapper包下 - 唯一标识符位置: 类注解@AutoGenerated中指定,uuid规则: ${Entity的uuid}|ENTITY|DAO
- 修改建议: 不建议修改
- 结构定义
2.5 聚合对象 (BO/业务对象)
- 定义与用途: 聚合对象封装一组关联实体。从聚合根实体开始,按层级关系组装其他实体,形成树形结构。提供内存一致性视图和数据操作入口。一个实体只能属于一个聚合对象,聚合对象只能在单一模块中组合。
- 包含元素: 聚合根实体 + 子实体对象。例如:ProductBO包含商品基本信息实体(聚合根)、商品SKU实体、商品库存实体(子对象)。
- 关键配置: 名称(${EntityName驼峰}BO,如StaffBO),聚合根实体,子对象实体。每个聚合必须包含一个聚合根。
- 与其他元素关系: 聚合是写方案的基础。
- 代码产物和修改建议
- 综述
- 业务对象包含多个Entity,通过嵌套组合表达Entity关系。有子对象时生成BO和BaseBO,BaseBO封装实体属性和关系,BO用于业务扩展。无子对象时只生成BO类。
- BO
- 生成产物:Manager层生成聚合对象类,符合Hibernate标准
- 职责: 定义聚合对象,组合成层级结构实现充血模型,支持写链路数据变更、监听变更、数据校验
- 命名规则:${entityName}BO
- 类路径:
**.manager.bo包下 - 唯一标识符位置: 类注解@AutoGenerated中,uuid规则: ${Entity的uuid}|BO|DEFINITION
- 聚合校验: BO中包含一个validateAggregate(聚合根BO)或valid(非聚合根BO)方法,用来编写用于验证业务不变性相关的聚合校验代码。在通过聚合对象操作数据库时,TOCO会在框架中自动生成对validateAggregate方法的调用,所以禁止在业务代码显式调用,以免造成重复校验。
- 修改建议: 建议修改BO中的validateAggregate或valid方法进行业务不变性校验。不建议修改校验方法以外的代码。校验方法由框架自动调用,不需要业务代码显式调用。
- BaseBO
- 生成产物:存在子BO时生成,封装不变代码部分
- 职责: 定义聚合对象,组合成层级结构实现充血模型,支持写链路数据变更、监听变更、数据校验
- 命名规则:${entityName}BO
- 类路径:
**.manager.bo包下 - 唯一标识符位置: 类注解@AutoGenerated中,uuid规则: ${Entity的uuid}|BO|DEFINITION
- 修改建议: 建议修改BO中的validateAggregate或valid方法进行业务不变性校验。不建议修改校验方法以外的代码。
- 综述
2.6 数据传输对象 (DTO)
- 定义与用途: 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;c.自定义字段,类型为基本类型、Eo、Enum、DTO。BaseDTO包含Entity全部字段,DTO包含BaseDTO全部字段,不裁剪字段,可通过外键关系扩展其他Entity,无法扩展时可增加自定义字段。
- 字段扩展方式: DTO通过外键关系替换/注入对应Entity信息。满足条件可扩展:a.正向替换:当前实体有指向其他实体的外键字段;b.反向注入:其他实体有指向当前实体的外键字段。
例如:两个Entity
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不替换:
MeetingWithRoomDto {
Long id;// 会议id
MeetingRoomDto 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反向注入到MeetingRoom中:
MeetingRoomWithMeetingDto {
Long id;// 会议室id
String name;// 会议室名称
List<MeetingBaseDto> meetingList { //反向注入使用该会议室的会议信息
Long id;// 会议id
String title; //会议标题
DateTime startTime; //开始时间
DateTime endTime; //结束时间
}
}
"正向替换"和"反向注入"可按需递归调用,组装多个有外键关系的对象。
- 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时包含。
示例:
- meeting_with_room_dto
{
"dto": {
"uuid": null,
"name": "meeting_with_room_dto",
"description": "会议详情,包含会议室信息",
"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":[
{
"uuid": "自定义字段唯一标识,更新DTO时需传入",
"name": "status",
"type": "Enum",
"typeUuid": "对应Enum的uuid",
"description": "当前状态"
}
]
}
}
- meeting_room_with_meetings_dto
{
"dto": {
"uuid": "d05c7b3d-1c92-45a1-2113-a01b245813c1",
"name": "meeting_room_with_meetings_dto",
"description": "会议室详情,包含会议信息",
"fromEntity": "meeting_room",
"reverseExpandList": [
{
"foreignKeyInOtherEntity": "room_id",
"dto": {
"uuid": "ffeec02d-2a32-1531-1ce1-b9bfc1993765",
"name": "meeting_base_dto",
"description": "会议基本信息",
"fromEntity": "meeting"
},
"dtoFieldName": "meeting_list"
}
]
}
}
meeting_with_room_dto无uuid,为待创建DTO。meeting_base_dto和meeting_room_with_meetings_dto为已存在DTO,带uuid。
- 预定义方法: 每个DTO基于根Entity的唯一索引自动生成预定义RPC方法及实现,预定义方法获取根Entity数据,通过RPC自动获取所有扩展字段数据并拼装。如实体user有唯一索引username,则为UserDto生成UserDto UserDtoService.getByUserName(String userName)和List UserDtoService.getByUserNames(List 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
判断使用方式的步骤:
- a. 查询DTO条件为主键或唯一索引的值或列表,使用第1种方式
- b. 通过其他复杂查询条件,采用第2种方式
判断使用方式时,只能根据查询条件判断是否使用读方案,禁止使用返回值是否需要数据拼装来判断!如现实代码和TOCO定义有冲突,只能使用TOCO定义!
- 代码产物和修改建议
- 结构定义
- 生成产物:Java类
- 职责: 表达DTO数据结构
- 命名规则:类名以Dto结尾
- 禁止 修改该类
- 类路径:
**.manager.dto包路径下 - 唯一标识符位置: 类注解@AutoGenerated中指定,uuid规则:${DTO在TOCO中的uuid}|DTO|DEFINITION
- Manager
- 生成产物: Java接口及实现类
- 命名规则: 接口类名以Manager结尾、实现类名以ManagerImpl结尾(${DtoName}Manager)、基类名以ManagerBaseImpl结尾(${DtoName}ManagerImpl)
- 职责: 提供DTO数据获取接口,包括根据id单个、id列表批量获取、根据DTO对应实体的数据库索引获取
- 类路径:
**.manager包路径下 - 禁止 删除该类中任何系统自动生成函数
- 唯一标识符位置: 类注解@AutoGenerated中指定,uuid规则:${DTO在TOCO中的uuid}|DTO|MANAGER
- Converter
- 生成产物: Java实现类及基类
- 禁止 删除该类中任何系统自动生成函数
- 命名规则: 实现类名以Converter结尾(${DtoName}Converter)、基类名以BaseConverter结尾(${DtoName}BaseConverter)
- 职责: Entity转换到BaseDTO或BaseDTO转换为普通DTO:从Entity转为BaseDTO的方法命名为convert${EntityName}To${DtoName};从BaseDTO转换为DTO的方法命名为convert${BaseDtoName}To${DtoName}
- 类路径:
**.manager.converter包路径下 - 唯一标识符位置:
- 实现类Converter类注解@AutoGenerated中指定,uuid规则:${DTO在TOCO中的uuid}|DTO|CONVERTER
- 基类BaseConverter类注解@AutoGenerated中指定,uuid规则:${DTO在TOCO中的uuid}|DTO|BASE_CONVERTER
- 例子:
- 如UserDto、UserDtoManager、UserDtoConverter extends UserDtoBaseConverter、UserDtoService(名称为${DtoName}Service,内部包含getBy${PrimaryKey}、getBy${PrimaryKey}s等方法)。如Dto为UserBaseDto,则生成类名为UserBaseDtoService
- 修改建议:
- 建议在Service与BaseConverter中扩展代码,不建议修改结构定义文件和Manager文件。DTO的自定义字段不直接派生自Entity,一般对应取数逻辑代码。涉及数据获取、计算和拼装,批量处理性能最好,代码位置必须放在BaseConverter中已自动生成的列表转换方法中,批量取数组装,如UserBaseDtoBaseConverter.convertUserToUserBaseDto(List)或UserDtoBaseConverter.convertUserBaseDtoToUserDto(List)
- 结构定义
2.7 视图对象 (VO)
-
定义与用途: VO基于BaseDTO(或指定的其他DTO)派生,通过外键关系关联多个BaseDTO的数据结构。用于视图层与前端数据传输,作为HTTP API返回值或读方案返回值。不能作为接口参数或RPC返回值。
-
关键配置: 名称(以Vo结尾,全局唯一)、根Entity、所属模块、派生源、字段列表。字段分三种:a.继承DTO字段(基础类型/EO/Enum保持类型不变,DTO类型转为对应VO类型);b.扩展字段(正向替换和反向注入,类型为VO或List);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所属模块: VO无法被跨模块引用,模块A内部的API只能使用模块A内部的VO。VO可以与其派生源DTO不在同一模块,如:meeting模块的API需要返回meeting_room_dto派生出的VO,则需要在meeting模块创建派生自meeting_room_dto的VO,以便被meeting模块的API使用,注意创建VO时传入的moduleName字段,用于指定VO所在的模块。
-
创建方式: 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
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; //会议结束时间 }Meeting和MeetingRoom是n:1关系。多个会议占用同一个会议室。 通过"正向替换"组装出:
MeetingWithRoomVo { //根VO,需通过TOCO创建 Long id;// 会议id MeetingRoomVo room { //正向替换会议室信息,TOCO自动生成的内部VO,派生自MeetingRoomBaseDto Long id; //会议室ID String name;// 会议室名称 } Long backupRoomid; // 不变 String title; //会议标题 DateTime startTime; //会议开始时间 DateTime endTime; //会议结束时间 }通过"反向注入"生成:
MeetingRoomWithMeetingVo{ Long id;// 会议室id String name;// 会议室名称 List<MeetingVo> meetingList{ //反向注入使用该会议室的会议信息,TOCO自动生成的内部VO,派生自MeetingBaseDto Long id;// 会议id String title; //会议标题 DateTime startTime; //会议开始时间 DateTime endTime; //会议结束时间 } } -
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字段非DTO类型则VO字段类型与DTO一致,若DTO字段为DTO或List类型,由于VO字段不能为DTO,必须将DTO转换为VO,所以extendFieldList会有vo结构表示该字段DTO类型派生出的VO定义,注意:该VO字段中的VO类型必须派生自继承的DTO字段类型!
示例:系统中存在meeting_detail_dto
{
"uuid": "cd55c96b-aa67-bfb2-7614-70b503a8f8bf",
"name": "meeting_detail_dto",
"fromEntity": "meeting",
"description": "会议详情",
"expandList": [
{
"foreignKeyInThisEntity": "create_user_id",
"voFieldName": "create_user",
"dto": {
"uuid": "53bb59cf-1ed2-6fb3-9f61-895b638903d8",
"name": "user_base_dto",
"fromEntity": "user",
"description": "用户基本信息"
}
}
]
}
- meeting_with_room_vo由meeting_detail_dto派生,继承id和create_user字段
{
"vo": {
"uuid": null,
"name": "metting_with_room_vo",
"description": "会议详情,包含会议室信息,以及会议室禁用列表",
"rootVo": "metting_with_room_vo",
"moduleName": "meeting",
"fromEntity": "meeting",
"fromDto": "meeting_detail_dto",
"fromDtoUuid": "cd55c96b-aa67-bfb2-7614-70b503a8f8bf",
"extendFieldList":[
{
"name": "id"
},
{
"name": "create_user",
"vo": {
"uuid": null,
"name": "user_base_vo",
"description": "用户信息",
"rootVo": "metting_with_room_vo",
"fromEntity": "user",
"fromDto": "user_base_dto",
"fromDtoUuid": "53bb59cf-1ed2-6fb3-9f61-895b638903d8",
"reverseExpandList": [],
"customFieldList":[],
"extendFieldList":[
{
"name": "name"
},
{
"name": "gender"
}
]
}
}
],
"expandList": [
{
"foreignKeyInThisEntity": "room_id",
"voFieldName": "meeting_room",
"vo": {
"name": "meeting_room_with_meetings_vo",
"description": "会议室信息,包含会议列表",
"rootVo": "metting_with_room_vo",
"fromEntity": "meeting_room",
"fromDto": "meeting_room_base_dto",
"fromDtoUuid": "88437212-6370-99a6-1e7a-fe1469082d08",
"reverseExpandList": [
{
"foreignKeyInOtherEntity": "room_id",
"vo": {
"uuid": null,
"name": "meeting_base_vo",
"description": "会议基本信息",
"rootVo": "metting_with_room_vo",
"fromEntity": "meeting",
"fromDto": "meeting_base_dto",
"fromDtoUuid": "1a768c5e-b449-db5d-fe55-9d572d64332a",
"extendFieldList":[
{
"name": "startTime"
},
{
"name": "endTime"
}
]
},
"voFieldName": "meeting_list"
}
],
"customFieldList":[
{
"uuid": "自定义字段的唯一标识,更新DTO的时候需要传入",
"name": "occupied",
"type": "Boolean",
"description": "是否被占用"
},
{
"uuid": "自定义字段的唯一标识,更新DTO的时候需要传入",
"name": "custom_eo",
"type": "Eo",
"typeUuid": "uuid of an eo"
},
{
"uuid": "自定义字段的唯一标识,更新DTO的时候需要传入",
"name": "status_list",
"type": "List",
"innerType": "Enum",
"innerUuid": "uuid of an enum"
},
{
"uuid": "自定义字段的唯一标识,更新DTO的时候需要传入",
"name": "custom_string_list",
"type": "List",
"innerType": "String"
}
],
"extendFieldList":[
{
"name": "id"
},
{
"name": "name"
}
]
}
}
]
}
}
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类型。
- 派生源默认使用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
TOCO中判断使用方式的步骤:
- a. 查询VO条件为主键或唯一索引的值或列表,使用第1种方式
- b. 通过其他复杂查询条件,可采用第2种或第3种方式
- c. 用户上下文中有指定返回DTO的读方案符合条件,使用第2种,否则使用第3种
注意判断使用方式时,只能根据查询条件判断是否使用读方案,绝对禁止使用返回值是否需要数据拼装来判断!现实代码编写方式和TOCO步骤冲突时,只能使用TOCO定义!
- 代码产物和修改建议
- 结构定义
- 生成产物: controller层生成Java类
- 命名规则: 类名以Vo结尾
- 职责: 表达VO数据结构
- 类路径:
**.entrance.web.vo包路径下 - 唯一标识符位置: 类注解@AutoGenerated中指定,uuid规则:${VO在TOCO中的uuid}|VO|DEFINITION
- Converter
- 生成产物: controller层生成Java类(有派生源的VO才有Converter,无根VO没有Converter)和基类
- 命名规则: 实现类名以Converter结尾(${VoName}Converter),基类名以BaseConverter结尾(${VoName}BaseConverter)
- 类路径:
**.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
- 唯一标识符位置:
- 实现类Converter类注解@AutoGenerated中指定,uuid规则:${VO在TOCO中的uuid}|VO|CONVERTER
- 基类BaseConverter类注解@AutoGenerated中指定,uuid规则:${DTO在TOCO中的uuid}|DTO|BASE_CONVERTER
- 例子:
- 如UserDetailVo、UserDetailVoConverter(含convertToUserDetailVo、convertToUserDetailVoList、convertToUserDetailVoMap、convertAndAssembleData、convertAndAssembleDataList方法)
- 修改建议:
- 建议在Converter中扩展代码,不建议修改结构定义文件。VO的自定义字段不直接派生自DTO,一般对应取数逻辑代码。涉及数据获取、计算和拼装时,批量处理性能最好,自定义字段对应代码位置必须放在Converter的Map基础转换方法convertTo${VoName}Map中,批量取数组装,如UserVoConverter.convertToUserVoMap
- 结构定义
2.8 查询对象(WO)
- 定义与用途: WO基于Entity通过外键关系关联多个Entity的数据结构。WO表达数据取数拼装逻辑,符合外键关系。WO作为ReadPlan查询上下文,创建ReadPlan前需先创建WO对象。理解ReadPlan语义时需要WO作为上下文。 当返回DTO|VO时需要对列表属性进行过滤,则根据DTO|VO结构定义,扩展对应WO对象(过滤字段名需与DTO|VO字段名一致)。例如: 查询DTO
class MeetingDto {
String meetingId;
String meetingName;
List<MeetingAgenda> agendaList;
}
需求:根据会议名称查询会议列表,根据议程信息过滤部分议程。定义查询对象时需包含议程信息,扩展字段名定义为agendaList
-
查询对象设计元素表达 以json格式表达,json schema如下:
{ "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": "扩展字段名称"} } } } } } -
创建/生成方式:
- 创建思路 按查询返回的DTO或从DTO派生出的VO结构,构建同构WO对象(扩展和反向扩展字段名保持一致),根据查询需求和过滤需求对WO二次裁剪或扩展:
- 去掉过滤和查询都不需要的扩展
- 补全查询或字段过滤需要的扩展
- 关键配置: WO字段分三种:a.继承Entity字段,类型与Entity相同;b.扩展字段,含正向替换和反向注入字段,类型为WO或List
- 字段扩展方式: WO通过外键关系替换/注入对应Entity信息,对象化表达有外键关系的Entity信息。存在外键关系且满足条件即可扩展:a.正向替换:当前实体有指向其他实体的外键字段;b.反向注入:其他实体有指向当前实体的外键字段。
- 禁止从无根VO创建WO
- 创建思路 按查询返回的DTO或从DTO派生出的VO结构,构建同构WO对象(扩展和反向扩展字段名保持一致),根据查询需求和过滤需求对WO二次裁剪或扩展:
例如:两个Entity
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反向注入到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示例:
{
"wo": {
"uuid": null,
"name": "meeting_with_room_and_agenda_wo",
"dtoOrVoId":"96eedc80-2c29-44ab-883f-031efdba43e8",
"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": "会议议程信息"
}
}
]
}
}
2.9 读方案 (ReadPlan)
-
定义与用途: 读方案描述如何从数据库获取DTO和VO列表数据,提供三个能力:
- 根据查询条件返回符合条件的DTO或VO的id列表
- 根据查询条件返回符合条件的DTO或VO的id数量
- 根据字段过滤条件对DTO和VO的列表字段数据进行过滤
-
注意 不派生自DTO的VO不能创建读方案
-
读方案能力边界
- 支持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语法)。属性字段是从根节点属性到当前属性的路径
-
自定义字段:可以添加自定字段,对度方案生成产物Qto对象进行扩展 (注意,自定义字段不能用于查询语法, 自定义字段主要用与参数传递)
-
查询语法和过滤语法
- 基本语法格式:属性名 操作符 变量或常量,变量格式为#变量名;枚举类型常量用单引号包围,例如:'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。本对象不能直接用此操作符
- 对象属性可对其子属性进行上述查询
- 查询变量不能作为条件属性
- 查询条件间可用AND, OR连接
- 可插入括号()对条件分组
- 列表对象属性只能使用contains、isNull、isNotNull操作符:wo列表类型可用contains(子查询),表示列表属性中需包含至少一个满足子查询条件的对象;其他列表类型只能用isNull或isNotNull
- 通过and, or, not连接符拼装子查询完成查询。不要使用未提及的操作符号、连接符
- 查询条件中的入参可在运行时传入或不传入值,不传入值表示该参数相关条件不起作用;基于这种动态效果,多条件联合查询可优先使用AND
- 使用点号(.)访问当前对象的单值对象类型子属性,可多个点号组合访问嵌套单值对象属性
- 查询条件中的属性必须是当前查询对象的属性或单值对象属性或单值对象子属性
- 禁止使用filter语法
- 禁止使用has语法
- 语法定义:使用 lezer 定义了如下语法
@top Program { expression? } @skip { spaces | newline | LineComment } @precedence { member, and @left, or @left } kw { @specialize[@name={term}]<identifier, term> } boolean { @specialize[@name=Boolean]<identifier, "true" | "false"> } @skip {} { String[isolate] { '"' (stringContentDouble | Escape)* ('"' | "\n") | "'" (stringContentSingle | Escape)* ("'" | "\n") } } commaSep { content ("," content)* } List { "[" commaSep ~destructure "]" } Value { String | Number | List | boolean } 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]) } Number { (@digit ("" | @digit)* ("." ("" | @digit)*)? | "." @digit ("" | @digit)) (("e" | "E") ("+" | "-")? ("" | @digit)+)? | @digit ("" | @digit) "n" | "0x" (hex | "")+ "n"? | "0b" $[01]+ "n"? | "0o" $[0-7_]+ "n"? }
@precedence { Number "." } ThreeOperator { "in" | "notIn" | "!=" | "==" | ">" | ">=" | "<" | "<=" | "isNullOrNot" | "like" | "has" } TwoOperator { "isNull" | "isNotNull" } ListOperator { "contains" | "filter" } LineComment[isolate] { "//" ![\n]* } "(" ")" "[" "]" "." } @detectDelim -
读方案设计元素的表达
- 以json格式表达,json schema 定义如下
{ "type":"object","description":"读方案定义", "properties":{ "qto": { "type":"object", "properties":{ "name": {"type":"string","description":"读方案名称,表达了查询的意图;英语描述,单词之间使用下划线分割,长度限制在32个字符内"}, "uuid": {"type":"string","description":"读服务设计元素的uuid,创建时不传入,在更新的时候必须传入"}, "woId": {"type":"string","description":"查询对象的uuid,作为查询服务的上下文,创建的时候必须指定,更新的时候不传递"}, "description": {"type":"string","description":"描述读方案的功能,长度限制在256个字符内"}, "supportCount": {"type":"boolean", "description":"是否需要生成计数接口"}, "supportPagination": {"type":"boolean", "description":"是否需要分页"}, "supportQueryAll":{"type":"boolean","description":"如果不需要分页,一次性返回部数据"}, "supportWaterfall":{"type":"boolean","description":"是否需要瀑布流"}, "query":{"type":"string","description":"查询语句,符合前述语法定义"}, "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(降序)"}}} }, "filters": {"type": "array", "description":"过滤条件定义", "items": {"type":"object","description":"定义过滤条件","required":["fieldPath", "filter"], "properties":{ "fieldPath": {"type":"string","description":"需要过滤的列表字段路径"}, "filter": {"type":"object","description":"过滤语法,符合前述的语法定义,不能使用contains语法"} } } }, "customField": { "type": "array", "items": { "type": "object", "description": "自定义字段名称", "properties": { "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" } }, "required": [ "name", "type" ] }, "description": "自定义字段列表" } "required":["name","description","query"] } }, "required":["qto"] } } -
举例
- 上下文
public class meeting_room { storey storey; // 楼层对象 String name; // 名称 room_type_enum room_type; // 会议室类型 List<equipment_enum> equipment; // 设备列表 Long id; // 主键 Long seat_number; // 座位数 String description; // 说明 Boolean enable_indicator; // 是否启用 String input_code; // 输入码 Long storey_id; // 楼层id Long contact_staff_id; // 联系员工ID List meeting; // 会议列表,可添加过滤条件 class storey { // 楼层 location location; // 位置对象 String name; // 楼层名 Long id; // 主键 Long sort_number; // 排序号 Long location_id; // 位置id } class meeting { Long id; // 主键 String title; // 标题 Date start_time; // 开始时间 Date end_time; // 结束时间 Long room_id; // 会议室id String description; // 描述 Long create_user_id; // 创建人id } class location { // 位置,如楼栋 String name; // 名称 Long id; // 主键 String description; // 描述 Long sort_number; // 排序号 } enum room_type_enum { SMALL, //小会议室 MEDIUM, //中会议室 LARGE } enum equipment_enum { TV, //电视 WHITEBOARD, //白板 PROJECTOR, //投影仪 VIDEO } } - 用户需求 获取一段时间未使用的会议室(包含当前会议已选择的会议室),根据会议标题过滤会议信息,分页返回。
- 读方案定义
{ "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" } ] }
- 上下文
-
代码产物和修改建议
-
QTO查询参数对象
- 生成产物: Service层生成Java类
- 命名规则: 类名以Qto结尾(${ReadPlanName}Qto)
- 类路径:
**.persist.qto包下 - 职责: 读方案的查询参数结构,可作为API参数,直接透传给RPC调用
- 唯一标识符位置: 类注解@AutoGenerated中指定,uuid规则: ${ReadPlan的uuid}|QTO|DEFINITION
- 内部结构: QTO包含分页相关字段:
private String scrollId; //瀑布流游标 private Integer size; //每页记录数 private Integer from; //开始记录位置
-
Dao
- 生成产物: Dao层生成Java类
- 命名规则: 类名以Dao结尾(${ReadPlanName}Dao)
- 类路径:
**.persist.mapper包下 - 职责: 读方案对应的数据库查询方法
- 唯一标识符位置: 类注解@AutoGenerated中指定,uuid规则: ${ReadPlan的uuid}|QTO|DAO
-
QueryExecutor
- 生成规则
- 只有当读方案明确指定返回VO时才生成QueryExecutor
- QueryExecutor的命名规则:${VoName}QueryExecutor
- 如果读方案返回DTO,则不生成QueryExecutor,需要通过DtoQueryService + VoConverter组合实现
- 生成产物 返回VO的查询方案,在entrance层生成Java类,包含独立函数:
- 分页查询: **Paged函数,返回VSQueryResult
- 不分页全量: query**函数,返回List
- 瀑布流: **Waterfall函数,返回VSQueryResult
- 查询数量: **Count函数,返回Integer
- 命名规则: 类名以QueryExecutor结尾(${VoName}QueryExecutor)
- 类路径:
**.entrance.web.query.executor包下 - 职责: 提供VO查询入口,将QtoService返回的id数据转化为目标VO
- 生成规则
-
QueryService
- 生成条件 如果一个读方案返回的是DTO结构,则会生成QueryService
- 生成产物 返回DTO的查询方案,在service层生成Java类,包含独立函数:
- 分页查询: **Paged函数,返回VSQueryResult
- 不分页全量: query**函数,返回List
- 瀑布流: **Waterfall函数,返回VSQueryResult
- 查询数量: **Count函数,返回Integer
- 命名规则: 类名以QueryService结尾(${DtoName}QueryService)
- 类路径:
**.service.index.entity包下 - 职责: 提供DTO查询入口,将QtoService返回的id数据转化为目标DTO,或返回符合条件的记录数量
- 生成产物 返回DTO的查询方案,在service层生成Java类,包含独立函数:
- 生成条件 如果一个读方案返回的是DTO结构,则会生成QueryService
-
VSQueryResult说明:
- VSQueryResult是固定类,全路径为com.vs.es.query.VSQueryResult,存在于jar中,直接使用,禁止创建新的VSQueryResult类!
- VSQueryResult代码结构:
package com.vs.es.query;
import lombok.Data;
import java.util.List;
@Data public class VSQueryResult { //T为DTO或VO类型 private int count; private List result;
private int from; //页码分页起始条数 private int size; //每页记录数量 private String scrollId; //瀑布流查询游标 private boolean hasMore; //瀑布流查询是否有下一页} -
例子:
- 根据用户名称查询用户列表返回UserDTO,生成UserNameQto、UserNameQtoService、UserNameQtoDao、UserNameQueryService; UserNameQueryService调用UserNameQtoService,UserNameQtoService调用UserNameQtoDao
-
修改建议:
- 如有结果数据二次处理,建议在QueryService和QueryExecutor中扩展代码,不建议修改QTO文件
-
2.10 查询传输对象(QTO)
- 定义与用途: QTO是读方案的查询参数结构。每个读方案对应一个QTO。调用方使用QTO结构传入查询字段值,完成数据库查询
- 如何创建/生成: 创建读方案后,TOCO自动生成对应的QTO。无需单独创建
- 关键配置: 名称(${ReadPlanNameQto},驼峰命名),查询字段列表(如idIs,nameLike,schoolNameLike等)
- 与API的关系: QTO可作为API参数。API接收参数后直接透传给内部RPC调用。QTO只用于读操作参数,禁止用于写参数结构
2.11 写方案 (WritePlan,单聚合操作)
-
定义与用途: 写方案是数据库写操作的唯一方式,每个写方案只能变更一个聚合的数据。写方案可以一次操作聚合内的多张表。例如: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 + ?,传入负值可减值
- 字段选择:
- 按需选择字段
- 当CREATE操作的时候,如果主键不又入参指定,不勾选主键字段,主键字段的值由框架分配,如果勾选了主键,但是不传递参数值,会造成运行时异常
- 自定义字段:除了来自实体的字段外,还可以添加自定字段,对生成物Bto对象进行扩展
- 写方案设计元素表达:
{ "type": "object", "description": "写方案定义", "properties": { "uuid": { "type": "string", "description": "写方案唯一标识,创建时不传,更新时必传" }, "name": { "type": "string", "description": "写方案名称,英文+下划线,32字符内" }, "description": { "type": "string", "description": "写方案描述,256字符内" }, "bo": { "type": "string", "description": "聚合根名称" }, "operations": { "type": "array", "description": "实体操作定义,包括操作类型和字段", "items": { "type": "object", "description": "具体实体操作定义", "properties": { "bo": { "type": "string", "description": "操作的实体" }, "action": { "type": "string", "description": "实体操作类型" }, "uniqueKey": { "type": "array", "description": "唯一键字段列表,用于确定数据记录", "items": { "type": "string", "description": "字段名称" } }, "fields": { "type": "array", "items": { "type": "string", "description": "字段名称,必须来自bo对象" }, "description": "操作字段列表" }, "incrFields": { "type": "array", "items": { "type": "string", "description": "增量字段名称" }, "description": "增量操作字段列表" }, "customField": { "type": "array", "items": { "type": "object", "description": "自定义字段名称", "properties": { "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" } }, "required": [ "name", "type" ] }, "description": "自定义字段列表" }, "required": [ "bo", "action", "fields" ] } } } }, "required": [ "name", "description", "operations", "bo" ] } -
示例:
- 上下文:
public class meeting {//会议聚合 //唯一键:(id)、(meeting_name) Long id; //主键 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;//好友数量 }- 需求: 创建会议
- 对应写方案定义:
{ "name": "create_meeting", "description": "创建会议", "bo": "meeting", "operations": [ { "bo": "meeting", "action": "CREATE", "fields": [ "meeting_name", "start_time", "end_time" ] } ] } -
代码产物和修改建议:
- 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函数,一起完成用户创建和设置创建逻辑。
-
修改建议:
- 不能修改BaseBOService函数,不建议修改BTO文件。建议在BOService中扩展代码,处理可能被复用的前后逻辑,如数据库前后值对比、常被复用的校验逻辑(业务不变性校验除外)、需要在一个事务内执行的其他写操作等。
- 父类函数返回BoResult类,记录各Bto和Bo实例对应关系及Bto实例操作结果,可通过以下接口获取。例如:创建用户的写方案中,BoService入参为CreateUserBto,用户id由数据库生成,需要返回创建用户id时,通过boResult.getAddedResult(createUserBto).getBo().getId()返回新建用户id
/** * 获取更新成功的bto结果 */ public UpdatedBto getUpdatedResult(final Object bto); /** * 获取成功创建的Bto结果 */ public AddedBto getAddedResult(final Object btoObj); /** * 获取Bto对应的删除结果 */ public DeletedBto getDeletedResult(final Object btoObj);//记录Bto和它对应的更新的实体的前值以及更新后的Bo的 @Setter @Getter public class UpdatedBto<Bto,Entity,BO> { private Bto bto;//Bto入参 private Entity entity;//Bto对应的Entity,更新前值 private BO bo;//bo,更新后的值 }//记录Bto和它对应删除的实体 @Setter @Getter public class DeletedBto<Bto,Entity> { private Bto bto; //Bto入参 private Entity entity; //Bto对应的Entity,前项 }//记录Bto和它对应创建出的Bo @Setter @Getter public class AddedBto<Bto,BO> { private Bto bto;//Bto入参 private BO bo;//根据bto创建出来的Bo }
2.12 业务变更传输对象(BTO)
- 定义与用途: BTO是写方案自动生成的参数结构,每个写方案生成一个BTO。BTO将操作实体按关系组成树形集合,聚合根在最外层。调用方按BTO结构传入实体字段值,完成数据库写操作
- 创建方式: BTO只能由写方案自动创建,不能单独新建。创建写方案后,TOCO自动生成对应BTO作为参数结构
- 关键配置: 名称(${WritePlanName}Bto,驼峰命名),嵌套树形实体和字段列表,字段全部来自Entity。示例:
class CreateUserBto { //对应实体user
Long id; //来自user.id
String name; //来自user.name
List<PictureBto> pictureList;
class PictureBto { //对应实体picture
String url; //来自picture.url
}
}
- 与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有4种创建方式:a.创建DTO后自动生成RPC,公开性与DTO一致;b.返回DTO的读方案根据分页和计数配置自动生成非公开RPC;c.写方案创建后自动生成非公开RPC,返回聚合根实体主键值;d.需求不满足时通过TOCO创建自定义RPC,需指定参数、返回值、公开性
- 优先复用: 用户需要创建RPC时,有明确要求按要求创建。无明确要求时先判断能否通过读方案、写方案、DTO自动创建RPC,最后考虑自定义RPC
- 自定义RPC和手写方法关系: 两者都可手动实现服务层方法。区别:需要被其他模块订阅用自定义RPC;只是API私有调用不需要外部开放用手写方法
- 关键配置: 类名(驼峰,首字母大写,以Service结尾)、是否公开、方法名(驼峰,首字母小写)、请求参数、返回值。分页查询参数为Qto类型时,Qto已包含from、size、scrollId属性,无需额外参数
- TOCO中RPC存储: RPC在TOCO中只存储方法签名,不存储执行逻辑。了解实现逻辑需阅读RPC对应代码
- 参数类型: RPC参数只能为QTO、BTO、Enum、EO、基本类型,可为单值或列表。对象类型优先用QTO、BTO,禁用VO和自定义结构如Object
- 返回值类型: RPC返回值只能为DTO、Enum、Eo、基本类型,可为单值或列表,禁用VO、QTO、BTO、自定义结构如Object。对象类型优先用DTO
- TOCO中json结构: TOCO中DTO用json表示,示例:
{
"methodName": "saveUsers",
"className": "UserSaveService",
"requestParams":[
{
"name": "saveUserBtoList",
"description": "批量保存用户参数",
"type": "List",
"innerType": "Bto",
"innerUuid": "dbvvc4d4-0063-442f-abd7-vrfded656988"
}
],
"response": {
"type": "Boolean"
}
}
关键字段:requestParams为请求参数列表,response为返回结构,参数和response结构相同,其中:name为参数名;type为参数类型,取值Boolean,String,Integer,Long,Float,Double,BigDecimal,Date,ByteArray,Enum,Eo,Dto,Qto,Bto,List,PageResult,Void,参数不能为Void和PageResult,无返回值设为Void,分页查询结果设为PageResult且innerType必为Dto,对应VSQueryResult;description为描述;typeUuid为类结构UUID,type为Enum、Eo、Dto时传入对象uuid,Qto时传入读方案uuid,Bto时传入写方案uuid;innerType为List内部类型,type为List或PageResult时包含;innerUuid为List内部类结构UUID,type为List或PageResult且innerType为Enum、Eo、Dto时传入对象uuid,innerType为Qto时传入读方案uuid,Bto时传入写方案uuid
- 生成代码: RPC在service层生成类文件和实现函数,包含DTO自动生成的RPC如UserDtoService.getById、读写方案自动生成的RPC如UserDtoQueryService.queryByListQto、UserBOService.createUser、自定义RPC如UserCustomService.customMethod。公开RPC才能被其他模块使用,订阅后生成RpcAdapter适配器,其他模块通过RpcAdapter调用。RpcAdapter的命名方式严格遵守以下格式:${ClassName}In${ModuleName}RpcAdapter,以下为示例:Order模块订阅User模块的UserDtoService.getById,在Order模块生成UserDtoServiceInOrderRpcAdapter.getById方法,Order模块代码必须通过@Resource private UserDtoServiceInOrderRpcAdapter userDtoServiceInOrderRpcAdapter;注入适配器后调用。必须注意:变量命名必须是类名首字母小写,禁用其他变量名
- 修改建议: 建议修改RPC方法,不建议修改RPC方法签名、适配器内容
2.14 应用程序接口 (API)
- 定义与用途: API定义对外HTTP接口
- 创建方式: 通过TOCO创建,需指定参数和返回值
- 存储特点: TOCO只存储URI和方法签名,不存储执行逻辑。了解实现需阅读对应代码
- 关键配置: uri(/api/${moduleName}/xxx,如/api/user/create,全局唯一)、类名(以Controller结尾)、方法名(驼峰小写)、请求参数、返回值。分页查询参数为Qto时,已包含from、size、scrollId属性,无需额外参数
- 参数类型: 只能为QTO、BTO、EO、Enum、基本类型,可为单值或列表。对象类型优先用QTO、BTO。禁用DTO和自定义结构
- 返回值类型: 脚手架自动包装返回值(code、message、data)。只能为VO、Enum、EO、基本类型,可为单值或列表。禁用DTO、QTO、BTO、自定义结构。对象类型优先用VO
- json结构: API用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为返回结构。参数和response结构相同:name为参数名;type为参数类型(Boolean,String,Integer,Long,Float,Double,BigDecimal,Date,ByteArray,Enum,Eo,List,PageResult,Vo,Qto,Bto,Void,参数不能为Void和PageResult,无返回值设为Void,分页结果设为PageResult且innerType必为Vo,对应VSQueryResult);description为描述;typeUuid为类结构UUID(type为Enum、Eo、Vo时传入对象uuid,Qto时传入读方案uuid,Bto时传入写方案uuid);innerType为List内部类型(type为List时包含);innerUuid为List或PageResult内部类结构UUID(type为List或PageResult且innerType为Enum、Eo、Vo时传入对象uuid,innerType为Qto时传入读方案uuid,Bto时传入写方案uuid)
- 代码产物和修改建议
- 生成代码: entrance层生成Controller及对应API方法
- 修改建议: 建议修改API方法实现内容,禁止直接修改API方法签名、URI。修改API定义需通过修改API设计元素实现
2.15 流程服务(FunctionFlow)
- 定义与用途: TOCO将复杂业务拆解为流程节点,基于业务逻辑内聚性合并功能,构建工作流式逻辑流程,提升代码可维护性。内嵌流程引擎执行生成代码。
- 使用场景:
- API/RPC涉及写服务超过3个时必须使用
- 用户要求使用时
- 节点封装:
- 以数据内聚为首要考虑,一个节点围绕一个核心写服务,包含读服务、数据处理转换、附属功能。除条件节点、入参校验、数据返回节点外,每节点至少包含一个写服务。
- 配置: 名称(小写字母+下划线),拆解复杂业务逻辑。简单业务流程无需使用。
- 创建方式:
- 流程非逻辑伪代码,不表达全部细节。相关内聚、相似功能封装到一个节点内。如用户注册:创建账号、用户信息、发送通知属于创建用户相关信息,无逻辑分叉,可内聚在一个节点。
- 节点间参数和返回值通过统一上下文Context传递(编码时按需修改Context文件添加字段)。
- 用JSON表达逻辑流。节点类型:顺序节点、条件节点、选择节点、开始节点。节点间通过有向边连接,表示执行方向。从开始节点可达各节点。
- 边定义:顺序节点多条入边多条出边(一条出边为下个执行节点,多条为并发执行分支);条件节点多条入边2条出边(True/False分支);选择节点多条入边多条出边(每条为选择路径);开始节点一条出边无入边。
- 条件节点只封装判断逻辑,返回TRUE/FALSE;循环节点只封装判断逻辑,返回TRUE/FALSE;选择节点只封装分支选择逻辑,返回下游分支节点名称。
- 终止流程:直接抛异常。
- 校验逻辑放在顺序节点中,失败则抛异常。
- 节点可复用,相同功能可抽取封装到一个节点。
- 表达方式:
- JSON表达,Schema如下:
{ "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字符" } } } }, "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退出循环"} } } } } }- 示例:用户注册流程
{ "moduleName":"user", // 该流程所属的模块 "name":"user_register", // 定义功能 "description":"注册用户", // 详细功能描述 "nodes": [ // 包含节点 { "name":"start", "type":"START_NODE", "description": "起始节点" }, { "name":"check_user_registed", "type":"CONDITION_NODE", "description":"校验入参合法性;判断昵称是否重名;判断用户是否已注册" }, { "name": "create_user", "description":"创建用户,发送'用户创建'消息, 给用户发送欢迎通知", "type": "PROCESS_NODE" }, { "name":"return_user_info", "description":"返回用户信息,返回推荐给用户可能感兴趣的好友", "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" } ] }
- 示例:用户注册流程
- JSON表达,Schema如下:
- 代码产物和修改建议
- FlowConfig
- 生成产物: 每个模块service层生成Java类,注册模块下所有流程到执行器
- 命名: ${moduleName}FlowConfig
- 职责: 应用启动时注册模块内所有流程服务到执行器
- 路径:
**.service.flow
- Service
- 生成产物: service层${moduleName}FlowService中生成流程入口函数
- 命名:
public void invoke${functionFlowName}(${functionFlowName}Context context) - 职责: 代码逻辑中使用该流程的调用入口
- 路径:
**.service - 标识: 函数注解@AutoGenerated,uuid规则:${FunctionFlow的uuid}|FLOW|METHOD
- FlowNode
- 生成产物: service层生成Java类,注意开始节点(StartNode)不生成
- 命名: ${nodeName}Node
- 入口函数:
public void process() - 职责: 封装内聚性业务逻辑
- 路径:
service.flow.node.${functionFlowName}
- FlowContext
- 命名: ${functionFlowName}Context
- 职责: 流程节点间参数传递(出参和入参),实现业务逻辑时按需在此类中添加字段
- 路径:
**.service.flow.context - 标识: 类注解@AutoGenerated,uuid规则:${FunctionFlow的uuid}|FLOW|CONTEXT
- 示例:
- 用户登录:UserFlowService中生成invokeLoginFlow函数,通过流程框架根据流程定义调用LoginNode,LoginNode封装用户登录逻辑,LoginFlowContext封装登录参数和结果。
- 修改建议: 不修改service函数和FlowConfig,可修改FlowContext添加/修改出入参数,修改FlowNode中具体业务逻辑。
- FlowConfig
2.16 自定义查询
- 读方案无法满足需求时,使用自定义查询
- 自定义查询使用复杂sql实现业务功能
- 数据访问层使用MyBatis、MyBatisPlus实现
- 自定义查询时框架不自动生成代码(需要手动编写全部代码)
- 各层代码位置严格遵守3.2 项目结构与导航,必须有mapper层、service层、DO对象、DTO对象,API返回数据必须有VO对象
2.17 领域消息(DomainMessage)
- 定义与用途: TOCO支持领域消息,通过创建和订阅领域消息,可以监听聚合对象的状态变化(创建、删除、更新),TOCO会自动生成消息的发送逻辑
- 关键配置: 名称(小写字母+下划线), 由于消息是全局可见,所以TOCO限制了名称项目级别不能重复; 注意 在监听状态变化的时候不要命名为某个字段的变更,比如 order_payed, 也不要按照业务语义命名,例如order_payed_complete,因为实际上是order表的任意变更都会受到消息,因此命名为order_changed或order_updated更准确的表达了消息的触发语义; 同理对于create事件,一般命名为 xxx_created;对于delete时间,一般命名为 xxx_deleted
- 与聚合的关系: 每个聚合下可以定义多个领域消息,每次可以监听聚合下面其中一个实体的状态变更
- 如何创建/生成: 创建领域消息,需要先确定聚合,然后确定需要监听的实体以及监听的变更类型
- 最佳实践 如果只是字段区别,应该修改现有的领域消息,增加相应的字段即可,而不是新创建一个领域消息;
- 领域消息的定义表达
- 以Json表达,Json Schema 定义如下:
{ "type": "object", "required": ["name","description", "bo", "entity", "action","fields"], "properties": { "name": {"type": "string", "description": "消息名称,单词之间使用下划线分割,总称不超过32个字符,一个模块的领域消息名称不能重复"}, "description": {"type": "string", "description": "消息描述,不超过128个字符"}, "bo": {"type": "string", "description": "聚合名称"}, "entity": {"type": "string", "description": "监听的实体名称,必须归属于聚合"}, "fields": {"type": "array", "description": "指定消息里返回的entity的字段对应的字段名称","items": {"type": "string", "description": "字段名称"}}, "action": {"type": "string", "description": "监听的变更类型,可选值:create、update、delete"}, "delayInSeconds": {"type": "integer", "description": "延迟时间,单位为秒, 如果希望消息延迟消费,则设置该字段, 例如: 订单下单后5分钟未支付,触发一个消息,则可以监听订单的创建状态,并且设置延迟时间5分钟,这样消息会在5分钟后被消费"}, "uuid": {"type": "string", "description": "消息的uuid,在更新领域消息时传入,创建的时候不传"} } } - 代码产物和修改建议
- 发送逻辑 会在相应聚合的实体中通过hibernate的监听器中实现消息的发生逻辑,不能修改
- 生成产物:
- 消息载体: 在manager层生成一个Java类,封装消息体
- 命名规则: 类名为${domainMessageName}Mo
- 职责: 消息内容载体
- 类路径:
**.manager.mo - 唯一标识符位置: 其对应的标识符在类注解@AutoGenerated中指定, uuid规则: ${DomainMessage在TOCO中的uuid}|DMO|DEFINITION
- 例子
用户聚合,包含了user实体, 用户实体有 user_id, user_name 字段,那么创建一个领域消息,名称为user_created,描述为用户创建成功,聚合名称为user,实体名称为user,监听的变更类型为create,返回的字段为user_id,user_name。则会生一个类:
UserCreatedMo {Long userId;String userName;};特别的,对于update的监听,生成消息里会包含字段的前值,命名以old结尾, 例如:UserUpdatedMo {String userName ; String userNameOld;}
2.18 普通消息 (CommonMessage)
- 定义与用途: TOCO支持普通消息,普通消息可以自由定义消息内容。定义消息后,TOCO可以生成消息体,以及消息的发送模板;只需调用这个发送函数,即可发送消息; 消息队列的创建配置等工作TOCO自动完成
- 关键配置: 名称(小写字母+下划线), 由于消息是全局可见,所以TOCO限制了名称项目级别不能重复;
- 普通消息的定义表达
- 以Json表达,Json Schema 定义如下:
{ "type":"object", "properties": { "name":{ "type": "string", "description": "消息名称,单词之间使用下划线分割,总称不超过32个字符,一个模块的领域消息名称不能重复"}, "description": { "type": "string","description": "消息描述,不超过128个字符"}, "uuid":{ "type": "string", "description": "唯一标识符,创建时为空,更新时必填"}, "moduleName":{ "type": "string", "description": "所属模块,创建时必填,更新时可空"}, "delay": {"type": "boolean","description": "是否是延迟消息,如果是延时消息,在生成的的发送消息函数里添加延迟时间参数"}, "transactional": {"type": "boolean","description": "是否是事务消息,如果是事务消息,消息的发送和外层事务保持一致"}, "fieldList":{ "type":"array", "description": "消息属性列表", "items":{ "type": "object", "properties":{ "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"} }, "required":[ "name","type"] } } }, "required":["name","description"] }
代码产物和修改建议
- 消息载体类: 在manager层生成一个Java类,封装消息体
- 命名规则: 类名为${commonMessageName}Mo
- 职责: 消息内容载体
- 类路径:
**.manager.mo - 唯一标识符位置: 其对应的标识符在类注解@AutoGenerated中指定, uuid规则: ${CommonMessage在TOCO中的uuid}|MO|DEFINITION
- 发送类 在service层生成一个Java类,封装消息发送逻辑
- 命名规则: 类型名为 ${commonMessageName}MoMessageSender
- 职责 封装发送函数,作为消息发送的入口
- 类路径:
**.service.mq.producer - 唯一标识符位置: 其对应的标识符在类注解@AutoGenerated中指定, uuid规则: ${CommonMessage在TOCO中的uuid}|MO|PRODUCER
- 例子
- 在用户注册成功后,需要给用户发一封邮件,可以定义个普通消息,名称为:user_registered, 包含了字段: long user_id, String nick_name, 生成代码后,会生成 类:
UserRegisteredMo {Long userId;String nickName;}和一个发送类UserRegisteredMoMessageSender {void send(UserRegisteredMo message);}
- 在用户注册成功后,需要给用户发一封邮件,可以定义个普通消息,名称为:user_registered, 包含了字段: long user_id, String nick_name, 生成代码后,会生成 类:
2.19 订阅消息 (SubscribeMessage)
- 定义与用途: TOCO支持订阅消息(普通消息和领域消息), 订阅消息后生成代码会生成消息消费的模板代码, 后续只需在对应的模板代码里填写业务逻辑,而无需关注如何订阅消息的代码逻辑 注意在TOCO中,消息订阅按照模块独立订阅:一个消息可以被多个模块订阅,同一个消息在一个模块中只能订阅一次
- 订阅消息的定义表达
- 以Json表达,Json Schema 定义如下: 通过msgId或者msgName指定订阅的消息
{ "type": "object", "required": ["moduleName"], "msgId": {"type": "string", "description": "消息id"}, "msgName": {"type": "string", "description": "消息名称"}, "moduleName": {"type": "string", "description": "模块名称,指定消息被订阅到哪个模块"} } - 代码产物和修改建议
- Consumer类
- 命名规则: 在指定模块的的service层生成一个Java类,封装了消息的消费类入口
- 命名规则: 类名为${messageName}Consumer
- 职责: 消息的消费类入口
- 类路径:
**.service.mq.consumer
- MsgHandler函数
- 命名规则: 在Consumer类中生成一个函数,封装了消息的消费逻辑的函数入口
- 命名规则: 函数名为handleMessage
- 职责: 消息的消费函数入口,该函数以对应的消息作为参数
- 唯一标识符位置: 其对应的标识符在函数@AutoGenerated中指定, uuid规则: ${模块id}_${DomainMessage在TOCO中的uuid}
- 修改建议
- handleMessage函数中实现业务逻辑, 如果消费成功,返回ture; 处理失败返回false, 消息会被多次投递
- 如果消息要实现幂等消费,则需要在handleMessage函数上添加@Transactional注解,同时在Consumer类中覆盖父函数 @Override boolean useDBIdempotentWithTransactional(){return true;}, 开启幂等机制; 对于监听实体状态变化的事件,因为实体的任何字段变更都会触发事件,因此在消费消息的时候有些情况需要根据具体的字段值进行过滤, 例如:监听订单支付状态, 创建的消息叫做 order_changed_mo,包含了status字段,那么仅当status_older!=payed && status=payed 为true的时候表示是订单支付完成的消息。
- Consumer类
3 代码生成说明
3.1 技术栈 Java、SpringBoot、MyBatis-plus(读取)、Hibernate(写入)
3.2 项目结构
- 多模块SpringBoot项目,子模块在
/modules目录下。子模块路径必须从modules开始,如:modules/module1/src/main/java... - 根目录的entrance默认已经依赖了所有子模块的entrance模块,无需额外引入pom配置。
项目根目录
|──main_module
│ ├── pom.xml # 项目级pom配置
│ ├── common # 项目级公共模块
│ │ ├──config/ # 中间件配置
│ │ ├──constants/ # 项目常量
│ │ ├──enums/ # 项目枚举
│ │ ├──redis/ # Redis配置
│ │ ├──response/ # 返回结果封装
│ │ └──utils/ # 项目工具类,存放全项目复用的、或多模块复用的工具类,如OSSUtils、UserTokenUtils等
└───├── entrance/ # 项目入口,引用了项目中的所有模块,通常适合放一些全局的Filter、Aspect等,如AuthFilter、LogAspect等
│ ├──pom.xml # 项目级pom配置
│ └──AppApplication.java # 启动类
└── modules # 子模块列表
└── module1/ # 子模块1
├── pom.xml # 子模块1的pom配置
├── common/
│ ├──pom.xml # common的pom配置
│ ├──constants/ # 模块常量
│ ├──utils/ #模块工具类
│ └──enums/ # 枚举
├── entrance/web/src/main/java/com/{project_name}/{module_name}/entrance/web/
│ ├──pom.xml # entrance的pom配置
│ ├──controller/ # API定义
│ ├──converter/ # DTO转VO
│ ├──vo/ # VO结构
│ └── query/ # 读方案转VO
│ ├── assembler/ # VO数据填充
│ ├── collector/ # 读方案返回的id数据展开成完整对象
│ └── executor/ # 调用Service的度方案实现,同时调用collector和assembler,返回最终的VO
├── manager/src/main/java/com/{project_name}/{module_name}/manager/
│ ├── bo/ # 聚合对象定义
│ │ └── base/ # 聚合对象的基类
│ ├── dto/ # DTO定义
│ ├── mo/ # 消息定义 (普通消息和领域消息)
│ ├── converter/ # 复杂Dto(非BaseDto)组装
│ ├── facade/ # 调用其他模块的RPC适配器,包含RpcAdapter,如UserDtoServiceInMeetingRpcAdapter,表示从meeting模块调用user模块中方法
│ └── impl/ # DTO查询实现
├── persist/src/main/java/com/{project_name}/{module_name}/persist/
│ ├── eo/ # 值对象定义
│ ├── dos/ # 数据库表映射
│ ├── qto/ # 读方案数据库查询
│ └── mapper/ # MyBatis Mapper
└── service/src/main/java/com/{project_name}/{module_name}/service/ # BOService(包含某个聚合下所有写方案生成的方法)、 DtoService(包含DTO生成的预定义方法)
├── mq/producer/ #普通消息的生成者
├── mq/consumer/ #消息的消费者
├── bto/ # 写方案入参定义
├── converter/ # 对返回的BaseDto按需进行二次扩展
├── query/ # 查询方案的service层入口,调用persist层的查询实现
└── base/ # BOService基类
模块依赖层级:entrance -> service -> manager -> persist,各层都依赖common
3.3 查找路径
- API查找 →
modules/{模块名}/entrance/web/controller/ - DTO查找 →
modules/{模块名}/manager/dto/ - Service查找 →
modules/{模块名}/service/ - 数据层查找 →
modules/{模块名}/persist/
3.4 特殊注解
自动生成的类和方法有@AutoGenerated注解:
locked属性:true表示不建议修改uuid属性:唯一标识,包含|字符表示特殊格式拼装
3.5 分层规则 TOCO严格遵照分层规则,必须严格遵守,否则会产生严重后果!
- VO以及相关的类属于entrance层
- Service禁止引用任何entrance层的类或代码
- Service层方法禁止返回VO,禁止调用任何entrance层方法(如VoQueryExecutor、VoConverter等)
4. TOCO 最佳实践
4.1 接口参数类型和返回值选择:
设计TOCO接口(API、RPC)时,先判断主要功能是读库还是写库,分析对应的读写方案及QTO、BTO。读场景优先使用QTO作参数;写场景优先使用BTO作参数。QTO、BTO不满足时,可增加基本类型、Enum、EO参数。必须遵循:a.DTO、VO不能作参数;b.QTO、BTO不能作返回值。
重要说明:
- 本节描述参数选择的优先级原则,非绝对限制
- API参数绝对限制见2.14章节
- "优先使用QTO/BTO"指在满足规范前提下,根据场景选择最合适的参数类型
4.2 并发数据保护最佳实践:
处理先读后更新场景时,为避免并发导致数据脏写,应充分利用BoService校验功能。例如:账户扣钱场景,避免并发扣除导致余额不足,传统raw sql用 update account set balance = balance - amount where user_id = xxx and balance > amount 的where条件保护;在TOCO中,应该:1、写方案中使用incr字段。2、在boService中添加保护代码 if(userBo.getBalance() >= 0) 或通过userBo业务不变性(聚合校验)添加 balance>=0 校验。
</TOCO知识库>