----------------------------------------------------------------------------- ### **1. TOCO 平台概览:** - **1.1 平台简介:** TOCO是一款重视软件设计及核心代码自动生成的专业研发平台。其基于DDD、分层设计、CQRS等经典研发理论,从数据库到API全部覆盖,可显著提升开发设计和编程效率,帮助开发团队实现更高的质量和生产力 - **1.2 核心价值/目标用户:** 从软件工程理论出发,提供软件设计能力,设计结果可直接转换为标准格式的代码,提升编码一致性及效率 - **1.3 主要特性概览:** 可视化设计、模型关联、多人协作、代码生成器等 ### **2. TOCO 设计元素:** #### **2.1 模块 (Module)** - **定义与用途:** 在TOCO中,我们将系统领域细分为具体的模块,映射为Java工程中的module。这些模块代表了系统的叶子子域,每个模块负责特定的功能。模块划分有助于系统的可维护性和可扩展性,并能提高开发效率和代码质量 - **关键配置:** 名称(小写英文字母+下划线,如meeting,user_detail,禁止加任何固定后缀,全局唯一),描述 - **与其他元素关系:** 下面的每种设计元素都属于一个模块 - **代码产物:** 每个Module会单独生成一个Java Module:项目路径/modules/模块名,内部采用了entrance、service、manager、persist、common分层结构 #### **2.2 枚举 (Enum)** - **定义与用途:** Enum用来表达一些常量值的集合,可被其他模块使用,可被用来做为字段的类型 - **关键属性/配置:** 名称(以_enum结尾,全局唯一),枚举值列表(全大写字母+下划线) - **与其他元素关系:** 枚举可以作为其他对象(Entity、Dto、Vo、Bto、Qto、Eo)的字段类型使用。 - **Enum设计元素的表达:** - 以Json格式表达,json schema 定义如下 ```json { "type": "object", "properties": { "name": { "type": "string","description": "名称,使用英语表达,单词之间使用下划线分割,总长度不能超过32个字符"}, "uuid": { "type": "string","description": "计元素(Enum)在TOCO中的唯一标识符,在创建枚举的时候,该字段为空; 在更新时,该字段不能为空"}, "description": { "type": "string", "description": "描述这个枚举的具体含义,介绍这个枚举的用途,控制在128个字符以内"}, "moduleName": { "type": "string", "description": "指定该设计元素(Enum)所属的模块,在创建的时候该字段不能为空,在更新的时候,该字段可以为空"}, "values": { "type": "array","description": "枚举值列表", "items": {"type": "string","description": "枚举值,使用英语表达, 单词之间使用下划线分割,总长度不能超过32个字符"} } }, "required":["name","description"] } ``` * **代码产物和修改建议** * **生成产物**:在common模块中生成一个Java类 * **职责:** 表达Enum的数据结构 * **命名规则**:类名以Enum结尾 * **类路径:** 位于 ```**.common.enums``` 包路径下 * **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Enum在TOCO中的uuid}|ENUM|DEFINITION - **生成代码:** Enum会在common层生成Enum文件,如StatusEnum - **修改建议:** 不建议修改 #### **2.3 值对象 (Eo)** - **定义与用途:** EO为一种POJO对象结构,可被其他模块使用,可被用来做为实体字段的类型。 - **关键属性/配置:** 名称(以_eo结尾,全局唯一)。EO的字段类型只能为基本类型(含List)、EO、Enum,其他类型不允许。 - **与其他元素关系:** 可以作为其他对象(Entity、Dto、Vo、Bto、Qto)的字段类型使用,同时一个Eo中可以嵌套其他EO作为字段类型。 - **Eo设计元素的表达:** - 以Json格式表达,Json Schema定义及如下 ```json { "type":"object", "properties": { "name":{ "type": "string", "description": "名称,使用英语表达,单词之间使用下划线分割,总长度不能超过32个字符"}, "description": { "type": "string","description": "描述,描述这个数据结构的具体含义,介绍这个数据结构的用途,控制在128个字符以内"}, "uuid":{ "type": "string", "description": "该设计在TOCO中的唯一标识符,在创建EO的时候,该字段为空; 在更新时,该字段不能为空"}, "moduleName":{ "type": "string", "description": "指定该设计元素所属的模块,在创建的时候该字段不能为空,在更新的时候,该字段可以为空"}, "fieldList":{ "type":"array", "description": "定义Eo的属性字段列表", "items":{ "type": "object", "properties":{ "name": { "type": "string","description": "属性字段名称,使用英语表达,单词之间使用下划线分割,总长度不能超过32个字符"}, "uuid":{ "type": "string","description": "当字段类型是枚举、值对象的时候,该字段不能为空,表示该字段对应的枚举或者值对象" } , "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": "当type是List的时候,表示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在TOCO中的uuid}|EO|DEFINITION - **修改建议:** 不建议修改 #### **2.4 实体关系 (ER / Entity)** - **定义与用途:** 实体及其关系。一个实体一般对应一个数据库表,关系为实体间的外键依赖关系 - **关键属性/配置:** 实体中包含名称、字段、字段类型、主键、索引等,关系分为1:1和1:N关系 - **与其他元素关系:** 实体关系是聚合的基础,也是DTO和VO的派生基础 - **代码产物和修改建议** - 结构定义 * **生成产物**:Java类,按照Mybatis-plus的要求生成 * **职责:** 按照Mybatis-plus的要求生成结构定义类文件 * **类路径:** 位于 ```**.persist.dos``` 包路径下 * **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Entity在TOCO中的uuid}|ENTITY|DEFINITION - Mapper * **生成产物**:在persist层生成Mybatis-plus的Mapper类 * **职责:** 提供Mapper给Mybatis-plus框架 * **命名规则**:类名以Mapper结尾(${entityName}Mapper) * **类路径:** 位于 ```**.persist.mapper.mybatis``` 包路径下 * **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Eo在TOCO中的uuid} - Dao接口 * **生成产物**:在persist层生成Dao接口 * **职责:** 提供Entity数据的查询接口,为service层提供数据访问入口 * **命名规则**:类名以Dao结尾(${entityName}Dao) * **类路径:** 位于 ```**.persist.mapper``` 包路径下 * **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Entity在TOCO中的uuid}|ENTITY|IDAO - Dao实现 * **生成产物**:在persist层生成Dao接口的实现类文件 * **职责:** 通过调用Mapper实现实现Dao接口 * **命名规则**:类名以DaoImpl结尾(${entityName}DaoImpl) * **类路径:** 位于 ```**.persist.mapper``` 包路径下 * **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Entity在TOCO中的uuid}|ENTITY|DAO - **修改建议:** 不建议修改 #### **2.5 聚合对象 (BO/业务对象)** - **定义与用途:** 在TOCO中,聚合对象是对一组密切关联的实体的封装。聚合对象从单一实体开始,这个实体我们称为聚合根,通过实体间关系,不断的顺序将其他实体按层级关系组装进这个聚合对象。聚合对象可以按实体表达为树形结构。聚合对象提供了这组实体的内存一致性视图,提供数据操作入口。由于写操作的内聚性,聚合对象只能在单一模块中组合,而且一个实体只能属于一个对象。同样如果有实体不在任何一个聚合对象中,TOCO将无法提供与之相关的写方法。 - **包含元素:** 聚合对象包括聚合根及其聚合下的其他子实体对象,例如,商品聚合ProductBO中,商品基本信息实体是ProductBO的聚合根,商品SKU实体、商品库存实体是ProductBO的子聚合对象。 - **关键配置:** 名称(${EntityName驼峰}BO结尾,如StaffBO),聚合根实体,聚合子对象实体。每个聚合必须包含一个聚合根 - **与其他元素关系:** 聚合是写方案的基础 - **代码产物和修改建议** - 综述 - 业务对象包含多个Entity,通过业务对象的嵌套组合表达了Entity之间的关系,如果一个业务对象包含了子对象,则会生成BO和BaseBO,BaseBO封装实体属性和关系,子类留给业务扩展逻辑; 如果是叶子节点(不存在子对象)的BO,则直接生成BO类文件,不生成BaseBO类文件 - BO * **生成产物**:在Manager层生成聚合对象类文件,符合Hibernate的标准 * **职责:** 定义聚合对象,多个聚合对象组合成层级结构实现充血模型,支持写链路上的数据变更,监听数据变更,支持数据校验 * **命名规则**:类名以BO结尾(${entityName}BO) * **类路径:** 位于 ```**.manager.bo``` 包路径下 * **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Entity在TOCO中的uuid}|BO|DEFINITION - **修改建议:** 建议修改BO中的validateAggregate或valid方法。不建议修改检验方法以外的其他代码,如果发现需求中有业务不变性校验,**注意** 上述的校验方法,在写方案内部由框架触发调用,而不是业务代码显式调用 - BaseBO * **生成产物**:对于存在子BO的聚合对象,封装不变的代码部分 * **职责:** 定义聚合对象,多个聚合对象组合成层级结构实现充血模型,支持写链路上的数据变更,监听数据变更,支持数据校验 * **命名规则**:类名以BO结尾(${entityName}BO) * **类路径:** 位于 ```**.manager.bo``` 包路径下 * **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Entity在TOCO中的uuid}|BO|DEFINITION - **修改建议:** 建议修改BO中的validateAggregate或valid方法,如果发现需求中有业务不变性校验;不建议修改检验方法以外的其他代码 #### **2.6 数据传输对象 (DTO)** - **定义与用途:** 在TOCO中,DTO表达某个Entity为基本,通过外键关系不断关联多个Entity的数据结构。DTO还隐式表达了数据的取数拼装,这种拼装符合外键关系。DTO分为BaseDTO和普通DTO,BaseDTO派生自Entity,包含Entity的所有字段,每个Entity有且仅有一个BaseDTO;普通DTO派生自BaseDTO,包含BaseDTO的所有字段,且可以增加扩展字段或自定义字段。注意DTO不能作为接口的参数,也不能作为HTTP API的返回值 - **如何创建/生成:** 对于每个Entity,TOCO会自动生成一个BaseDTO,命名为${Entity名字}BaseDto,如UserBaseDto,该BaseDTO包含了Entity的全部字段。除了BaseDTO,其他的DTO均需要手动以BaseDTO为根来创建。在TOCO中,必须要先判断需要的DTO是否为BaseDTO,如果是BaseDTO,则可通过Entity名称获取BaseDTO;如果不是BaseDTO,则需要通过DTO要表达的信息来创建DTO,如:会议及其议程信息。 - **关键配置:** 名称(BaseDTO以BaseDto结尾,其他DTO以Dto结尾,全局唯一)、根Entity、字段列表。DTO中的字段分为三种:a.继承Entity或BaseDTO的字段,和Entity及BaseDTO的字段类型一样;b.扩展字段,含正向替换和反向注入字段,类型为DTO或List;c.自定义字段,类型为基本类型、Eo、Enum、DTO类型。BaseDTO中一般包含Entity的全部字段,DTO中一般包含BaseDTO中的全部字段,不进行字段裁剪,可以根据外键关系扩展其他Entity(详见**字段扩展方式**),在明确无法扩展外部Entity的情况下,可增加对应的自定义字段。 - **字段扩展方式:**TOCO定义了一个DTO组装方法,适用于DTO通过外键关系替换/注入对应Entity的信息,对象化表达有外键关系的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; //会议结束时间 } ``` 又例如:需要会议和其候选会议室时,将Meeting表中的backupRoomid进行正向替换。 又例如:需要会议,且即需要其占用会议室,也需要候选会议室时,将Meeting表中的roomid,backupRoomid都进行正向替换。 同时TOCO还定义了“反向注入”这个行为,选定一个表,如果有另外的表到前表有外键,选择和需求相关的具体的外键属性在选定表中增加一个以另外表为根的组合对象(当外键关系是1:1时)或者组合对象的列表(当外键关系是n:1时)。需求是:“获取会议室,和占用它的会议信息”,需要选定MeetingRoom,那么基于另外的表Meeting中存在字段roomid,为到MeetingRoom的n:1关系外键。可以将List反向注入到MeetingRoom中,最终生成一个以Meeting为根的组装对象,生成: ``` MeetingRoomWithMeetingDto { Long id;// 会议室id String name;// 会议室名称 List meetingList { //反向注入的用该会议室的会议信息,是以会议为根的对象 Long id;// 会议id String title; //会议标题 DateTime startTime; //会议开始时间 DateTime endTime; //会议结束时间 } } ``` 这种“正向替换”和“反向注入”可以按需递归调用,去将多个互相之间有外键关系的对象组装成最终对象。例如,还有另外一张表MeetingAgenda到Meeting有n:1的外键,和另外一张表AgendaAttendance到MeetingAgenda有n:1外键。那么如果我要去组装以Meeting开始,包含MeetingRoom, MeetingAgenda, AgendaAttendance的组装对象,首先发现MeetingRoom是可以正向扩展到Meeting的,反向注入MeetingAgenda,而AgendaAttendance需要先反向注入到MeetingAgenda中。 - **TOCO中DTO的json结构描述:** 在TOCO中,DTO使用一个json结构表示,该结构可用于理解DTO的含义,或作为创建、更新DTO工具的参数。部分字段的含义为:dto的uuid为唯一标识,如果需要创建DTO,则设置为null;如果需要复用,则填入其uuid。expandList为正向替换,reverseExpandList为反向注入,customFieldList为自定义字段。expandListList中,foreignKeyInThisEntity为正向替换对应的本表外键字段的名字,dtoFieldName为正向替换之后给该字段的起的新名字;reverseExpandList中,foreignKeyInOtherEntity为反向注入对应的他表外键字段的名字,dtoFieldName为反向注入之后给该字段的起的新名字;customFieldList中,uuid为自定义字段特有的UUID,创建DTO的时候不需要填入,因为TOCO会自动为其分配UUID,更新DTO的时候需要传入,用于定位需要更新的自定义字段;typeUuid参数对应类结构的UUID,当type为Enum、Eo时包含该字段;innerType为List内部类型,当type为List时包含该字段;innerUuid为List内部类结构的UUID,当type为List且innerType=Enum、Eo时包含该字段。示例如下: - meeting_with_room_dto ```json { "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 ```json { "dto": { "uuid": "d05c7b3d-1c92-45a1-2113-a01b245813c1", "name": "meeting_room_with_meetings_dto", "description": "会议室详情,包含会议室信息,以及其中的会议信息", "fromEntity": "meeting", "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,TOCO会自动其根Entity的唯一索引生成一些预定义的RPC方法及实现,预定义方法不仅获取了根Entity的数据,还通过RPC调用的方式**自动**获取了**所有扩展字段**的数据,并对数据进行了拼装处理。如实体user有unique index(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 在TOCO中通常使用以下步骤判断使用哪种方式: - 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)** - **定义与用途:** 在TOCO中,VO表达某个BaseDTO(如果用户指明派生源,也可使用其他DTO)为派生源,通过外键关系不断关联多个BaseDTO的数据结构。VO用于在视图层与前端之间进行数据传输,往往被当做HTTP API的返回值、或读方案的返回值使用,由服务端返回至前端。注意VO不能作为接口的参数,也不能作为RPC的返回值。 - **关键配置:** 名称(以Vo结尾,全局唯一)、根Entity、派生源、字段列表。VO中的字段分为三种:a.继承DTO的字段,如果DTO中的字段为基础类型或EO、Enum,则VO中的继承字段和和DTO的字段类型一样,如果DTO中的字段为DTO或List类型,则继承字段为字段对应的DTO类型派生出的VO或List类型;b.扩展字段,含正向替换和反向注入字段,类型为VO或List;c.自定义字段,类型为基本类型或VO类型。VO中的字段来源于DTO,可以根据页面需要将无用字段进行裁剪,可以根据外键关系扩展其他BaseDto(详见**字段扩展方式**)。如果在派生源中没有合适字段,且明确无法通过外键扩展外部BaseDto的情况下,可增加对应的自定义字段 - **继承字段类型转换:** VO只和其派生源VO之间有转换关系。当一个VO派生自某个DTO,且该VO继承了派生源DTO中的DTO类型字段(假设为DTO-A),此时VO中对应的继承字段类型必须为VO(假设为VO-A),且**VO-A也必须派生自DTO-A**,如此才能维持VO和DTO之间的转换关系 - **与DTO的区别 (在TOCO语境下): **DTO用于服务层传输,通常作为RPC的返回值,与数据模型更近,复用性较强;VO用于视图层传输,通常作为API的返回值,与UI展示更为接近,可裁剪掉不需要的DTO中的冗余字段,复用性较弱。 - **如何创建/生成:** VO通常由某个BaseDTO以及外键关系为基础派生,也可以直接创建和DTO无关、内部全为自定义字段的VO(尽量少用,只为应对某些特殊页面,需要组装一组完全无关的返回数据的场景)。 - **根VO和子VO:**TOCO中的VO分为两种:1.根VO,指最外层的VO结构,需要经由TOCO创建,有uuid作为唯一标识,根VO可被其他根VO或子VO引用;2.子VO,某个根VO的内部嵌套VO,通过外键关系关联BaseDTO之后由TOCO自动创建,只附属于某个根VO,只能被这一个根VO引用,且没有uuid。所以在TOCO中,我们需要描述VO要的字段、扩展关系,并通过**创建根VO**的行为使TOCO**自动创建**其子VO,或引用其他根VO,即可完成一个复杂嵌套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关系。即多个会议室会占用同一个会议室。 通过“正向替换”这个行为,可以组装出如下VO: ``` 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 meetingList{ //反向注入的用该会议室的会议信息,TOCO自动生成的内部VO,派生自MeetingBaseDto Long id;// 会议id String title; //会议标题 DateTime startTime; //会议开始时间 DateTime endTime; //会议结束时间 } } ``` - **TOCO中VO的json结构描述:** 在TOCO中,VO使用一个json结构表示,该结构可用于理解VO的含义,或作为创建、更新VO工具的参数。部分字段的含义为:expandList为正向替换,reverseExpandList为反向注入,extendFieldList为来自派生源DTO的字段,customFieldList为自定义字段。expandListList中,foreignKeyInThisEntity为正向替换对应的本表外键字段的名字,voFieldName为正向替换之后给该字段的起的新名字;reverseExpandList中,foreignKeyInOtherEntity为反向注入对应的他表外键字段的名字,voFieldName为反向注入之后给该字段的起的新名字;customFieldList中,uuid为自定义字段特有的UUID,创建DTO的时候不需要填入,因为TOCO会自动为其分配UUID,更新DTO的时候需要传入,用于定位需要更新的自定义字段;typeUuid参数对应类结构的UUID,当type为List且innerType=Enum、Eo时会包含该字段;extendFieldList中,name为继承字段的名称,如果要继承的DTO字段不是DTO类型,则VO中的字段类型将与DTO字段一致,如果DTO中的字段为DTO或List类型,由于VO中的字段类型不能为DTO,**必须**将该DTO转换为VO才可作为VO字段使用,所以extendFieldList中会有**一个vo结构**,用来表示该字段DTO类型派生出的VO定义,注意:该VO字段中的VO类型必须派生自继承的DTO字段类型! 示例如下: 系统中存在meeting_detail_dto ```json { "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字段 ```json { "vo": { "uuid": null, "name": "metting_with_room_vo", "description": "会议详情,包含会议室信息,以及会议室禁用列表", "rootVo": "metting_with_room_vo", "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)和基类 * **命名规则:** 实现类名以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)** - **定义与用途:** 在TOCO中,WO表达某个Entity为基本,通过外键关系不断关联多个Entity的数据结构。WO还隐式表达了数据的取数拼装,这种拼装符合外键关系. WO作为ReadPlan的查询上下文使用,所以在创建ReadPlan之前需要先创建WO对象。在理解一个ReadPlan的语义的时候需要以WO作为上下文。 如果对应的需求,在返回DTO|VO的时候需要对DTO|VO的列表属性进行过滤,则需要根据对应的DTO|VO的结构定义,扩展出对应的WO对象(需要过滤的字段名需要和DTO|VO的字段名保持一致),例如: 查询DTO ```java class MeetingDto { String meetingId; String meetingName; List 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": "扩展出来的字段名称"} } } } } } ``` - **如何创建/生成:** - **创建思路** 按照需要查询返回的DTO|VO的结构,构建出同构的WO对象(扩展和反向扩展的字段名保持一致),然后根据查询需求和过滤需求对WO进行二次裁剪或和扩展: - 去掉过滤和查询都不需要的扩展 - 补全查询或者字段过滤需要扩展 - **关键配置:** Wo中的字段分为三种:a.继承Entity的字段,和Entity的字段类型一样;b.扩展字段,含正向替换和反向注入字段,类型为WO或List; - **字段扩展方式:** TOCO定义了一个WO组装方法,适用于WO通过外键关系替换/注入对应Entity的信息,对象化表达有外键关系的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对应的候选会议室具体信息和本需求无关,不做任何替换。如下: ``` MeetingWithRoomWo { Long id;// 会议id MeetingRoomWo room { //正向替换该会议用的会议室信息,是一个以会议室为根的对象 Long id; //会议室ID String name;// 会议室名称 } Long backupRoomid; // 不做变化 String title; //会议标题 DateTime startTime; //会议开始时间 DateTime endTime; //会议结束时间 } ``` 又例如:需要会议和其候选会议室时,将Meeting表中的backupRoomid进行正向替换。 又例如:需要会议,且即需要其占用会议室,也需要候选会议室时,将Meeting表中的roomid,backupRoomid都进行正向替换。 同时TOCO还定义了“反向注入”这个行为,选定一个表,如果有另外的表到前表有外键,选择和需求相关的具体的外键属性在选定表中增加一个以另外表为根的组合对象(当外键关系是1:1时)或者组合对象的列表(当外键关系是n:1时)。需求是:“获取会议室,和占用它的会议信息”,需要选定MeetingRoom,那么基于另外的表Meeting中存在字段roomid,为到MeetingRoom的n:1关系外键。可以将List反向注入到MeetingRoom中,最终生成一个以Meeting为根的组装对象,生成: ``` MeetingRoomWithMeetingWo { Long id;// 会议室id String name;// 会议室名称 List 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 { "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)** - **定义与用途:** 在TOCO中,针对DTO和VO,读方案描述了如何基于查询对象从数据库获取DTO和VO列表数据,主要提供了两个能力 - 根据查询条件,返回符合条件的DTO或VO的id列表 - 根据字段过滤条件,对DTO和VO的列表字段数据进行过滤 - **注意** 不派生自DTO的VO不能创建读方案 - 读方案能力边界 - 读方案的的查询能力是sql的简化,实现了部分sql的查询能力,仅支持Sql的 count、exists、left join, not、in、like、between、and、or、not 以及 ( ) , 以及排序 order by, 以及分页 limit ,offset, 不能使用group by, having , avg 等函数 - 在使用exists 和 left join的时候只能通过外键扩展,其中如果外键是1:N的时候,只能使用exist, 如果是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) 合法; elect * from t left join t1 on t.id=t1.id 非法 - 在过滤字段的选择上,在使用exists的时候,在条件中只能选择后表的字段, 在使用 left join的时候,则可以使用前表和后表字段 例如: 实体 t (id,name) ,实体 t1 (id,name,t_id) ,t1存在列t_id为指向t的外键, 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 ? 合法 select * from t left join t1 on t.id=t1.id where t.name like ? 合法, select * from t left join t1 on t.id=t1.id where t1.name like ? 合法 - count语法的限制,count语法只能在最外层单独使用,如果使用了count语法,只能返回总的数量,不能返回其他字段 例如:select count(*) from t 合法, select count(*),t.name from t where t.name like ? 非法 - 在选择使用度方案的时候**必须**符合度方案能力编辑,如果需求在读方案的能力之外,必须使用 **2.16 自定义查询** 实现功能 - **特别注意** 对于根据主键,获取单个或者批量获取DTO或者VO,并且不需要对DTO、VO的列表字段数据进行过滤的需求,必须使用**DTO自动生成的预定义方法**,禁止使用读方案 - **关键配置:** 名称(小写字母+下划线,不要以read_plan结尾,全局唯一)、返回结构(DTO/VO,一个读方案**不能**同时返回多种DTO或VO)、查询条件的自然语言描述、是否生成计数方法、排序字段(如果选择不分页,则不需要)、过滤字段以及过滤条件。 - **与RPC、代码的关系:** 对于每一个返回DTO的读方案,TOCO会为每种分页方式自动生成一个RPC方法,其参数为对应的QTO,返回值为DTO列表;如果选择了生成计数方法,则还会在生成一个RPC,参数为QTO,返回值为符合条件的DTO数量。同样,对于每一个返回VO的读方案,TOCO会自动生成一个Java方法,其参数为对应的QTO,返回值为VO列表,方法内部逻辑已经由TOCO完全实现;如果选择了生成计数方法,则还会生成一个count方法,参数为QTO,返回值为符合条件的DTO数量,方法内部逻辑已经由TOCO完全实现 - **生成的读方案RPC** 的使用: 读方案生成的RPC属于该写方案所属的模块,其他模块如果需要调用该读方案,需要先订阅该写方案RPC,在生成代码后使用adapter调用该读服务;当前模块则可以直接调用该读方案对应的Service - **如何创建/生成:** 在创建读方案时,必须先调用工具创建或选择现有的**一种**DTO或VO作为返回值类型,然后再定义查询条件的自然语言描述(如:根据用户姓名、年龄、学校名称,查询用户列表),分页方式、是否生成计数方法、排序字段等。 - **排序** 对于返回列表数据的排序,排序规则和SQL的排序类似(通过指定字段以及升序|降序,字段可以组合;ReadPlan支持有**两种**方式: - **默认排序**:指定默认排序字段(不需要入参指定排序字段) - **自定义排序**:指定排序字段(需要入参指定排序字段),这种方式通常能很好的满足列表头动态指定排序的需求 - **排序字段的来源**:并不是查询字段中的所有字段都能作为排序字段,因为返回的根DTO|VO的id去重列表,所以排序字段只能来源自根WO(不包括列表WO属性: 比如MeetingWO包含了List agendaList属性, 那么agendaList不能作为排序字段),以及根WO扩展出来的非列表属性的WO的字段(比如MeetingWO包含了MeetingRoomWO,那么MeetingRoomWO的属性也能作为排序字段),**注意**:为了唯一确定是给按照那个字段进行排序,排序字段是一个从根节点到当前属性的路径,例如:ADto包含了BDto bDto,BDto具备属性name,如果指定对BDto的name字段排序,则字段路径为:bDto.name - 你需要提取需求中的查询部分信息,以输入的查询对象作为查询上下文件,构建一个查询语句 - 如果有列表属性过滤需求(如果没有filter,对每个列表属性都是放回全部数据,例如:MeetingDto的MeetingAgendaDtoList属性,如果不设置MeetingAgendaDtoList的filter,则返回该会议的全部议程信息):你需要提取需求中的过滤部分信息,以输入的查询对象作为查询上下文件,针对可过滤字段(列表属性)分别创建过滤条件, **注意** 过滤条件不能使用列表属性作为查询条件属性(即不能使用contains语法),为了唯一确定是给那个列表属性的指定过滤条件,属性字段是一个从根节点属性到当前属性的路径,例如:ADto包含了List bDtoList;BDto包含了List cDtoList,如果对cDtoList使用Filter,则字段路径为:bDtoList.cDtoList - 查询语法和过滤语法 - 基本语法格式是 属性名 操作符 变量或常量,变量表示了这个数据是外部传入的,用 #变量名为格式;如果是枚举类型常量,需要使用'符号包起来,例如: 男性枚举类型 'MALE' - 对于数值、时间类型属性有:!=, ==, >, <,<=,>=, in,notIn, isNullOrNot 这些操作符 - 对于文本类型属性有:like, isNotNull, isNull,!=, ==, in,notIn, isNullOrNot 这些操作符 - isNullOrNot操作符是一个三元操作符,用于根据入参判断过滤某个值是否为null。举例:user对象有gender字段,如果定义了语句 gender isNullOrNot #genderIsNullOrNot, 如果入参 genderIsNullOrNot为true,等价于条件 where gender is null; 如果入参#genderIsNullOrNot为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}] } boolean { @specialize[@name=Boolean] } @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 定义如下 ```json { "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个字符内"}, "generateCountApi": {"type":"boolean", "description":"是否需要生成计数接口"}, "supportPaginate": {"type":"boolean", "description":"是否需要分页"}, "supportUnPage":{"type":"boolean", "description":"如果不需要分页,一次性返回部数据,则返回true"}, "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语法"} } } }, "required":["name","description","query"] } }, "required":["qto"] } } ``` - **举例** - 上下文 ```java public class meeting_room { storey storey; // 楼层id String name; // 名称 room_type_enum room_type; // 会议室类型 List 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; // 位置id 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 } } ``` - 用户需求 获取一段时间未被使用的会议室(包含当前会议已选择的会议室),并且根据会议名称会议信息,最后分页返回。 - 对应的读方案定义 ```json { "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的参数,API接收到参数后可直接透传给内部的RPC进行调用 * **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定,uuid规则: ${ReadPlan在TOCO中的uuid}|QTO|DEFINITION * **内部结构:** QTO内部包含了如下分页相关的字段: ``` private String scrollId; //瀑布流分页时用作游标 private Integer size; //页码分页时用作每页记录的数量 private Integer from; //页码分页时用作开始的记录位置 ``` - **Dao** - **生成产物:** 在Dao层生成一个Java类 - **命名规则:** 类名以Dao结尾(${ReadPlanName}Dao) - **类路径:** 位于 ```**.persist.mapper```包路径下 - **职责:** 读方案对应的数据库查询方法 - **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定,uuid规则: ${ReadPlan在TOCO中的uuid}|QTO|DAO - **QueryExecutor** - **生成产物** 对于返回**VO**的查询方案,在controller层生成一个Java类,对于分页查询、不分页返回全量、瀑布流查询、计数都有后独立的函数实现 - 分页查询, 会生成一个 **Paged函数, 返回VSQueryResult - 不分页返回全量, 会生成一个query**函数,返回List - 瀑布流,会生成一个,**Waterfall函数,返回VSQueryResult - 查询数量, 会生成一个 **Count函数,返回Integer - **命名规则:** 类名以QueryExecutor结尾(${VoName}QueryExecutor) - **类路径:** 位于 ```**.entrance.web.query.executor```包路径下 - **职责:** 提供针对VO的查询入口,把QtoService返回的id数据转化成目标**VO** - **QueryService** - **生成产物** 对于返回**DTO**的查询方案,在service层生成一个Java类,对于分页查询、不分页返回全量、瀑布流查询、计数都有后独立的函数实现 - 分页查询, 会生成一个 **Paged函数,, 返回VSQueryResult - 不分页返回全量, 会生成一个query**函数, 返回List - 瀑布流,会生成一个,**Waterfall函数, 返回VSQueryResult - 查询数量, 会生成一个 **Count函数,返回Integer - **命名规则:** 类名以QueryService结尾(${DtoName}QueryService) - **类路径:** 位于 ```**.service.index.entity```包路径下 - **职责:** 提供对DTO查询入口,把QtoService返回的id数据转化成目标**DTO**,或返回符合条件的记录数量 - **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)** - **定义与用途:** 在TOCO中,QTO为读方案的查询参数结构,每个读方案会对应一个QTO,读方案调用方按照QTO的结构向读方案生成的RPC方法传入需要查询的实体字段值,完成对数据库的查询 - **如何创建/生成:** 在创建读方案后,TOCO会自动生成QTO作为该读方案传入的查询参数结构,无需单独创建 - **关键配置:** 名称(${ReadPlanNameQto},驼峰展示),查询字段列表(如idIs,nameLike, schoolNameLike等) - **与API的关系:** QTO通常可作为API的参数,API接收到参数后可直接透传给内部的RPC进行调用,注意QTO只用作读操作的参数,**禁止用作写参数结构** #### **2.11 写方案 (WritePlan,单聚合操作)** - **定义与用途:** TOCO针对写场景定义了一种写方案,所有对数据库的写操作都只能通过写方案实现(一个写方案只可以变更一个聚合的数据,无法同时操作多个聚合!)。写方案包含了对数据库表的写操作。每个写方案只能操作一个聚合内部的表,同时对一个聚合内表的操作尽量合并至一个写方案中(根据复用性、内聚性等方面具体情况具体分析)。注意写方案可以一次操作聚合内的多张表,如location和storey是同一个聚合,并且location和storey是**1:N**关系,location是父对象(聚合根),storey是子对象,那么我们可以创建一个写方案,同时更新单个聚合根location中的信息,以及操作其下的子表storey**列表**信息,如新增、删除、修改storey等;也可以创建一个写方案单独更新单个子表storey对象信息。 - **关键配置:** 名称(小写字母+下划线,不要以write_plan结尾,全局唯一),操作的聚合,聚合内的实体和字段,对每个实体的操作类型:CREATE(创建**单个**实体),UPDATE(更新**单个**实体), DELETE(删除**单个**实体), CREATE_ON_DUPLICATE_UPDATE(创建或者更新**单个**实体),FULL_MERGE( 批量更新**列表**数据,根据传入的列表数据,一条一条执行CREATE_ON_DUPLICATE_UPDATE的操作;并且删除掉老的列表中不在传入列表数据中的部分), PARTIAL_MERGE(批量更新**列表**数据,根据传入的列表数据,一条一条执行) - **与RPC的关系:** 对于每一个写方案,TOCO会自动生成一个RPC方法,其参数为写方案对应的BTO,返回值为本次操作的聚合根实体的主键值,内部只实现了对当前聚合的数据库操作 - **生成的写方案RPC** 的使用: 写方案生成的RPC属于该写方案所属的模块,其他模块如果需要调用该写方案,需要先订阅该写方案RPC,在生成代码后使用adapter调用该读服务;当前模块则可以直接调用该写方案对应的Service - **与聚合的关系:** 每个聚合下可以定义多个写方案,但每个写方案只能操作一个聚合内的表,无法同时操作多个聚合内的表 - **如何创建/生成:** 创建写方案时需要先选定对应的聚合,以及要操作的聚合内部的实体,然后确定对每个实体的具体操作类型 - 提取需求中的更新部分需求,根据上下文信息构建出一个写更新方案 - 上下文中输入了备选聚合对象的的定义,每个聚合对象定义了多个实体的嵌套关系 - 先明确聚合内实体的父子关系层级 - 根据需求提取出变更相关的名称 - 根需求提取出变更相关的描述 - 根据需求选择确定变更范围,选出一个最合适的聚合对象;注意: 最多只能选择一个聚合对象 - 根据需求在选定的聚合对象按照对象的粒度,选出所需变更对象 - 根据需求选定变更对象的字段;每个选定的对象至少需要选定一个字段,如果无法确定任何字段,默认选择全部字段, 字段必须明确指定名称,不能类似“所有字段”的模糊表达 - 每个对象上可以设置对应的一个操作,操作包括:CREATE(创建),UPDATE(更新), DELETE(删除), CREATE_ON_DUPLICATE_UPDATE(创建或者更新),FULL_MERGE( 批量更新列表数据,根据传入的列表数据,一条一条执行CREATE_ON_DUPLICATE_UPDATE的操作;并且删除掉老的列表中不在传入列表数据中的部分), PARTIAL_MERGE(批量更新列表数据,根据传入的列表数据,一条一条执行CREATE_ON_DUPLICATE_UPDATE的操作) - 列表属性可以批量操作 - 批量更新列表数据(FULL_MERGE或者PARTIAL_MERGE),则操作方案需要包含它的父对象 - 批量删除列表数据,则操作方案需要包含父对象,父对象的操作设置为UPDATE - 批量创建列表数据,则操作方案需要包含父对象,父对象的操作设置为UPDATE、或者是CREATE(当父对象也需要一起创建的时候) - 举例:meeting对象有List agdendaList属性,如果需要批量创建agenda,则需要同时设置metting对象和meeting_agenda对象,并且对meeting_agenda设置create操作;这是会生成一个Bto参数,其中包含了 List agendaList的属性,用于批量传递参数;反正,如果只设置了meeting_agenda的create操作,生成的Bto参数,只包含了meeting_agenda实体的属性,表示一次只能创建一条数据; 同理对于UPDATE、DELETE也是类似的规则 - FULL_MERGE和PARTIAL_MERGE只能用在列表属性上, 单值属性请使用CREATE_ON_DUPLICATE_UPDATE - 操作是 UPDATE、DELETE、CREATE_ON_DUPLICATE_UPDATE、FULL_MERGE、PARTIAL_MERGE的时候必须且只能指定一个唯一键(unique_key)(包括主键),用于确定对应的数据记录,主键对应的字段也要包含在fields字段中; - 唯一键(unique_key)的具体解释:在实体中,存在普通索引和唯一索引,这里的唯一键值的是唯一索引,由一个或多个字段组成 - 选定的对象层级不能跳跃;如果发生不连续,去掉不连续的叶子对象 - 注意父对象和下一层子对象的操作类型有如下限制,必须严格检查以下规则 - 如果父对象的操作是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 则可以设置为增量更新字段, 把值设置在incrFields属性中,和操作"UPDATE"、"CREATE_ON_DUPLICATE_UPDATE"、"PARTIAL_MERGE"或者"FULL_MERGE"搭配表示对该字段增量更新: 例如A实体的字段count类型是Long, 如果该字段被设置为增量跟新字段,最终的效果是A.count = A.count + ? ; 对于减少字段的值也用该方法表示,可以通过传入负值的入参达到减值的效果 - **写方案设计元素的表达**: 以json格式表达,json schema 定义如下: ```json { "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":"对选定的Bo的操作的字段列表" }, "incrFields":{ "type":"array", "items":{ "type":"string", "description":"增量字段名称"}, "description":"选定的字段中进行增量操作的字段列表" }, "required":["bo","action","fields"] } } } }, "required":["name","description","operations","bo"] } ``` - **例子** - **上下文:** ```java public class meeting {//会议聚合 //唯一键:(id)、(meeting_name) Long id; //主键id String meeting_name; //会议名称 Date start_time; //开始时间 Date end_time;//结束时间 List meetingAgendaList; class meeting_agenda { Long id;//主键 Date start_time;//开始时间 Date end_time;//结束时间 String title;//议程名称 List 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;//好友数量 } ``` - **需求:** 创建会议 - 对应的写方案定义: ```json { "name": "create_meeting", "description": "创建会议", "bo": "meeting", "operations": [ { "bo": "meeting", "action": "CREATE", "fields": [ "meeting_name", "start_time", "end_time" ] } ] } ``` - **代码产物和修改建议** - **BOService:** * **生成产物:** 在对应的聚合服务BOService里生成一个函数 * **函数命名规则:** 和写方案同名 * **职责:** 按需对入参进行转换,调用BaseBOService里的函数完成对聚合对象的操作, 实现对数据库的写操作; * **返回值:** 操作的聚合根实体记录的主键字段的值 * **BOService的类路径:** ```**.service``` * **唯一标识符位置:** 其对应的标识符在函数的注解@AutoGenerated中指定, uuid规则: ${WritePlan在TOCO中的uuid} - **BaseBOService** - **生成产物:** 在对应的聚合的BaseBOService里生成一系列函数,根据入参完成对聚合对象的变更 - **职责** 增对每个实体的生成增删改的函数,并且根据参数的结构以及聚合的结构,构建嵌套的调用逻辑,完成对一个聚合对象的变更,记录并且返回对应的变更情况 - **类路径:** ```**.service.base``` - **禁止** 修改该类 - **BTO (业务变更传输对象)** * **生成产物:** 一个Java类,以内部类的方式表示层级结构, **注意** BTO只能由写方案生成,不能凭空创建 * **命名规则:** 一个Bto结尾(${WritePlanName}Bto,驼峰展示) * **禁止** 修改该类 * **类路径:** 位于```**.service.bto```包路径下 * **职责:** 在**TOCO**中,BTO为写方案的参数结构,每个写方案会对应一个BTO,写方案调用方按照BTO的结构向写方案生成的RPC方法传入需要查询的实体字段值,完成对数据库的变更 * **唯一标识符位置:** 其对应的唯一标识符在类注解@AutoGenerated中指定, uuid规则: ${WritePlan在TOCO中的uuid}|BTO|DEFINITION - **例子:** - 创建用户以及用户设置的写方案create_user_and_setting,生成CreateUserAndSettingBto, 在用户的聚合(UserBO)对应的UserBOService中生成函数createUserAndSetting,该函数调用BaseUserBOService中生成的createUserAndSetting, 其中在BaseUserBOService中还生成了createUser和createSetting的函数, 一起完成了用户的创建和设置创建的逻辑。 - **修改建议:** - 不能修改BaseBOService中的函数,不建议修改BTO文件。建议在BOService中进行手动代码扩展,处理可能被复用的修改前后的逻辑,如修改数据库的前后值对比、或常被复用的校验逻辑(业务不变性校验逻辑除外)、需要经常在一个事务内执行的其他写操作等。 - 父类函数会返回一个BoResult类,此类记录了各个Bto和Bo实例的对应关系,以及各个Bto实例的操作结果, 可以通过以下接口获取。 例如:在创建用户的写方案中,BoService里的入参为CreateUserBto 例如:在创建用户的写方案中,BoService里的入参为CreateUserBto,用户id由数据库生成,如果需要返回创建用户id,那么通过boResult.getAddedResult(createUserBto).getBo().getId()可以返回新建的用户id ``` /** * 获取更新成功的bto结果 * * @return */ public UpdatedBto getUpdatedResult(final Object bto) /** * 获取成功插入的Bto的结果 * @param btoObj * @return */ public AddedBto getAddedResult(final Object btoObj) /** * 获取Bto对应的删除结果 * * @param btoObj * @return */ public DeletedBto getDeletedResult(final Object btoObj) ``` ``` public class UpdatedBto { //Bto 入参 private Bto bto; //Bto 对应的Entity,前项 private Entity entity; //bo,后项 private BO bo; } ``` ``` //记录删除记录的情况 @Setter @Getter public class DeletedBto { //Bto 入参 private Bto bto; //Bto 对应的Entity,前项 private Entity entity; } ``` ``` //记录Bto创建的前后值 @Setter @Getter public class AddedBto { //Bto 入参 private Bto bto; //bo 后项 private BO bo; } ``` #### **2.12 业务变更传输对象(BTO)** - **定义与用途:** 在TOCO中,BTO为写方案自动生成的参数结构,每个写方案会生成一个BTO。BTO为写方案选定的操作实体根据关系形成的树形集合,最外层为聚合根。写方案调用方按照BTO的结构向写方案生成的RPC方法传入需要操作的实体字段值,完成对数据库的写操作 - **如何创建/生成:** 在创建写方案后,TOCO会自动生成一个BTO作为该写方案传入的参数结构,无需通过TOCO创建。 - **关键配置:** 名称(${WritePlanName}Bto,驼峰展示),嵌套的树形实体和字段列表,BTO内部的字段全部都来自Entity。以下为一个示例: ``` class CreateUserBto { //对应实体user Long id; //来自于user.id String name; //来自于user.name List pictureList; class PictureBto { //对应实体picture String url; //来自于picture.url } } ``` - **与API的关系:** BTO通常可作为API的参数,API接收到参数后可直接透传给内部的RPC进行调用,注意BTO只用作写操作的参数,**禁止用作查询参数结构** - **复杂场景处理:** 对于涉及多个写方案的复杂API,可以: a. 创建一个主要的写方案,让其BTO作为API入参 b. 使用基本类型参数,在流程中转换为各个写方案的BTO c. 如果流程过于复杂,优先考虑使用基本类型参数 #### **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;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、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;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个字符" } } } }, "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", "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" } ] } ``` - **代码产物和修改建议** - **FlowConfig** * **生成产物:** 每个模块在service层的生成一个Java类,负责注册模块下的所有流程到执行器 * **命名规则:** 类名为${moduleName}FlowConfig * **职责:** 在应用启动的时候注册模块内的所有的流程服务到执行器 * **类路径:** ```**.service.flow``` - **Service** * **生成产物:** 在service层的以模块名为类名前缀的${moduleName}FlowService中生成一个流程的入口函数 * **函数命名规则:** 流程名为方法名为后缀:pubic void invoke${functionFlowName}(${functionFlowName}Context context) * **职责:** 在代码逻辑中,使用该流程需要以该函数作为调用入口 * **类路径:** ```**.service``` * **唯一标识符位置:** 其对应的标识符在函数的注解@AutoGenerated中指定, uuid规则: ${FunctionFlow在TOCO中的uuid}|FLOW|METHOD - **FlowNode** * **生成产物:** 在service层生成一个Java类, **注意** 每个FunctionFlow的开始节点(StartNode)不生成 * **类命名规则:** ${nodeName}Node * **入口函数命名:** pubic void process() * **职责:** 用于封装内聚性的业务逻辑 * **类路径:** ```service.flow.node.${functionFlowName}``` - **FlowContext** * **函数命名规则:** {nodeName}Node * **职责:** 作为流程节点之间的参数传递(包括出参和入参),在实现业务逻辑的时候,按需在这个上下文类中添加所需的字段 * **类路径:** ```**.service.flow.context``` * **唯一标识符位置:** 其对应的标识符在类注解@AutoGenerated中指定, uuid规则: ${FunctionFlow在TOCO中的uuid}|FLOW|CONTEXT - **例子:** - 用户登录,在UserFlowService中生成一个函数invokeLoginFlow,该函数通过流程框架根据流程定义调用LoginNode,LoginNode中封装了用户登录的逻辑,LoginFlowContext中封装了用户登录的参数和结果。 - **修改建议:** 不修改 service 中的函数, 不修改FlowConfig, 可以修改FlowContext, 添加/修改出入参数, 修改FlowNode中的具体业务逻辑。 #### **2.16 自定义查询** - 在读方案无法满足需求的情况下,可以使用自定义查询 - 自定查询使用复杂的sql实现业务功能 - 自定义查询的数据访问层使用MyBatis、MyBatisPlus实现 - 自定查询的时候框架不自动生成任何代码(需要模型编写全部代码) - 各层的代码位置严格遵守章节:**3.2 项目结构与导航**,必须有mapper层、service层、必须有DO对象、必须有DTO对象,如果是API返回数据,必须有VO对象 ### **3 生成代码产物补充说明** - **3.1 支持的语言/框架** Java、SpringBoot、MyBatis-plus(读)、Hibernate(写) - **3.2 项目结构与导航** ``` TOCO生成的项目是一个多模块的SpringBoot项目,包括主模块和子模块,它的子模块位于根目录下的/modules目录中,**注意** 获取子模块代码文件路径的时候需要从根目录开始,所以必须从modules节点开始,例如:子模块module1下的的java类路径应该是`modules/module1/src/main/java...` |──main_module │ ├── common # 项目级别公共的基础模块 │ │ ├──config/ # 中间件配置 │ │ ├──constants/ # 项目级别常量 │ │ ├──enums/ # 项目级别枚举 │ │ ├──redis/ # Redis配置 │ │ ├──response/ # 返回结果封装 │ │ └──utils/ # 项目级别utils └── └── entrance/ # 项目入口 │ └──AppApplication.java # 项目启动类 └── modules # 子模块列表 └── module1/ # 子模块1 ├── common/ │ ├──constants/ # 模块常量 │ ├──utils/ #模块级别utils │ └──enums/ # 枚举 ├── entrance/web/src/main/java/com/{project_name}/{module_name}/entrance/web/ │ ├──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/ # 数据传输对象定义 │ ├── converter/ # 复杂Dto(非BaseDto)组装 │ ├── facade/ # 调用其他模块的RPC适配器,包含RpcAdapter,如UserDtoServiceInMeetingRpcAdapter,表示从meeting模块调用user模块中方法 │ └── impl/ # Dto的查询接口的实现 ├── persist/src/main/java/com/{project_name}/{module_name}/persist/ │ ├── eo/ # 值对象(Eo)的结构定义 │ ├── dos/ # 数据库单表结构的映射 │ ├── qto/ # 读方案的数据库查询实现 │ └── mapper/ # MyBatis的Mapper定义 └── service/src/main/java/com/{project_name}/{module_name}/service/ # BOService(包含某个聚合下所有写方案生成的方法)、 DtoService(包含DTO生成的预定义方法) ├── bto/ # 写方案入参的定义 ├── converter/ # 对返回的BaseDto按需进行二次扩展 ├── query/ # 查询方案的service层入口,调用persist层的查询实现 └── base/ # 每个BOService对应的基类 在一个子模块的内,它的依赖层级为entrance -> service -> manager -> persist, 同时各个层都依赖 common ``` - **3.3 标准查找流程** 1. **API查找** → `modules/{模块名}/entrance/web/controller/` 2. **DTO查找** → `modules/{模块名}/manager/dto/` 3. **Service查找** → `modules/{模块名}/service/` 4. **数据层查找** → `modules/{模块名}/persist/ - **3.4 特殊注解及含义** TOC自动生成的类和方法会带有@AutoGenerated注解,注解中有2个属性:locked为boolean类型,如果locked=true,则代表该文件或方法不建议修改;uuid为String类型,表示该类或方法的唯一标识,如果uuid中包含|字符,则说明该uuid为特殊格式,由不同类型的数据拼装而成(见**[3.2 设计元素到代码的映射规则及修改建议]**中每种设计元素的代码说明)。 ### 4. TOCO 最佳实践 #### 4.1 接口参数类型和返回值选择(Interface Parameter & Return Type Definition): 在做TOCO接口(API、RPC)设计时,通常会先判断接口的主要功能是读数据库或写数据库,并分析相关的读写方案以及对应的QTO和BTO。如果是读场景,则参数会**优先使**用相关的QTO;如果是写场景,则参数会**优先**使用相关的BTO,如果BTO和QTO无法满足要求,则可以再增加基本类型或Enum、EO等类型参数。参数类型选择时必须遵循以下要求:a.DTO、VO不能作为参数类型;b.QTO、BTO不能作为返回值类型。 **重要说明:** - 本章节描述的是参数选择的**优先级原则**,不是绝对限制 - API参数的绝对限制请参考2.14章节 - "优先使用QTO/BTO"意思是在满足规范的前提下,根据场景选择最合适的参数类型 -----------------------------------------------------------------------------