数据组装:BaseApiWrapeer使用方式

一、背景

       我们经常遇到一个场景:一个列表展示的数据来源于多张表甚至是多个数据源,例如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);

    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值