自学内容网 自学内容网

关于Mybatis中,IPage<PO>转换成IPage<VO>的问题

以下是一个比较常见通用的一个查询并且为单表查询,在开发初期,或者项目不是很复杂的时候,或者一开始项目框架就规划好的情况下,通常我们都会封装。

在我们的项目中,这部分代码其实是自动生成的,足以满足大部分的简单系统,尤其是你很喜欢单表查询的时候(个人写代码,比较排斥多表联合查询,性能太低),具体相关的怎么实现,这里不多说,可以参考以往的文章。
SpringBoot集成OpenAPI(Swagger3)和Mybatis-plus代码生成器
https://blog.csdn.net/m0_37892044/article/details/126154714
下面这是我们freemarker自动生成代码的框架代码

@ApiOperation(value = "按照条件进行分页查询${table.comment}")
@PostMapping("/info/page")
public R<IPage<${entity}>> page${entity}Info(@RequestBody PageQuery pageQuery) {
IPage<${entity}> page = queryService.queryByPage(${table.serviceName ? substring(1) ? uncap_first}, pageQuery);
return R.ok(page);
}

以下是我们项目中自动生成的代码

@ApiOperation(value = "6按照条件进行分页查询数据服务")
@PostMapping("/info/page")
@ApiOperationSupport(order = 6)
public R<IPage<DataService>> pageDataServiceInfo(@RequestBody PageQuery pageQuery) {
IPage<DataService> page = queryService.queryByPage(dataServiceService, pageQuery);
return R.ok(page);
}

以下是自动生成的接口,返回的分页查询结果。可以发现,其实最基本的分页查询功能已经实现了。但是现在有个问题,当数据在前端展示的时候,可能需要我们进行进一步转换,如下所示,上面部分是框架自动给我们实现的接口,并且能用,但是距离我们想要的有一点点差距。下面是我们实际想要的接口。

在这里插入图片描述
差距主要体现,自动生成的接口完全按照数据库来,例如type数据库存储300050001数字,但是用户在页面肯定是希望看到Mysql这个字符串,同理,请求参数和响应参数示例,在数据库都是一个字符串,并且接口返回的数据,是经过转码以后得字符串,但是用户实际上想看到的是Json对象。

相信大家经常都会遇到诸如此类,利用自己项目搭建的框架,或者分页框架,使用了一些框架自带的查询,但是对于查询结果,还需要做进一步转换才能返回给前端或者第三方。这里我就说一下我的使用感受,也算是寻求下,有没有其他的好的解决方案。

这个问题,我也一直很烦恼,有时候仅需要小改造,就能复用自动生成的代码,有时候为了复用自动生成的代码,结果代码改了一大堆,最后实在忍不了,干脆自己直接实现来的方便和好维护。

方案1:同对象同属性,直接修改原来的属性值

适用场景:就是你分页接口返回的属性和你要进行转换的数据属性是完全一样的。
例如,这里我接口返回分页对象是DataService,转换后也是DataService

修改方式1:直接去修改原来的属性值。例如下面dataViewId是字符串,该ID所对应的name也是字符串,那么直接把name的值赋值到id上即可。简单粗暴,就是代码可读性差,属性字段叫做ID,结果里面是name。

IPage<DataService> page = queryService.queryByPage(dataServiceService, pageQuery);
page.getRecords().forEach(temp->{
temp.setDataViewId(dataServiceService.getById(temp.getDataViewId()).getName();)
});

方案2:同对象不同属性,新增属性

修改方式2:为了不引起歧义,或者不方便修改原来的值,就从新增加一个字段,叫做dataViewName,把name属性赋值给dataViewName,如下所示,此外,下图中,数据服务的类型type这个字段明显是不能直接在原来的属性上修改,因为它是Integer 类型,只能赋值正数,因此我们也是新增一个字段typeName,并且均使用@TableField(exist = false)标明,这个不是数据库字段,如果不想返回原来的字段给前端,用@JsonIgnore屏蔽即可。

@ApiModelProperty("数据服务类型")
@TableField("type")
@DicCheck(dicGroup = DicGroupEnum.数据服务类型)
@JsonIgnore
private Integer type;
    
@TableField(exist = false)
private String typeName;

@ApiModelProperty("所属视图")
@TableField("data_view_id")
private String dataViewId;
    
@TableField(exist = false)
private String dataViewName;

此事下面代码这样转换。

IPage<DataService> page = queryService.queryByPage(dataServiceService, pageQuery);
page.getRecords().forEach(temp->{
temp.setDataViewName(dataServiceService.getById(temp.getDataViewId()).getName())
temp.setTypeName(dicTypeService.getById(temp.getType()).getName())
});

疑问:方案1、2大数据量怎么优化

可以发现,我们其实上面的两个例子中,都存在一个问题,那就是在for循环中去转换,正常分页一般也就10页,15页,循环个10次,15次的数据库操作,也不至于有啥大问题,就怕万一有人操作每页5000条,然后你循环个5000次查询数据库,那样肯定不行的。解决问题也简单,要么限制分页查询的,数量的大小,比如最多每页20,不然就得想办法解决掉for循环查数据库。
以下是一种很常见的方式:

优化1:实时查询的本地缓存

其思路就是,根据原始的结果,遍历一遍,找出需要转换的ID,然后在根据ID查询对应的名称,在将名称存入Hash,一般需要3次循环,两次数据库操作(假设只有一个字段转换)。对比优化前,是1次循环,以及一次循环的总数+1次的数据库操作。
比如,数据总量1万,优化前,是循环10001次的数据库操作,以及10000次的set操作。优化后是2次数据库操作,以及10000的add操作,10000次的put操作,10000次的set操作。而三次for循环,其实加起来也就3万次,3万次的这种简单的add,set,put操作,和1万次的操作,时间上几乎不会有差异,但是1万次的数据库操作,其差距是会尤其特别大,大到你怀疑人生。

public R<IPage<DataService>> pageDataServiceInfo(@RequestBody PageQuery pageQuery) {
//查询结果中某个字段为ID,但是在页面上需要显示此ID对应的名称
IPage<DataService> page = queryService.queryByPage(dataServiceService, pageQuery);

//从page结果中,获取所有需要转换的dataViewIds
Set<String> dataViewIds = new HashSet<>();
page.getRecords().forEach(temp -> dataViewIds.add(temp.getDataViewId()));

//查出dataViewIds对应的名称,存入本地缓存
Map<String,String> idToName = new HashMap<>();
List<DataService> list = dataServiceService.list(new QueryWrapper<DataService>().in("ID", dataViewIds));
list.forEach(temp -> idToName.put(temp.getId(), temp.getName()));

//将page结果中的dataViewId值,从新设置成名称
page.getRecords().forEach(temp->{
temp.setDataViewId(idToName.get(temp.getDataViewId()));
return R.ok(page);
}

优化2:分布式缓存替换本地缓存

上述的优化1中,我们每次都是去数据库查询(适用于数据库经常存在数据变化的情况),假设数据不常变化,我们可以考虑使用诸如redis这样的缓存
相对比与上面,其实就是减少了每次去实时查数据库,然后在将查询的结果转成hash,使用缓存数据库以后,可以直接将上述hash直接存到redis。伪代码如下所示。

public R<IPage<DataService>> pageDataServiceInfo(@RequestBody PageQuery pageQuery) {
//查询结果中某个字段为ID,但是在页面上需要显示此ID对应的名称
IPage<DataService> page = queryService.queryByPage(dataServiceService, pageQuery);

Map<String,String> idToName = redisService.getStrMap("data_view_id");

//将page结果中的dataViewId值,从新设置成名称
page.getRecords().forEach(temp->{
temp.setDataViewId(idToName.get(temp.getDataViewId()));
return R.ok(page);
}

优化3:枚举替换分布式/本地缓存

这种情况适用于,数据两较少,并且万年不变的这种,例如,性别,状态,类型,这种字典类型的数据。

public R<IPage<DataService>> pageDataServiceInfo(@RequestBody PageQuery pageQuery) {
//查询结果中某个字段为ID,但是在页面上需要显示此ID对应的名称
IPage<DataService> page = queryService.queryByPage(dataServiceService, pageQuery);
page.getRecords().forEach(temp->{
temp.setTypeName(DbTypeEnum.getNameById(temp.getType()));
return R.ok(page);
}

上面的情况都是基于IPage转IPage的问题,接下来说IPage转IPage的问题

方案3:不同对象,取出泛型在拷贝

不同对象,也就是说,我们自带的框架查询出来的对象,和我们要返回给前端的对象,就不一样。
我不知道有没有人尝试写过下面的代码。利用框架查询的分页对象,是数据库实体类DataService,而我们要返回给前端的是DataServiceRes,他两可能大部分属性一样,也可能DataServiceRes的属性是DataService的子集,不方便暴露DataService的所有数据,当然也可能是有字段不一样,需要转换,于是我们想能否使用copyProperties方法,将IPage拷贝给IPage,然后在修改IPage

public R<IPage<DataServiceRes>> pageDataServiceInfo(@RequestBody PageQuery pageQuery) {
IPage<DataService> page = queryService.queryByPage(dataServiceService, pageQuery);
IPage<DataServiceRes> pageVO = new Page<>();
BeanUtils.copyProperties(page, pageVO);
pageVO.getRecords().forEach(temp->{
temp.setTypeName(DbTypeEnum.getNameById(temp.getType()));
});
return R.ok(pageVO);
}

如果你这样做了,你将得到一个错误

java.lang.ClassCastException:DataService cannot be cast to DataServiceRes

当然,你要是把for循环注释掉,你会发现他不报错了。debug看一下,就会发现,虽然我们new的page是DataServiceRes,但是发现拷贝过来以后,是DataService
在这里插入图片描述
因为,IPage本身是泛型的,我们使用的
org.springframework.beans.BeanUtils.copyProperties,是没办法办法进行泛型的拷贝,因为是通过反射进行赋值的。以如下的源码所示,因此换个思路。既然无法拷贝泛型。那我们先取出泛型,在拷贝。

所以方法就是,从新创建一个对象,把原来对象的数据在复制过去,复制的时候,要避免泛型的出现,如果某个参数是泛型,那就先把这个参数取出来,创建一个新的参数去替换这个泛型参数。
在这里插入图片描述

public R<IPage<DataServiceRes>> pageDataServiceInfo(@RequestBody PageQuery pageQuery) {
IPage<DataService> page = queryService.queryByPage(dataServiceService, pageQuery);

//先通过属性拷贝,把page中的非泛型的分页参数拷贝到pageVO
IPage<DataServiceRes> pageVO = new Page<>();
BeanUtils.copyProperties(page, pageVO);

//通过page.getRecords()取出泛型数据
List<DataService> records = page.getRecords();

//将List<DataService> records拷贝到List<DataServiceRes> listRes(此时没有泛型了)
List<DataServiceRes> listRes = CopyUtils.copyList(records, DataServiceRes.class);

//修改转义需要的字段
listRes.forEach(temp-> {temp.setDataViewId("大王叫我来巡山");});

//替换pageVO中的Records
pageVO.setRecords(listRes);
return R.ok(pageVO);
}

CopyUtils.copyList()也是用的BeanUtils.copyProperties();

public static <T> List<T> copyList(List<?> listSource,Class<T> targetClazz){
    if (listSource == null){
        return new ArrayList<>();
    }
    List<T> listTarget = new ArrayList<>(listSource.size());
    listSource.forEach(temp ->{
     T newInstance;
try {
newInstance = targetClazz.newInstance();
BeanUtils.copyProperties(temp, newInstance);
listTarget.add(newInstance);
} catch (Exception e) {
e.printStackTrace();
} 
       });
return listTarget;
}

结果如下
在这里插入图片描述

方案4:Mybatis自带的convert

如下所示,Mybatis给我们提供了convert方法,我们可以在convert方法中返回转换后的对象

public R<IPage<DataServiceRes>> pageDataServiceInfo(@RequestBody PageQuery pageQuery) {
IPage<DataService> page = queryService.queryByPage(dataServiceService, pageQuery);
IPage<DataServiceRes> pageVO = page.convert(t->{
    /*
    DataServiceRes s = new DataServiceRes();
    BeanUtils.copyProperties(t, s);
    */
DataServiceRes s = CopyUtils.copyBean(t, DataServiceRes.class);
s.setReqExam(JsonUtils.strToJavaBean(t.getReqExam(),Object.class));
s.setResExam(JsonUtils.strToJavaBean(t.getResExam(),Object.class));
return s;
});
return R.ok(pageVO);
}

方案5:重构大法

改不下去了,干脆自己最后自己去从新写自己特定业务的分页查询了


原文地址:https://blog.csdn.net/m0_37892044/article/details/142785515

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!