前言
本章主要介绍基于多表关联的「增删改查」接口的设计与实现,建议我们的开发者用户,在自己动手编码之前,先花上 1-2 个小时的时间通读本章,相信磨刀不误砍柴工,您的付出会让后续的开发工作变得事半功倍。以下是橙单可生成的标准化表单接口的功能列表。
- 新增,可跨服务支持,主表与一对一、一对多和多对多关联从表的级联添加。
- 修改,可跨服务支持,主表与一对一、一对多和多对多关联从表的级联修改。
- 删除,可跨服务支持,主表与一对一、一对多和多对多关联从表的级联删除。
- 查询,可跨服务支持,主表与一对一、一对多和多对多关联从表的关联查询,同时还支持主从表数据的字典翻译。
- 聚合,可跨服务支持,主表与一对多和多对多从表数据的聚合计算。
- 分组,可跨服务支持,GROUP BY 结果数据的字典翻译。
- 更多,以上所有功能,无论是单体还是微服务工程,代码接口完全一致。
新增接口
本小节介绍的所有接口,均可通过橙单代码生成器自动生成。
新增主表数据
功能截图。
代码示例,具体细节可参考以下代码中的注释说明。
@PostMapping("/add")
public ResponseResult<Long> add(@MyRequestBody PaperDto paperDto) {
// 1. 基于DTO对象字段上的的Validator API注解,进行输入数据的合法性验证。比如,非空验证等。
// 这里我们还会针对那些取值范围是静态字典的字段数据,做合法性验证。规则也是遵守Validator API
// 的标准注解规则。具体可参考下面PaperDto,部分字段声明的代码片段。
String errorMessage = MyCommonUtil.getModelValidationError(paperDto, false);
if (errorMessage != null) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
}
// 2. 因为DTO对象,仅仅面向于前后端的数据交互,而Controller和Service之间的数据传递,
// 还是使用服务内部的实体对象。所以这里需要做一次对象拷贝。
Paper paper = MyModelUtil.copyTo(paperDto, Paper.class);
// 3. 验证关联Id的数据合法性。比如Paper对象中,包含一些字典字段,就需要去字典表中验证这些待插入的数据
// 是否都是合法的数据。我们还是推荐要从操作入口,尽量保证数据的干净。这也可以参考下面的代码片段。
CallResult callResult = paperService.verifyRelatedData(paper, null);
if (!callResult.isSuccess()) {
return ResponseResult.errorFrom(callResult);
}
// 4. 所有的输入数据验证都通过了,就可以在下面的事务方法中,插入该条数据就行了。
paper = paperService.saveNew(paper);
return ResponseResult.success(paper.getPaperId());
}
// 这数据验证的方法是上面的接口方法调用的具体实现。
@Override
public CallResult verifyRelatedData(Paper paper, Paper originalPaper) {
String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!";
// 这里是判断Paper中的gradeId字段,是否是合法的字典数据。
// 首先会判断该值是否为NULL,如果是NULL,就不用验证,顺利通过。
// 如果该字段不允许为NULL,那么逻辑也走不到这里, 前面的实体字段Validator API验证就会返回错误信息了。
// 因为add/update使用的相同接口,所以对于新增而言是每次都要验证的,如果是update,还会判断gradeId
// 是否变化了,如果没有变化,就不用验证,提升了效率。
if (this.needToVerify(paper, originalPaper, Paper::getGradeId)
&& !gradeService.existId(paper.getGradeId())) {
return CallResult.error(String.format(errorMessageFormat, "所属年级"));
}
// 这里是对于作业类型字典的数据验证。
// 在橙单代码生成器中,我们会根据表关系和字典关系,生成这些可二次开发的高质量代码。
if (this.needToVerify(paper, originalPaper, Paper::getPaperType)
&& !paperTypeService.existId(paper.getPaperType())) {
return CallResult.error(String.format(errorMessageFormat, "作业类型"));
}
return CallResult.ok();
}
// 与Paper实体对象对应的Dto对象。因为是录入接口的参数对象,所以Validator API的验证注解,
// 都声明在该对象中。
@Data
public class PaperDto {
@Schema(description = "主键Id")
@NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class})
private Long paperId;
@Schema(description = "作业名称")
@NotBlank(message = "数据验证失败,作业名称不能为空!")
private String paperName;
@Schema(description = "科目Id")
@NotNull(message = "数据验证失败,所属科目不能为空!")
@ConstDictRef(constDictClass = Subject.class, message = "数据验证失败,所属科目为无效值!")
private Integer subjectId;
// ... ... 为了节省篇幅和流量,这里省略其他字段的定义。
}
新增多对多关联数据
在上一个代码示例中,我们添加了一个作业对象。在本例中,作业数据仍然是主表数据,我们需要为作业添加与其「多对多关联」的习题对象。功能很直观,每个线上作业都会包含多道习题,反之亦然,每到习题也可能被多个作业所包含,这就是非常典型的多对多关联,见如下截图。
代码示例,具体细节可参考以下代码中的注释说明。
@PostMapping("/addPaperExercise")
public ResponseResult<Void> addPaperExercise(
@MyRequestBody Long paperId, @MyRequestBody List<PaperExerciseDto> paperExerciseDtoList) {
if (MyCommonUtil.existBlankArgument(paperId, paperExerciseDtoList)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
// 1. 这里需要从列表参数中,提取出所有多对多关联的从表关联Id字段的数据集合,比如本例的习题Id(exerciseId)。
Set<Long> exerciseIdSet =
paperExerciseDtoList.stream().map(PaperExerciseDto::getExerciseId).collect(Collectors.toSet());
// 2. 这里会同时判断主表关联Id(paperId)和所有从表关联Id集合(exerciseIdSet)是否在原有的主表(作业表)和从表(习题表)
// 中是否都是存在的。注意这里的existUniqueKeyList方法是批量验证,比如有10条习题数据,也只会查询数据库一次,其余的
// 数据比对工作,都是在Java服务的内存中完成的,以此降低与数据库的交互次数并提升接口性能。
if (!paperService.existId(paperId)
|| !exerciseService.existUniqueKeyList("exerciseId", exerciseIdSet)) {
return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID);
}
// 3. 这是一个规范化处理,所有前端传入的对象都是Dto,Controller和Service之间传递的是对应的实体对象。
List<PaperExercise> paperExerciseList =
MyModelUtil.copyCollectionTo(paperExerciseDtoList, PaperExercise.class);
// 4. 在同一个事务内,插入作业和习题的多对多关联数据。
paperService.addPaperExerciseList(paperExerciseList, paperId);
return ResponseResult.success();
}
新增主从表关联数据
这里是以合同录入的审批流程为例的。合同数据通常存储于合同主表,与其关联的合同产品和交付详情等数据,存储于合同产品关联表和合同交付详情表。合同主表与合同产品表是多对多关系,与合同交付详情是一对多的关联关系,通过合同 ID 进行表关联。最后补充一句,本实例是主表数据与一对多和多对多从表数据列表的级联添加,然而事实上,对于并不常见的一对一级联添加我们也是支持的,只是当前的演示示例没有配置,具体配置如下图所示。
代码示例,具体细节可参考以下代码中的注释说明。
@PostMapping("/add")
public ResponseResult<Long> add(
// 主表合同数据。
@MyRequestBody FlowContractDto flowContractDto,
// 一对多合同交付详情数据列表。
@MyRequestBody List<FlowDeliveryDetailDto> flowDeliveryDetailDtoList,
// 多对多合同产品详情数据列表。
@MyRequestBody List<flowContractProductDto> flowContractProductDtoList) {
// 这里会验证验证每一个输入数据的合法性,并将他们从DTO转换为对应的实体对象类型。
// 该验证方法在下面的级联更新的示例中也会被调用,由此可见,我们在生成代码的过程中,
// 已经考虑到,尽可能的减少重复代码的生成。
ResponseResult<Tuple2<FlowContract, JSONObject>> verifyResult = this.doBusinessDataVerifyAndConvert(
flowContractDto,
// forUpdate参数,在级联添加中是false,在级联更新中是true。
false,
flowDeliveryDetailDtoList,
flowContractProductDtoList);
if (!verifyResult.isSuccess()) {
return ResponseResult.errorFrom(verifyResult);
}
Tuple2<FlowContract, JSONObject> bizData = verifyResult.getData();
FlowContract flowContract = bizData.getFirst();
// 这里会在同一个事务之内,批量插入主表和一对多从表的数据的。
// 为了保证接口的应答效率,橙单会生成真正的批量插入INSERT LIST的SQL脚本代码。
flowContract = flowContractService.saveNewWithRelation(flowContract, bizData.getSecond());
return ResponseResult.success(flowContract.getContractId());
}
// 该私有方法,就是上面接口调用的私有方法。
private ResponseResult<Tuple2<FlowContract, JSONObject>> doBusinessDataVerifyAndConvert(
// 合同主表数据。
FlowContractDto flowContractDto,
// 为了保证代码的最大可复用性,add和update接口都会调用该方法,只是该参数值不同。
boolean forUpdate,
// 一对多的合同详情列表数据。
List<FlowDeliveryDetailDto> flowDeliveryDetailDtoList,
// 多对多的合同所包含的产品列表数据。
List<lowContractProductDto> flowContractProductDtoList) {
ErrorCodeEnum errorCode = ErrorCodeEnum.DATA_VALIDATED_FAILED;
// 1. 因为add和update都需要验证实体对象字段的合法性,所以我们一并发到了这个数据验证的私有方法中了。
// 都是基于Validator API的机制,这个在前面的例子中已经介绍过了。
// 下面分别对合同主表对象,一对多合同详情对象列表,多对多合同产品中间表数据对象列表,都要执行该验证。
String errorMessage = MyCommonUtil.getModelValidationError(flowContractDto, forUpdate);
if (errorMessage != null) {
return ResponseResult.error(errorCode, errorMessage);
}
errorMessage = MyCommonUtil.getModelValidationError(flowDeliveryDetailDtoList);
if (errorMessage != null) {
return ResponseResult.error(errorCode, "参数[flowDeliveryDetailDtoList]数据验证失败!" + errorMessage);
}
errorMessage = MyCommonUtil.getModelValidationError(flowContractProductDtoList);
if (errorMessage != null) {
return ResponseResult.error(errorCode, "参数[flowContractProductDtoList]数据验证失败!" + errorMessage);
}
// 全部关联从表数据的验证和转换
JSONObject relationData = new JSONObject();
CallResult verifyResult;
// 下面是输入参数中,主表关联数据的验证。
FlowContract flowContract = MyModelUtil.copyTo(flowContractDto, FlowContract.class);
FlowContract originalData = null;
// 如果是更新接口调用的验证方法,就需要验证主表主键id关联的数据是否存在了。
if (forUpdate && flowContract != null) {
originalData = flowContractService.getById(flowContract.getContractId());
if (originalData == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
relationData.put("originalData", originalData);
}
// 验证主表中字典id字段的合法性,这个在前面的例子中已经介绍过了。
verifyResult = flowContractService.verifyAllRelatedData(flowContract, originalData);
if (!verifyResult.isSuccess()) {
return ResponseResult.errorFrom(verifyResult);
}
// 验证并转换主表的一对多关联 [合同交付详情数据列表]
List<FlowDeliveryDetail> flowDeliveryDetailList =
MyModelUtil.copyCollectionTo(flowDeliveryDetailDtoList, FlowDeliveryDetail.class);
// 这里的列表数据验证,橙单也是批量验证的,比如有10条从表数据,但是和数据库的交互仅有一次,
// 其余的验证逻辑,都是在java服务的内存中完成的,从而减少数据库的交互,提升接口应答效率。
verifyResult = flowDeliveryDetailService.verifyAllRelatedData(flowDeliveryDetailList);
if (!verifyResult.isSuccess()) {
return ResponseResult.errorFrom(verifyResult);
}
relationData.put("flowDeliveryDetailList", flowDeliveryDetailList);
// 验证并转换主表的多对多关联 [合同产品多对多关联表]
List<FlowContractProduct> flowContractProductList =
MyModelUtil.copyCollectionTo(flowContractProductDtoList, FlowContractProduct.class);
relationData.put("flowContractProductList", flowContractProductList);
return ResponseResult.success(new Tuple2<>(flowContract, relationData));
}
修改接口
本小节介绍的所有接口,均可通过橙单代码生成器自动生成。
修改主表数据
功能截图。
代码示例,具体细节可参考以下代码中的注释说明。
@PostMapping("/update")
public ResponseResult<Void> update(@MyRequestBody PaperDto paperDto) {
// 1. 与前面的add接口一样,这里也必须对参数对象进行Validator API的验证。
String errorMessage = MyCommonUtil.getModelValidationError(paperDto, true);
if (errorMessage != null) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
}
Paper paper = MyModelUtil.copyTo(paperDto, Paper.class);
// 2. 根据主键值获取之前的对象,如果不存在就不能修改。
Paper originalPaper = paperService.getById(paper.getPaperId());
if (originalPaper == null) {
// NOTE: 修改下面方括号中的话述
errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!";
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
}
// 验证关联Id的数据合法性。代码的实现逻辑,在前面的add接口中,已经给出了详细注释。
// 说一下和add接口数据字段验证的区别。add接口中,所有非空的字典字段都要验证值的合法性,
// 而这里之所以要传递原有对象(originalXxxxxx)参数,是因为verifyRelatedData方法的
// 内部实现,会比较字典字段值前后是否发生变化,如果没有变化就不用验证了,尽可能的减少了
// 和数据库的交互次数,提升接口的响应性能。
CallResult callResult = paperService.verifyRelatedData(paper, originalPaper);
if (!callResult.isSuccess()) {
return ResponseResult.errorFrom(callResult);
}
// 更新主表数据。
if (!paperService.update(paper, originalPaper)) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
return ResponseResult.success();
}
修改多对多关联数据
继续前面的「新增作业习题多对多」示例场景,如果作业中的某道习题是必做习题,但是该习题在其他作业中,不一定是必做习题,这样我们就需要在作业习题中间表中添加「是否为必做习题」的标记字段,用以标记该习题在作业中是否必做。
代码示例,具体细节可参考以下代码中的注释说明。
@PostMapping("/updatePaperExercise")
public ResponseResult<Void> updatePaperExercise(@MyRequestBody PaperExerciseDto paperExerciseDto) {
String errorMessage = MyCommonUtil.getModelValidationError(paperExerciseDto);
if (errorMessage != null) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
}
PaperExercise paperExercise = MyModelUtil.copyTo(paperExerciseDto, PaperExercise.class);
// 以主表关联Id(paperId)和从表关联Id(exerciseId)作为过滤条件,修改中间表数据的字段值。
// 这样就不用提前验证这两个关联字段的合法性了,如果不存在,下面的updatePaperExercise就会
// 返回false。
if (!paperService.updatePaperExercise(paperExercise)) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
return ResponseResult.success();
}
修改主从表关联数据
这里继续以前面的合同审批流程为例。在审批过程中,有些合同数据可能会被审批人修改,为了避免出现脏数据,主从表的所有数据变更需要在同一事务内级联更新。否则如果放在多个接口的多个事务中,一旦前面的事务提交成功,而后面的接口调用出现网络故障,就会导致审批中的合同及其关联数据,出现不一致的现象。我想这就是橙单必须要支持,生成级联更新接口代码的原因吧。
代码示例,具体细节可参考以下代码中的注释说明。
@PostMapping("/update")
public ResponseResult<Long> update(
// 合同主表数据对象。
@MyRequestBody FlowContractDto flowContractDto,
// 合同的一对多关联从表数据列表(合同交付详情)
@MyRequestBody List<FlowDeliveryDetailDto> flowDeliveryDetailDtoList,
// 合同的多对多关联从表数据列表(合同产品关联表)
@MyRequestBody List<FlowContractProductDto> flowContractProductDtoList) {
String errorMessage;
// 这个验证方法的代码和详解,我们在上面的级联添加的代码中已经给出,这里不再展开。
// 由此可见,我们在生成代码的过程中已经考虑到,尽可能的减少重复代码的生成。
ResponseResult<Tuple2<FlowContract, JSONObject>> verifyResult = this.doBusinessDataVerifyAndConvert(
flowContractDto,
// forUpdate参数在这里是true,级联添加中是false。
true,
flowDeliveryDetailDtoList,
flowContractProductDtoList);
if (!verifyResult.isSuccess()) {
return ResponseResult.errorFrom(verifyResult);
}
Tuple2<FlowContract, JSONObject> bizData = verifyResult.getData();
FlowContract originalFlowContract = bizData.getSecond().getObject("originalData", FlowContract.class);
FlowContract flowContract = bizData.getFirst();
// 级联更新主从表数据。如果是同库,下面的方法会标有 @Transactional 注解,并在同一事务内完成。
// 如果主表和任一从表不同库,我们就会添加Seata的 @GlobalTransactional 注解,从而保证分布式事务的数据一致性。
// 最后需要说明的是,对于从表数据,如果为null,则忽略更新,如果是空数组,则会删除从表数据。
if (!flowContractService.updateWithRelation(flowContract, originalFlowContract, bizData.getSecond())) {
errorMessage = "数据验证失败,[FlowContract] 数据不存在!";
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
}
return ResponseResult.success(flowContract.getContractId());
}
删除接口
本小节介绍的所有接口,均可通过橙单代码生成器自动生成。
删除主表数据
功能截图。
代码示例,具体细节可参考以下代码中的注释说明。
@PostMapping("/delete")
public ResponseResult<Void> delete(@MyRequestBody(required = true) Long paperId) {
// 在下面的remove方法中,不仅会删除主表数据,同时一定会删除与其关联的多对多关联数据。
// 重点说明的是,当前表无论作为多对多关联中的主表还是从表,下面的方法都会删除与其关联的中间表数据。
// 具体可见下面的代码。
if (!paperService.remove(paperId)) {
String errorMessage = "数据操作失败,删除作业不存在,请刷新后重试!";
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
}
return ResponseResult.success();
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean remove(Long paperId) {
if (paperMapper.deleteById(paperId) == 0) {
return false;
}
// 在这个多对多关联中,作业表是主表,习题表是从表。开始删除多对多子表的关联。
PaperExercise paperExercise = new PaperExercise();
paperExercise.setPaperId(paperId);
paperExerciseMapper.delete(new QueryWrapper<>(paperExercise));
// 在这个多对多关联中,课程章节表是主表,作业表是从表。开始删除多对多父表的关联。
CourseSectionPaper courseSectionPaper = new CourseSectionPaper();
courseSectionPaper.setPaperId(paperId);
courseSectionPaperMapper.delete(new QueryWrapper<>(courseSectionPaper));
return true;
}
删除多对多关联数据
继续前面的「新增作业习题多对多」示例场景,这里可以移除任何一个作业习题的关联数据,移除后,仅仅该习题不在与该作业存在关联关系,而习题本身的数据不会被删除。
代码示例,具体细节可参考以下代码中的注释说明。
@PostMapping("/deletePaperExercise")
public ResponseResult<Void> deletePaperExercise(
@MyRequestBody Long paperId, @MyRequestBody Long exerciseId) {
if (MyCommonUtil.existBlankArgument(paperId, exerciseId)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
// 移除单条作业和习题之间的多对多关系。
if (!paperService.removePaperExercise(paperId, exerciseId)) {
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
}
return ResponseResult.success();
}
删除主从表关联数据
这里继续以前面的合同审批流程为例。合同与合同交付详情是一对多的主从关系,从业务视角来看,如果合同被删除了,那么合同详情数据也没有存在的意义了,因此这种强关联的数据,可以考虑级联删除。具体细节可参考以下代码中的注释说明。
// 仅从Controller的接口代码逻辑来看,和普通删除没啥区别。因为级联删除需要在同一事务内完成,
// 所以实现细节都在下面的ServiceImpl提供的remove方法中。
@PostMapping("/delete")
public ResponseResult<Void> delete(@MyRequestBody(required = true) Long contractId) {
String errorMessage;
if (!testFlowContractService.remove(contractId)) {
errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
}
return ResponseResult.success();
}
// 级联删除主从表数据。如果是同库,下面的方法会标有 @Transactional 注解,并在同一事务内完成。
// 如果主表和任一从表不同库,我们就会添加Seata的 @GlobalTransactional 注解,从而保证分布式事务的数据一致性。
@Transactional(rollbackFor = Exception.class)
@Override
public boolean remove(Long contractId) {
// 这里必须先获取主表数据对象,从而可以拿到与其他关联表的关联字段数据。
FlowContract flowContract = flowContractMapper.selectById(contractId);
if (flowContract == null) {
return false;
}
// 删除主表数据。
if (flowContractMapper.deleteById(contractId) == 0) {
return false;
}
// 根据关联字段删除一对多从表数据,下面的removeByContractId方法也是橙
// 单根据配置动态生成的方法。
flowDeliveryDetailService.removeByContractId(contractId);
// 开始删除多对多中间表的关联
FlowContractProduct flowContractProduct = new FlowContractProduct();
flowContractProduct.setContractId(contractId);
flowContractProductMapper.delete(new QueryWrapper<>(flowContractProduct));
return true;
}
查询接口
本小节介绍的所有接口,均可通过橙单代码生成器自动生成。
查询主表及关联数据
这里主要分为两个场景,一个是主表的查询列表数据中,包含一对一从表的关联字典,如下图的视频课程列表中,包含了年级、学科、课程难度等字典翻译类型的数据关联。另一种是主表和一对多从表的主从联动,这些我们都可以通过橙单的表单编辑配置而得。
代码示例,具体细节可参考以下代码中的注释说明。
@PostMapping("/list")
public ResponseResult<MyPageData<VideoCourseVo>> list(
@MyRequestBody VideoCourseDto videoCourseDtoFilter,
@MyRequestBody MyOrderParam orderParam,
@MyRequestBody MyPageParam pageParam) {
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}
// 标准化的处理方式,前端和后台之前用Dto对象传递数据,而Controller和Service之间可以使用
// 注解信息更多的实体对象。
VideoCourse videoCourseFilter = MyModelUtil.copyTo(videoCourseDtoFilter, VideoCourse.class);
// 构建动态多字段排序ORDER BY字符串。前端会根据规则传递需要排序的字段和升降序规则。
// 在保证如此足够灵活性的同时,buildOrderBy方法进行了较为严格的验证,避免了SQL注入的发生。
String orderBy = MyOrderParam.buildOrderBy(orderParam, VideoCourse.class);
// getXxxxxListWithRelation方法,这样命名规则的方法,都会根据实体对象中,数据关联的注解,
// 实现从表数据的批量获取和数据自动组装,最后以统一的格式返回给前端页面。
List<VideoCourse> videoCourseList =
videoCourseService.getVideoCourseListWithRelation(videoCourseFilter, orderBy);
return ResponseResult.success(MyPageUtil.makeResponseData(videoCourseList, VideoCourse.INSTANCE));
}
@Override
public List<VideoCourse> getVideoCourseListWithRelation(VideoCourse filter, String orderBy) {
// 单表查询出主表的数据列表。
List<VideoCourse> resultList = videoCourseMapper.getVideoCourseList(filter, orderBy);
// 这里基类中的通用方法,会根据实体对象中的注解,自动查询并组装关联的从表数据。
this.buildRelationForDataList(resultList, MyRelationParam.normal());
return resultList;
}
@Data
@TableName(value = "zz_video_course")
public class VideoCourse {
@TableId(value = "video_course_id")
private Long videoCourseId;
@TableField(value = "course_name")
private String courseName;
@TableField(value = "grade_id")
private Integer gradeId;
// ... ... 为了便于演示,省略了若干其他字段。
// 这里仅是字典数据翻译的注解。关联后的字典数据会组装到gradeIdDictMap字段。
// 这类注解都会被上面的buildRelationForDataList方法识别,并自动批量查询和自动组装。
// 更多细节可以参考橙单的线上开发文档。
@RelationDict(
masterIdField = "gradeId",
slaveServiceName = "gradeService",
slaveModelClass = Grade.class,
slaveIdField = "gradeId",
slaveNameField = "gradeName")
@TableField(exist = false)
private Map<String, Object> gradeIdDictMap;
}
查询主表及关联聚合数据
在下面的截图中,班级和学生、班级和课程都是多对多的关联关系。其中班级主表中并不包含「课时数量」、「学生数量」和「课程进度」等字段,这里是根据多对多关联从表中的数据动态聚合计算而来。
代码示例,具体细节可参考以下代码中的注释说明。
@PostMapping("/list")
public ResponseResult<MyPageData<StudentClassVo>> list(
@MyRequestBody StudentClassDto studentClassDtoFilter,
@MyRequestBody MyOrderParam orderParam,
@MyRequestBody MyPageParam pageParam) {
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}
StudentClass studentClassFilter = MyModelUtil.copyTo(studentClassDtoFilter, StudentClass.class);
String orderBy = MyOrderParam.buildOrderBy(orderParam, StudentClass.class);
// 所有的文章都在这里,getStudentClassListWithRelation会根据实体对象中的注解,
// 实现从表或中间表数据查询,然后再将查询结果,根据注解说明进行指定的聚合计算后,以虚拟字段的形式,赋值给主表数据列表。
// 在下面的查询中,班级学生数量,仅仅通过聚合中间表数据即可,因此可以不用查询从表了,从而可以提升查询效率。
// 而“课程数量”和“课程进度”两个动态聚合虚拟字段,其数据必须来自于从表课程表的“课时数量”和“完成状态”两个字段,因此,
// 他们的计算必须通过从表数据的聚合计算而得。
List<StudentClass> studentClassList =
studentClassService.getStudentClassListWithRelation(studentClassFilter, orderBy);
return ResponseResult.success(MyPageUtil.makeResponseData(studentClassList, StudentClass.INSTANCE));
}
@Data
@TableName(value = "zz_class")
public class StudentClass {
@TableId(value = "class_id")
private Long classId;
@TableField(value = "class_name")
private String className;
@TableField(value = "finish_class_hour")
private Integer finishClassHour;
@TableLogic
private Integer status;
// ... ... 中间忽略若干字段的声明。
// 重点看下面两个注解声明的字段,注解的具体含义可参考橙单的线上文档。
// 这里我们只需知道上面的buildRelationForDataList方法,会识别该注解,并实现数据查询、优化和聚合计算。
// 总课时数 (多对多聚合计算字段)。
@RelationManyToManyAggregation(
// 当前主表和中间表的关联字段是classId。
masterIdField = "classId",
// 通过课程服务实现类去查询课程数据。
slaveServiceName = "courseService",
// 从表的对象类型是Course。
slaveModelClass = Course.class,
// 从表和中间表关联的字段是courseId。
slaveIdField = "courseId",
// 中间表的对象类型。
relationModelClass = ClassCourse.class,
// 中间表中和主表的关联字段。
relationMasterIdField = "classId",
// 中间表中和从表的关联字段。
relationSlaveIdField = "courseId",
// 基于从表的对象字段进行聚合计算。
aggregationModelClass = Course.class,
// 聚合计算的类型是SUM求和。
aggregationType = AggregationType.SUM,
// 从表中求和的字段是课时字段。
aggregationField = "classHour")
@TableField(exist = false)
private Integer totalClassHour = 0;
// 学生数量 (多对多聚合计算字段)。
@RelationManyToManyAggregation(
// 当前主表和中间表的关联字段是classId。
masterIdField = "classId",
// 通过学生服务实现类去查询课程数据。
slaveServiceName = "studentService",
// 从表的对象类型是Student。
slaveModelClass = Student.class,
// 从表和中间表关联的字段是studentId。
slaveIdField = "studentId",
// 中间表的对象类型。
relationModelClass = ClassStudent.class,
// 中间表中和主表的关联字段。
relationMasterIdField = "classId",
// 中间表中和从表的关联字段。
relationSlaveIdField = "studentId",
// 基于从表的对象字段进行聚合计算。
aggregationModelClass = Student.class,
// 聚合计算的类型是COUNT汇总,这里会被优化为基于中间表的计算。
aggregationType = AggregationType.COUNT,
aggregationField = "studentId")
@TableField(exist = false)
private Integer studentCount = 0;
}
查询主表分组统计数据
在下面的截图中,课程流水统计主表包含三个统计图表,GROUP BY 的维度字段分别为「年级」、「科目」和「统计日期」。这里的用例可以在橙单代码生成器中,将主表配置为具备统计能力的数据表,并为该表分别指定可能的维度和指标字段,前端可以根据图表的配置,作为参数动态的传入分组字段值。
代码示例,具体细节可参考以下代码中的注释说明。
@PostMapping("/listWithGroup")
public ResponseResult<MyPageData<CourseTransStatsVo>> listWithGroup(
@MyRequestBody CourseTransStatsDto courseTransStatsDtoFilter,
@MyRequestBody(required = true) MyGroupParam groupParam,
@MyRequestBody MyOrderParam orderParam,
@MyRequestBody MyPageParam pageParam) {
String orderBy = MyOrderParam.buildOrderBy(orderParam, CourseTransStats.class);
// 和上面的排序对象参数一样,分组字段也是由前端作为参数动态传入的,这样就会更加灵活。
// buildGroupBy工具方法会根据接口参数和主表对象的类型进行必要的验证和转换,最后返回
// GROUP BY从句所需的字符串,这个过程可以完全规避SQL注入的风险。
groupParam = MyGroupParam.buildGroupBy(groupParam, CourseTransStats.class);
if (groupParam == null) {
return ResponseResult.error(
ErrorCodeEnum.INVALID_ARGUMENT_FORMAT, "数据参数错误,分组参数不能为空!");
}
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}
CourseTransStats filter = MyModelUtil.copyTo(courseTransStatsDtoFilter, CourseTransStats.class);
MyGroupCriteria criteria = groupParam.getGroupCriteria();
// 进行分组计算,同时分组计算后的结果,还可以进行字典翻译。如上图中的年级信息,在课程流水表中,我们保存
// 保存的都是grade_id值,分组的统计也是基于这些id数据,但是在返回给前端显示之前,我们还会根据配置,对
// 分组统计的结果集,进行一次字典翻译的工作。
List<CourseTransStats> resultList = courseTransStatsService.getGroupedCourseTransStatsListWithRelation(
filter, criteria.getGroupSelect(), criteria.getGroupBy(), orderBy);
return ResponseResult.success(MyPageUtil.makeResponseData(resultList, CourseTransStats.INSTANCE));
}
@Override
public List<CourseTransStats> getGroupedCourseTransStatsListWithRelation(
CourseTransStats filter, String groupSelect, String groupBy, String orderBy) {
// 橙单会根据用户在生成器中的配置,生成getGroupedCourseTransStatsList方法所需的SQL语句。
List<CourseTransStats> resultList =
courseTransStatsMapper.getGroupedCourseTransStatsList(filter, groupSelect, groupBy, orderBy);
// 可以看到这里对分组计算的结果进行了关联绑定和字典翻译等工作。该方法在前面已经介绍过了。
this.buildRelationForDataList(resultList, MyRelationParam.normal());
return resultList;
}
<!-- 这里的SQL也是橙单根据开发者的在线配置生成而来,无需手写。-->
<select id="getGroupedCourseTransStatsList" resultMap="BaseResultMap"
parameterType="com.orangeforms.demo.webadmin.app.model.CourseTransStats">
SELECT * FROM
(SELECT
SUM(student_attend_count) student_attend_count,
SUM(student_flower_amount) student_flower_amount,
SUM(student_flower_count) student_flower_count,
${groupSelect}
FROM zz_course_trans_stats
<where>
<include refid="filterRef"/>
</where>
GROUP BY ${groupBy}) zz_course_trans_stats
<if test="orderBy != null and orderBy != ''">
ORDER BY ${orderBy}
</if>
</select>
查询多对多关联数据
在前面的作业和习题多对多关联的示例代码中,我们需要显示当前作业已经关联了哪些习题的列表,并在前端页面中展示出来。
代码示例,具体细节可参考以下代码中的注释说明。
// 这里的标准化的接口,都是由橙单代码生成工具,根据用户的配置动态生成的,无需手写一行。
@PostMapping("/listPaperExercise")
public ResponseResult<MyPageData<ExerciseVo>> listPaperExercise(
@MyRequestBody(required = true) Long paperId,
// 这里我们还支持主表的过滤条件。
@MyRequestBody ExerciseDto exerciseDtoFilter,
// 支持中间表的过滤条件,比如本例可以查询“必做习题”的列表。
@MyRequestBody PaperExerciseDto paperExerciseDtoFilter,
@MyRequestBody MyOrderParam orderParam,
@MyRequestBody MyPageParam pageParam) {
// 必要的数据验证,先判断当前主表id是否存在。
if (!paperService.existId(paperId)) {
return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID);
}
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}
// 进行标准化的对象转换,所有Controller和Service之间,都是传递实体对象的,而非Dto。
Exercise filter = MyModelUtil.copyTo(exerciseDtoFilter, Exercise.class);
PaperExercise paperExerciseFilter =
MyModelUtil.copyTo(paperExerciseDtoFilter, PaperExercise.class);
String orderBy = MyOrderParam.buildOrderBy(orderParam, Exercise.class);
// 这里会根据配置生成下面的方法,即根据主表对象的关联Id,获取从表的数据对象列表。
List<Exercise> exerciseList =
exerciseService.getExerciseListByPaperId(paperId, filter, paperExerciseFilter, orderBy);
return ResponseResult.success(MyPageUtil.makeResponseData(exerciseList, Exercise.INSTANCE));
}
上面执行的关联查询语句如下。
<select id="getExerciseListByPaperId" resultMap="BaseResultMapWithPaperExercise">
SELECT
zz_exercise.*,
zz_paper_exercise.*
FROM
zz_exercise,
zz_paper_exercise
<where>
AND zz_paper_exercise.paper_id = #{paperId}
AND zz_paper_exercise.exercise_id = zz_exercise.exercise_id
<!-- 这里支持主表字段过滤 -->
<include refid="filterRef"/>
<!-- 中间表字段过滤,如:是否必选习题标记 -->
<include refid="com.orangeforms.demo.webadmin.app.dao.PaperExerciseMapper.filterRef"/>
</where>
<if test="orderBy != null and orderBy != ''">
ORDER BY ${orderBy}
</if>
</select>
查询多对多未关联数据
在前面的作业和习题多对多关联的示例代码中,当为作业添加新习题时,我们需要显示出尚未与当前作业进行关联的那些习题列表,并作为候选者在前端页面中展示出来。
代码示例,具体细节可参考以下代码中的注释说明。
// 这里的标准化的接口,都是由橙单代码生成工具,根据用户的配置动态生成的,无需手写一行。
@PostMapping("/listNotInPaperExercise")
public ResponseResult<MyPageData<ExerciseVo>> listNotInPaperExercise(
@MyRequestBody Long paperId,
@MyRequestBody ExerciseDto exerciseDtoFilter,
@MyRequestBody MyOrderParam orderParam,
@MyRequestBody MyPageParam pageParam) {
// 必要的数据验证,先判断当前主表id是否存在。
if (MyCommonUtil.isNotBlankOrNull(paperId) && !paperService.existId(paperId)) {
return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID);
}
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}
Exercise filter = MyModelUtil.copyTo(exerciseDtoFilter, Exercise.class);
String orderBy = MyOrderParam.buildOrderBy(orderParam, Exercise.class);
// 这里会根据配置生成下面的方法,即根据主表对象的关联Id,获取未关联的从表数据对象列表。
List<Exercise> exerciseList =
exerciseService.getNotInExerciseListByPaperId(paperId, filter, orderBy);
return ResponseResult.success(MyPageUtil.makeResponseData(exerciseList, Exercise.INSTANCE));
}
上面执行的关联查询语句如下。
<!-- 这里的SQL,也是橙单代码生成工具根据开发者的配置动态生成的。-->
<select id="getNotInExerciseListByPaperId" resultMap="BaseResultMap">
SELECT
zz_exercise.*
FROM
zz_exercise
<where>
AND NOT EXISTS (SELECT * FROM zz_paper_exercise pe
WHERE pe.paper_id = #{paperId} AND pe.exercise_id = zz_exercise.exercise_id)
<include refid="filterRef"/>
</where>
<if test="orderBy != null and orderBy != ''">
ORDER BY ${orderBy}
</if>
</select>
微服务实现
我们需要继续强调的是,上面的所有表关联接口,全部适用于「主表和从表位于不同服务」的微服务架构场景,并且主表的 Controller 接口参数完全一致。这样最大的一个优势就是,如果你的项目前期是单体架构,而后面准备升级改造为微服务架构时,前端代码的接口调用逻辑,权限规则等均可以保持不变。变化的只是 Controller 的内部实现,以及调用的 Service 方法,从之前的本地事务注解 @Transational,改为基于 Seata 的分布式事务注解 @GlobalTransactional。
在橙单代码生成工具中,我们会根据用户的配置,为每个选定的业务表 Controller 接口,生成与之对应的标准化的 FeignClient 接口,以便于接口间进行标准化的相互调用。
远程数据验证
调用主表的数据新增接口时,如果该主表包含其他微服务的字典表数据,此时就需要调用远程微服务的数据验证接口,以判断主表中的关联字段是否为合法数据。在前面的代码示例中,我们已经给出了本服务内的关联字段验证,下面的代码为基于远程 FeignClient 接口的数据验证。
@PostMapping("/add")
public ResponseResult<Long> add(@MyRequestBody CourseDto courseDto) {
String errorMessage = MyCommonUtil.getModelValidationError(courseDto, false);
if (errorMessage != null) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
}
Course course = MyModelUtil.copyTo(courseDto, Course.class);
// 新增课程的时候,课程表中的teacher_id和school_id字段,与其关联的zz_teacher和zz_school表,
// 与当前课程表zz_course,位于不同的微服务中,因此对teacherId和schoolId字段数据的验证,就需要
// 通过FeignClient的远程服务接口实现。
CallResult callResult = courseService.verifyAllRelatedData(course, null);
if (!callResult.isSuccess()) {
return ResponseResult.errorFrom(callResult);
}
course = courseService.saveNew(course);
return ResponseResult.success(course.getCourseId());
}
// 下面的verifyRemoteRelatedData方法,仅包含所有需要远程验证的字典。而本地验证的字段,会在
// verifyRelatedData方法中实现,这两个方法会被上面的verifyAllRelatedData分别调用。这样拆分
// 的好处是,代码结构和性能测试都非常清晰。
@Override
public CallResult verifyRemoteRelatedData(Course course, Course originalCourse) {
String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!";
// needToVerify方法,顾名思义,判断当前对象的字段值是否需要验证,如果关联字段值为null,
// 或者与原有对象originalXxxx对象字段值相比没有变化的时候,需无需验证,从而避免额外的性能开销。
if (this.needToVerify(course, originalCourse, Course::getTeacherId)) {
// teacherClient,在橙单缺省生成的代码中,多有FeignClient对象都是XxxxClient的命名规则。
ResponseResult<Boolean> responseResult = teacherClient.existId(course.getTeacherId());
// 如果验证没有通过,远程服务的接口会返回明确的错误信息,这里直接返回即可。
if (this.hasErrorOfVerifyRemoteRelatedData(responseResult)) {
return CallResult.error(String.format(errorMessageFormat, "主讲老师Id"));
}
}
// ... ... 这里省略了schoolId的验证逻辑。
return CallResult.ok();
}
// 在upms服务的TeacherController接口中,会给出该FeignClient接口的实现。逻辑比较简单,这里就不给出具体实现了。
@FeignClient(
// Teacher主表数据和接口位于upms微服务中。
name = "upms",
configuration = FeignConfig.class,
fallbackFactory = TeacherClient.TeacherClientFallbackFactory.class)
public interface TeacherClient extends BaseClient<TeacherDto, TeacherVo, Long> {
// 判断主键Id是否存在。
@Override
@PostMapping("/teacher/existId")
ResponseResult<Boolean> existId(@RequestParam("teacherId") Long teacherId);
// ... ... 这里省略了其他标准化远程调用接口的声明,以及服务熔断降级接口的声明。
}
```
远程数据级联增删改
为了缩减篇幅,这里我们仅给出远程级联删除的代码示例,另外两个数据操作场景 (远程级联增改),与本例的代码实现机制基本一致。在开始看代码之前,我们这里需要重点说明一下的是,橙单自动生成的所有分布式事务代码逻辑,都是基于 Seata 分布式事务框架中的 AT 模式,该模式可以自动推演简单 SQL 的反向回滚补偿逻辑,「看好重点是简单SQL」。因此橙单自动生成的代码中,都是非常简单的单表增删改逻辑,非常易于 Seata AT 模式的回滚补偿推演。然而对于您手写的复杂逻辑,如果担心 Seata 的自动补偿推演会产生错误,那么就只能使用 TCC 的补偿模式了。
// 可以看出这里的接口定义和实现逻辑,与前面单体服务中的示例代码完全一样。
@PostMapping("/delete")
public ResponseResult<Void> delete(@MyRequestBody Long courseId) {
String errorMessage;
if (MyCommonUtil.existBlankArgument(courseId)) {
return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
}
// 删除课程的同时,还有删除与之关联的远程从表数据。具体见下面的代码实现。
if (!courseService.remove(courseId)) {
errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
}
return ResponseResult.success();
}
// 这里添加了Seata的分布式事务的注解。
@GlobalTransactional(rollbackFor = Exception.class)
@Transactional(rollbackFor = Exception.class)
@Override
public boolean remove(Long courseId) {
// 删除课程主表数据
if (courseMapper.deleteById(courseId) == 0) {
return false;
}
// 删除与之一对多关联的课程章节的从表数据。课程和课程章节位于同一服务之内,因此下面的调用是本地Service的删除逻辑。
courseSectionService.removeByCourseId(courseId);
// 删除与远程多对多父表的关联。
// 课程和班级是多对多的关联关系,其中在橙单的教学版配置示例中,班级是主表,课程是从表,班级课程表是中间表。
// 在这里demo中,班级和班级课程位于upms服务,而这里的课程,则位于course-paper服务,因此待删除的中间表
// 数据,与课程接口位于不同的微服务。这就需要用到了FeignClient的远程删除接口。
// 注意,下面的接口定义和实现,都是橙单工具为您生成的,无需手写一行。
ResponseResult<Integer> courseResult = studentClassClient.deleteClassCourseByCourseId(courseId);
if (!courseResult.isSuccess()) {
// 如果远程调用失败了,这里可以获得准确的远程错误信息。
String errorMessage = MyCommonUtil.makeDeleteRelationGlobalTransError(
getClass(), studentClassClient.getClass(), courseResult.getErrorMessage(), courseId);
// 在当前服务的日志中,记录准确错误信息。至于错误的异常栈,会在服务降级接口中统一处理,绝对不会漏掉的。
log.error(errorMessage);
try {
// 由于Feign的降级接口自动吃掉了远程的异常错误,所以Seata的代码不会捕捉到异常,也就不会自动回滚,
// 所以这里必须手动及时调用回滚操作。切记!
GlobalTransactionContext.reload(RootContext.getXID()).rollback();
} catch (TransactionException e) {
e.printStackTrace();
}
// 为了保证事务的一致性,务必以异常的方式抛出错误。统一异常处理AOP,会拦截该异常,并返回给前端具体的错误信息。
throw new MyRuntimeException(errorMessage);
}
return true;
}
// 完全不用担心,这些远程调用接口,都是橙单低代码工具,会根据您的配置,为您自动生成的。
// 当然如果您没有为Controller配置远程接口,我们也不会生成相关的代码,以污染您的工程的。
@FeignClient(
// 由此可见,学生班级对象及其接口,均位于upms微服务中。
name = "upms",
configuration = FeignConfig.class,
fallbackFactory = StudentClassClient.StudentClassClientFallbackFactory.class)
public interface StudentClassClient extends BaseClient<StudentClassDto, StudentClassVo, Long> {
// 保存或更新数据。
@Override
@PostMapping("/studentClass/saveNewOrUpdate")
ResponseResult<StudentClassVo> saveNewOrUpdate(@RequestBody StudentClassDto data);
// 批量新增或保存数据列表。
@Override
@PostMapping("/studentClass/saveNewOrUpdateBatch")
ResponseResult<Void> saveNewOrUpdateBatch(@RequestBody List<StudentClassDto> dataList);
// 删除主键Id关联的对象。
@Override
@PostMapping("/studentClass/deleteById")
ResponseResult<Integer> deleteById(@RequestParam("classId") Long classId);
// 删除多对多关联关系表中,和从表id相关的所有关联数据。
@PostMapping("/studentClass/deleteClassCourseByCourseId")
ResponseResult<Integer> deleteClassCourseByCourseId(@RequestParam("courseId") Long courseId);
// ... ... 这里省略了其他标准化远程调用接口的声明,以及服务熔断降级接口的声明。
}
```
远程数据查询关联
查询主表数据列表时,其某些字段可以来自于远程服务的字典数据和一对一关联数据,如前面介绍的课程对象,其中的 teacherId 字段所关联的数据,来自于 upms 微服务。在查询课程列表时,我们会根据实体对象中的注解信息,自动实现远程关联数据的批量查询和数据组装。
@PostMapping("/list")
public ResponseResult<MyPageData<CourseVo>> list(
@MyRequestBody CourseDto courseDtoFilter,
@MyRequestBody MyOrderParam orderParam,
@MyRequestBody MyPageParam pageParam) {
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}
Course courseFilter = MyModelUtil.copyTo(courseDtoFilter, Course.class);
String orderBy = MyOrderParam.buildOrderBy(orderParam, Course.class);
// 下面的列表查询方法,会实现远程关联数据的批量查询和自动组装。
List<Course> courseList = courseService.getCourseListWithRelation(courseFilter, orderBy);
return ResponseResult.success(MyPageUtil.makeResponseData(courseList, Course.INSTANCE));
}
@Override
public List<Course> getCourseListWithRelation(Course filter, String orderBy) {
// 这里仅仅基于单表查询,获取到主表数据列表。
List<Course> resultList = courseMapper.getCourseListEx(null, null, filter, orderBy);
// 下面的基类方法,会识别远程调用数据关联的注解。并自动实现批量查询和数据组装。
this.buildRelationForDataList(resultList, MyRelationParam.normal());
return resultList;
}
@Data
@TableName(value = "zz_course")
public class Course {
@TableId(value = "course_id")
private Long courseId;
@TableField(value = "course_name")
private String courseName;
@TableField(value = "teacher_id")
private Long teacherId;
@RelationOneToOne(
// 通过课程主表中的teacherId字段进行关联。
masterIdField = "teacherId",
// 调用TeacherClient接口方法。
slaveClientClass = TeacherClient.class,
// 返回的数据对象类型是TeacherVo对象。
slaveModelClass = TeacherVo.class,
// 从表老师表中的关联字段是teacherId。
slaveIdField = "teacherId")
@TableField(exist = false)
private TeacherVo teacher;
// ... ... 为了缩减篇幅,这里仅仅给出需要远程关联的teacherId的注解。
}
// 下面的listByIds接口,会在TeacherController中给出具体的实现。
// 通过listByIds接口的定义可以看出,我们是批量获取从表数据的。具体步骤如下。
// 1. 根据课程主表的查询结果集,可以提取出这些课程列表中的所有teacherId集合,并去重。
// 2. 将teacherIds的集合作为参数调用TeacherClient.listByIds接口,可以一次获取所有老师对象。
// 3. 批量的读取,可以降低远程RPC调用的次数,以提升本次应该和系统整体的运行时效率。
// 4. 如果withDict为true,也可以获取从表的字典翻译字段的翻译后数据。
@FeignClient(
name = "upms",
configuration = FeignConfig.class,
fallbackFactory = TeacherClient.TeacherClientFallbackFactory.class)
public interface TeacherClient extends BaseClient<TeacherDto, TeacherVo, Long> {
// 基于主键的(In-list)条件获取远程数据接口。
@Override
@PostMapping("/teacher/listByIds")
ResponseResult<List<TeacherVo>> listByIds(
@RequestParam("teacherIds") Set<Long> teacherIds,
@RequestParam("withDict") Boolean withDict);
}
远程数据聚合关联
还是以前面的班级列表为例,在橙单微服务教学版工程配置中,班级和课程分别位于 upms 和 course-paper 两个不同的微服务中,别问为什么如此拆分,该工程只是一个用于我们内部测试,以及对外演示我们代码生成能力的示例工程。具体见下图。
代码示例如下。
@PostMapping("/list")
public ResponseResult<MyPageData<StudentClassVo>> list(
@MyRequestBody StudentClassDto studentClassDtoFilter,
@MyRequestBody SysDeptDto sysDeptDtoFilter,
@MyRequestBody MyOrderParam orderParam,
@MyRequestBody MyPageParam pageParam) {
if (pageParam != null) {
PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
}
StudentClass studentClassFilter = MyModelUtil.copyTo(studentClassDtoFilter, StudentClass.class);
SysDept sysDeptFilter = MyModelUtil.copyTo(sysDeptDtoFilter, SysDept.class);
String orderBy = MyOrderParam.buildOrderBy(orderParam, StudentClass.class);
// 下面的关联查询方法,会根据学生班级主表实体对象中的注解,进行聚合计算的动态数据关联。
List<StudentClass> studentClassList =
studentClassService.getStudentClassListWithRelation(studentClassFilter, sysDeptFilter, orderBy);
return ResponseResult.success(MyPageUtil.makeResponseData(studentClassList, StudentClass.INSTANCE));
}
@Override
public List<StudentClass> getStudentClassListWithRelation(StudentClass filter, SysDept sysDeptFilter, String orderBy) {
List<StudentClass> resultList =
studentClassMapper.getStudentClassListEx(null, null, filter, sysDeptFilter, orderBy);
// 这个方法在前面已经多次提到了,BaseService基类中的方法。会根据实体对象的注解,实现数据组装。
this.buildRelationForDataList(resultList, MyRelationParam.normal());
return resultList;
}
@Data
@TableName(value = "zz_class")
public class StudentClass {
@TableId(value = "class_id")
private Long classId;
@TableField(value = "class_name")
private String className;
// 班级课程数量 (多对多聚合计算字段)。
@RelationManyToManyAggregation(
// 班级主表的关联字段
masterIdField = "classId",
// 需要通过远程FeignClient调用,获取从表聚合计算后的数据。
slaveClientClass = CourseClient.class,
// 中间表对象类型。
relationModelClass = ClassCourse.class,
// 中间表和主表关联的Id
relationMasterIdField = "classId",
// 中间表和从表关联的Id
relationSlaveIdField = "courseId",
// 从表的Vo对象。
slaveModelClass = CourseVo.class,
// 从表中的关联字段。
slaveIdField = "courseId",
// 聚合计算后的数据,所在的对象类型。
aggregationModelClass = CourseVo.class,
// 聚合计算类型,对从表的课时字段进行求和计算。
aggregationType = AggregationType.SUM,
// 参与聚合计算的从表字段,这里是课程表的课时字段。
aggregationField = "classHour")
@TableField(exist = false)
private Integer totalClassHour = 0;
// ... ... 为了缩减篇幅,省略了其他字段的定义。
}
// 本示例中的课程作业服务(course-paper)的CourseController接口类,会提供该API方法的实现。
// 这里需要强调的是,橙单自动生成的代码中,仍然会按照批量查询的方式,对远程对象数据进行汇总求和。
// 1. 根据班级主表的查询结果集,提取中classIds集合,并去重。
// 2. 在服务本地的中间表中,批量得到与这些classIds关联的课程Id(courseIds)集合。
// 3. 再将这些courseIds集合参数,通过一次远程调用获取远程聚合计算的结果。
// 4. 通过批量查询的方法,可以将远程RPC调用降低为一次,从而提升本次应答以及系统的整体运行时性能。
@FeignClient(
name = "course-paper",
configuration = FeignConfig.class,
fallbackFactory = CourseClient.CourseClientFallbackFactory.class)
public interface CourseClient extends BaseClient<CourseDto, CourseVo, Long> {
// 获取远程对象中符合查询条件的分组聚合计算Map列表。
@Override
@PostMapping("/course/aggregateBy")
ResponseResult<List<Map<String, Object>>> aggregateBy(@RequestBody MyAggregationParam aggregationParam);
// ... ... 为了缩减篇幅,省略了其他接口方法的定义。
}
结语
赠人玫瑰,手有余香,感谢您的支持和关注,选择橙单,效率乘三,收入翻番。