字数:6137

引文

曾听说把一件事情做到极致,或许就是取胜之道其一。本文不是想要论述这个道理,只有感于组员在工作中细节部分不够到位,这种不到位并不是方案 的问题、也不是技术深度的问题,仅仅就是有没有多想一点,类似的总结,这里也有。虽然项目工作量大,人力吃紧,但是 做任何东西先要自己这关吧?

1 起因

后台管理程序要开发一个接口,功能就是将绘本的信息入库。包括几大块:

  • 书本的基本信息,名称、出版属性等等

  • 书本的作者,一对多

  • 书本定价,由于有多种定价,还要考虑不设限的扩展空间,也是一对多

  • 书籍的分类,一对多

  • 书籍获得的奖项,一对多

本文要聊的就是组员在实现后面4个一对多时的一些细节做法,以及存在的问题,这些东西我也当然找他们聊了,以求后续工作中不出现这种比较一般的问题。

2 问题

2.1 参数的问题

下面是书籍获得的奖项参数,用对象封装:

@Valid
public class FrmBookAward {

    @Column(name="id")
    @JsonView(SimpleView.class)
    long id;
    
    @Column(name="id_book")
    @JsonView(SimpleView.class)
    long idBook;

    @Column(name="id_award")
    @JsonView(SimpleView.class)
    long idAward;
  • 问题1,@Column@JsonView等注解在这里是无用的,应该去掉

  • 问题2,long id是对应关系数据行的键值,一开始网页端并不知道,idBook也是一样的情况。如果仅是传递多个idAward,大可以用long[]数组就好了。 不必要用对象列表

2.2 更新操作的问题

下列是执行具体更新的代码:

 /**
 * 图书表对应奖项信息
 * @param frmBookAwards
 * @param idBook
 */
@Override
//标示4,由于此方法内更新了数据,是可以直接更新缓存的,而这里选择剔除缓存,是为了下次查的时候再次缓存,有点回避。
更关键的是,这里缓存的是对应关系,而奖项的基本信息,比如名称,还需要再次查询,何不直接缓存奖项的基本信息,这个基本信息才是被大量读的内容
@CacheEvict(value = "ggh", key = "'book_award_'.concat(#idBook)")
public void saveAllAward(List<FrmBookAward> frmBookAwards, long idBook) {
    List<BookAward> bookAwardList = new ArrayList<>();
    List<Long> ids = new ArrayList<>();
    for (FrmBookAward frmBookAward : frmBookAwards) {
        BookAward bookAward = utils.createEntity(BookAward.class);
        bookAward.setIdBook(idBook);
        if(frmBookAward.getId() == 0){
            BeanUtils.copyProperties(frmBookAward, bookAward, ArrayUtils.add(CommConst.DO_NOT_REPLACE, "idBook"));
        }else{
            BeanUtils.copyProperties(frmBookAward, bookAward, ArrayUtils.add(CommConst.DO_NOT_REPLACE_ID, "idBook"));
        }
        bookAwardList.add(bookAward);
        //添加ids
        ids.add(bookAward.getIdAward());
        //标示2,如果里面有错误id,即后续的检验通不过,则此为不必要的构造对象列表 
    }
    //查询数据库中是否存在id_category
    int listSize = awardRepository.countByIdIn(ids);
    //如果二者的数值不相等,可以认为有数据库不存在的id存在
    if(listSize != bookAwardList.size() ){
        throw  new  MyException(400,"奖项id有误");
    }
    //查询数据库中存在的信息,对比传入值,如果id对应不上则删除
    //标示5,findBookAwardByIdBook原本想读缓存里的内容,而在同一个类里,是无效的
    List<BookAward> bookAwards = findBookAwardByIdBook(idBook);
    List<BookAward> notExists = new ArrayList<>();
    for (BookAward bookAward : bookAwards) {
        //是否存在
        boolean f =false;
        //标示3,这里才是最无聊的,bookAwardList里构造的对象的ID全是重新生成的,是不可能相等的,所以
        结果就是数据库中的每条数据都会被逻辑删除
        for (BookAward paramBookAward : bookAwardList){
            if(bookAward.getId() == paramBookAward.getId()){
                f = true;
                //标示1,这里是可以break跳出的
            }
        }
        if(!f){
            bookAward.setDelete(true);
            notExists.add(bookAward);
        }
    }
    bookAwardList.addAll(notExists);
    bookAwardRepository.saveAll(bookAwardList);
}
  • 问题3,标示1,这里是可以break跳出的

  • 问题4,标示2,如果里面有错误id,即后续的检验通不过,则此为不必要的构造对象列表

  • 问题5,标示3,这里才是最无聊的,bookAwardList里构造的对象的ID全是重新生成的,是不可能相等的,所以 结果就是数据库中的每条数据都会被逻辑删除。这也是不必要的,如果存在的理应不更新,插入会对数据库影响不少,另外也会产生垃圾数据。

为了解决这个问题,如果仅id数据都还好办,而如果像书籍作者,对应信息有type(类型,比如著、图、文;作者姓名;国别),如何比对对象呢? 图方便的话,修改一下对应的equals和hashCode方法

  • 问题6,标示4,由于此方法内更新了数据,是可以直接更新缓存的,而这里选择剔除缓存,是为了下次查的时候再次缓存,有点回避。 更关键的是,这里缓存的是对应关系,而奖项的基本信息,比如名称,还需要再次查询,何不直接缓存奖项的基本信息, 这个基本信息才是被大量读的内容。

  • 问题7,标示5,findBookAwardByIdBook原本想读缓存里的内容,而在同一个类里,是无效的

2.3 更新没考虑业务的特点

书籍与价格的一对多,小伙也是如出一辙的处理,其实不能这样,价格有很多种——‘原借阅价’,‘现借阅价’,‘会员价’,‘活动价1’,‘活动价2’,‘活动价3’, 而在这个接口对应的前端页面只涉及到其中的部分,比如’原借阅价’,‘现借阅价’。

  • 问题8,上述实现会逻辑删除别处设置的价格,其实只要覆盖页面涉及的价格即可

3 第1次重构

基于以上的问题,所以我实现的版本的是:

@Override
@Caching(put = {
        @CachePut(value = "ggh", key = "'book_awards_'.concat(#bookAwards.get(0).idBook)")
})
public List<Award> saveBookAwards(List<BookAward> bookAwards) {
    if (bookAwards == null || bookAwards.isEmpty()) return null;
    //更新关联
    bookAwardRepository.saveAll(bookAwards); //这里可能含有isdelete的数据
    Iterable idsAward = new ArrayList<>();

    for (BookAward bookAward : bookAwards) {
        if (!bookAward.isDelete()) {
            ((ArrayList) idsAward).add(bookAward.getIdAward());
        }
    }
    //查awards基本信息
    List<Award> awards = (List<Award>)awardRepository.findAllById(idsAward);
    return awards;
}

而业务判断放在service方法中:

//3 保存奖项
long[] frmBookAwardIds = frmBook.getBookAwards();
boolean isEmptyFrmBookAwardIds = (frmBookAwardIds == null) || (frmBookAwardIds.length == 0);

//检查所有奖项编号是否合法
if (!isEmptyFrmBookAwardIds) {
    int validAmountExists = awardRepository.countByIdIn(frmBookAwardIds);

    if (validAmountExists != frmBookAwardIds.length) {
        throw new MyException(400, "存在非法奖项id");
    }
}
if (frmBookAwardIds == null) { //初始化后,可直接用
    frmBookAwardIds = new long[0];
}
List<BookAward> existsBookAwards = bookAwardRepository.findByIdBook(existsBook.getId());
boolean isEmptyExistsBookAwards = (existsBookAwards == null) || (existsBookAwards.isEmpty());

if (!isEmptyExistsBookAwards) {
    for (BookAward existsBookAward : existsBookAwards) {
        if (!ArrayUtils.contains(frmBookAwardIds, existsBookAward.getIdAward())) {
            existsBookAward.setDelete(true);
        } else {
            //已比对的,排除,剩下的即数据库中不存在,要新增
            frmBookAwardIds = ArrayUtils.removeElement(frmBookAwardIds, existsBookAward.getIdAward());
        }
    }
}
if (frmBookAwardIds.length > 0 && existsBookAwards == null) {
    existsBookAwards = new ArrayList<>();
}

//新增
for (long frmBookAwardId : frmBookAwardIds) {
    BookAward newBookAward = utils.createEntity(BookAward.class);
    newBookAward.setIdBook(existsBook.getId());
    newBookAward.setIdAward(frmBookAwardId);
    newBookAward.setDelete(false);
    newBookAward.setLastTime(new Timestamp(System.currentTimeMillis()));
    existsBookAwards.add(newBookAward);
}
//更新到数据库中
if (existsBookAwards != null && !existsBookAwards.isEmpty()) {
    cacheMgrService.saveBookAwards(existsBookAwards);
    existsBook.setBookAwards(cacheMgrService.findAwardsOfBook(existsBook.getId()));
}

4 第2次重构

虽然第1次重构使逻辑清晰化了,但是后来在用的过程中还是发现有一点点不能满足使用:

  • 虽然缓存了书籍其它对应关系的原生信息,但是要拿书籍的所有信息,必然还要包含这些原生信息,如何给前端?显然我们要在大的book对象 里包含这些。这样,bean对象要加list属性字段了,初始化时,要读取出对应关系的原生信息,并赋值给list属性字段。

  • 缓存的时候,为什么不直接缓存书籍呢?这样做(1)能减少缓存的项目(2)不用考虑get book接口的缓存同步更新问题

这样第2次重构又势在必行了,还是以上述的奖项为例,核心重构代码比如变为:

@Override
@Caching(put = {
        //直接更新书籍缓存,而非对应关系的原生数据
        @CachePut(value = "ggh", key = "'book_idbook_'.concat(#newBook.id)"),
        @CachePut(value = "ggh", key = "'book_isbn_'.concat(#newBook.publishIsbn)")
})
public Book saveBookAwards(Book newBook) {
    //把要更新的对应关系数据(数据库和表单双向对比之后的确认结果)取出来
    List<BookAward> bookAwards = newBook.getBookAwardsRelative();

    if (bookAwards == null || bookAwards.isEmpty()) return null;
    //更新关联
    bookAwardRepository.saveAll(bookAwards); //这里可能含有isdelete的数据
    Iterable idsAward = new ArrayList<>();

    for (BookAward bookAward : bookAwards) {
        if (!bookAward.isDelete()) {
            ((ArrayList) idsAward).add(bookAward.getIdAward());
        }
    }
    //查awards基本信息
    List<Award> awards = (List<Award>) awardRepository.findAllById(idsAward);
    newBook.setBookAwards(awards);
    return newBook;
}

5 结语

由上可见,虽然功能简单,如没想仔细,也可能做岔了,程序不健壮,事情还得反复。