一、背景
我们经常遇到一个场景:一个列表展示的数据来源于多张表甚至是多个数据源,例如sku列表除了展示sku相关数据,还要展示spu的字段、库存单位、规格等数据,而且不同的场景(不同的页面)所需要的数据是不一样的,有可能spu列表只需要展示sku和spu的信息,但是spu详情页面可能除了展示spu和sku的信息之外还要展示价格的信息、库存的信息等多个中心多个数据源的信息。不同的页面不同的场景可能有不同信息的聚合。我们不可能针对这些不同的数据组合编写不同的接口去封装(开发成本大,复用性差),也不可能返回一个大而全的数据(返回了大量不需要的数据,消耗性能和带宽等)
因此我们可以编写一个Wrapper,这个Wrapper是可以复用的,其他场景需要组装就使用该wrapper去组装,例如sku列表需要展示spu相关的字段,则编写spuApiWrapper,然后使用该wrapper去组装数据。
二、解决方案
1. 提供一个大而全的接口,返回所有的聚合数据字段。例如sku聚合接口,同时返回商品、价格、库存等十几个字段的信息,由各场景自己组装所需的数据。
优点:简单粗暴,减少场景查询频率和次数,减少一定的网络开销,在一定程度上减少场景层的代码编写
缺点:可能有些简单的页面,只需要聚合一两个额外的字段信息,例如以后移动端sku列表可能只需要额外聚合多一个【商品名称】的字段,其他的如价格库存等其他字段都是不需要的,这样其实也会造成一定的网络开销成本,并且每次新增其他类型的数据或者其他字段,都要调整该接口,这个接口的修改的频率会很大,不稳定,影响上游服务。而且这个接口的数据量不会收敛,一直是在递增,以后数据量太大会有更多的问题暴露出来。
2. 针对每个页面不同的业务场景,都开发一个单独的接口,例如sku列表只有库存、价格,那就组装库存价格,sku详情只有商品信息,那就组装商品信息。每个接口只返回这个场景仅用到的字段和数据
优点:接口不会返回过多冗余的数据,减少数据传输的成本,由于每个查询接口都只对一个业务场景,那么当某个接口有问题,也不会影响到其他业务场景的查询,同时也减少修改的频率和次数,提高稳定性。
缺点:接口的复用性太差,需要针对每个业务场景都要编写一个接口,提高开发成本。一般来说,中心层因为要对接不同的平台不同的业务场景,其提供的接口很大的一个原则就是要保证其复用性,尽量减少个性化接口的开发。
3. 由于方案1和2都有各自的缺点和优点,也不好划分接口的细粒度,因此这里提供第三种解决方案,针对不同的实体提供不同的wrapper,由各业务各场景自己组装所需要的数据,每个实体的wrapper只需要做一次开发,即可复用多个场景和页面
优点:提高聚合的灵活性,只需要组装自己所需要的数据或者字段,细粒到字段的粒度,减少冗余字段的返回,减少网络传输成本,由于每个wrapper只需要编写一次即可复用在不同的业务场景,在一定程度上可以减少代码的开发。
缺点:相对方案1一次网络开销,这种方案可能会有多次网络开销,每组合一次可能就会多一次网络开销成本,这种方案也有一定的局限性,不支持跨表多条件分页查询的场景,也不支持中间关联表一次性查询,可能需要多次查询等,仅适合简单数据的组装。
总结:以上三种方案有各自的优点,因此可以结合起来使用。个人建议:
1. 简单的数据组装不涉及跨表查询的可以使用wrapper组装 ,如sku需要展示spu名称的字段
2.复杂的数据组装涉及跨表多条件查询的就单独写一个接口,接口粒度看业务场景。
3.页面展示的数据很全,并且访问频率也比较高的,也是单独写一个接口,例如spu详情接口,因为该页面同时展示属性、属性项、品类信息、单位信息、sku信息、多媒体信息
a.
/**
* 数据组装Wrapper
*/
public
abstract
class
BaseApiWrapper<T> {
/**
* 列表查询
*/
protected
abstract
List<T> selectList(Map<String, Object> params);
/**
* 单个查询
*/
protected
abstract
T selectOne(Long id);
/**
* 获取T泛型对象的ID
*/
protected
abstract
Function<T, Long> idFunction();
/**
* 单个组装
*
* @param r 需要组装的目标对象
* @param function 获取id的function
* @param consumer 数据组装consumer
* @param <E> 需要组装的目标对象的泛型
* @return
*/
public
<E> ResultDTO<E> setOne(ResultDTO<E> r, Function<E, Long> function, BiConsumer<E, T> consumer) {
if
(!Objects.equals(r.getCode(), ResultDTO.ok().getCode())) {
return
r;
}
setOne(r.getData(), function, consumer);
return
r;
}
/**
* 单个组装
*
* @param function 获取id的function
* @param consumer 数据组装consumer
* @param <E> 需要组装的目标对象的泛型
* @return
*/
public
<E> E setOne(E e, Function<E, Long> function, BiConsumer<E, T> consumer) {
Long id = function.apply(e);
T t = selectOne(id);
if
(t !=
null
) {
consumer.accept(e, t);
}
return
e;
}
/**
* 列表组装
*
* @param r 需要组装的目标对象集合
* @param function 获取id的function
* @param consumer 数据组装consumer
* @param <E> 需要组装的目标对象的泛型
* @return
*/
public
<E> ResultDTO<List<E>> setList(ResultDTO<List<E>> r, Function<E, Long> function, BiConsumer<E, T> consumer) {
if
(!Objects.equals(r.getCode(), ResultDTO.ok().getCode())) {
return
r;
}
setList(r.getData(), function, consumer);
return
r;
}
/**
* 列表组装
*
* @param entities 需要组装的目标对象集合
* @param function 获取id的function
* @param consumer 数据组装consumer
* @param <E> 需要组装的目标对象的泛型
* @return
*/
public
<E> List<E> setList(List<E> entities, Function<E, Long> function, BiConsumer<E, T> consumer) {
if
(CollectionUtils.isEmpty(entities)) {
return
entities;
}
List<String> ids = entities.stream().map(function).filter(Objects::nonNull).map(id -> id.toString()).collect(Collectors.toList());
HashMap<String, Object> map =
new
HashMap<>();
map.put(
"qp-id-in"
, StringUtils.join(ids));
List<T> list = selectList(map);
if
(CollectionUtils.isNotEmpty(list)) {
Map<Long, T> resDtoMap = list.stream().collect(Collectors.toMap(idFunction(), e -> e));
entities.stream().filter(e -> resDtoMap.containsKey(function.apply(e))).forEach(e -> consumer.accept(e, resDtoMap.get(function.apply(e))));
}
return
entities;
}
/**
* 分页组装
*
* @param r 需要组装的目标对象集合
* @param function 获取id的function
* @param consumer 数据组装consumer
* @param <E> 需要组装的目标对象的泛型
* @return
*/
public
<E> ResultDTO<PagerDTO<E>> setPage(ResultDTO<PagerDTO<E>> r, Function<E, Long> function, BiConsumer<E, T> consumer) {
if
(!Objects.equals(r.getCode(), ResultDTO.ok().getCode())) {
return
r;
}
setPage(r.getData(), function, consumer);
return
r;
}
/**
* 分页组装
*
* @param function 获取id的function
* @param consumer 数据组装consumer
* @param <E> 需要组装的目标对
* @return
*/
public
<E> PagerDTO<E> setPage(PagerDTO<E> pager, Function<E, Long> function, BiConsumer<E, T> consumer) {
if
(CollectionUtils.isEmpty(pager.getList())) {
return
pager;
}
List<E> list = pager.getList();
setList(list, function, consumer);
return
pager;
}
}
b.c因为要组装spuName,所以需要编写一个SpuApiWrapper
@Builder
public
class
SpuApiWrapper
extends
BaseApiWrapper<SpuResDto> {
@Override
public
List<SpuResDto> selectList(Map<String, Object> params) {
//这里可以使用feign调用,这里为了方便测试,使用的是本地化调用
SpuAppService appService = SpringUtil.getBean(SpuAppService.
class
);
return
appService.selectList(params);
}
@Override
public
SpuResDto selectOne(Long id) {
//这里可以使用feign调用,这里为了方便测试,使用的是本地化调用
SpuAppService appService = SpringUtil.getBean(SpuAppService.
class
);
return
appService.selectOne(id);
}
@Override
protected
Function<SpuResDto, Long> idFunction() {
return
SpuResDto::getId;
}
}
c.业务层、场景层、应用层的编写
@ApiOperation
(
"列表查询"
)
@GetMapping
(produces = {
"application/json"
}, value =
"getSkuList"
)
public
ResultDTO<List<SkuResDto>> getSkuList(
@ApiIgnore
@RequestParam
Map<String, Object> params) {
List<SkuResDto> skuResDto = skuAppService.selectList(params);
//组装Spu名称
SpuApiWrapper.builder().build().setList(skuResDto, SkuResDto::getSpuId, (skuDto, spuDto) -> {
skuDto.setSpuName(spuDto.getName());
});
// 组装其他的wrapper
// XXXApiWrapper.builder().build().setList(XXXResDto, XXXResDto::getXXXId, (skuDto, XXXDto) -> {
// skuDto.setXXX(XXXDto.getXXX());
// });
return
ResultDTO.ok(skuResDto);
}