一对多模型Diff

572 阅读4分钟

需求说明

非常常见的需求,一对多的模型中,前端会更新“多”端,后台常规做法是removeAll then insertAll!但有些时候就不能这样。

image.png

场景说明

电商的规格设计方案中,规格表attr,规格选项表attr_opt,映射表attr_2_opt。在这里我把规格attr和规格选项attr_opt做成池,规格和规格选项是两个池子里的鱼,需要两鱼交配的时候,就拿一个证书,记下两条鱼的名字id等信息,记在映射表attr_2_opt,然后简单给它俩走一下洞房仪式,最后鱼归鱼池,鳖归鳖池。

有人会问,鱼三和鳖五成亲,鱼三还和鳖六成亲,那鳖五和鳖六谁做大谁小。为了解决这个问题,在成婚证书里记下双方的一些信息。下图可见sort和hidden字段。。。

image.png

代码说明

常规做法,三下两除二

image.png 整体逻辑分三步走

1.判断前端参数是否为空,为空删除数据库映射表,并返回;

2.查出数据库已经存在的映射关系,循环遍历参数id,如果该id不存在数据库,放到insertList

3.第三步和第二步一样,找出要删除的id,放到deleteList

代码应付当前需求没啥问题,但项目内如果还有其他地方需要用到一对多逻辑,同样的代码就得复制一次,次数多了就触发SonarLint “Duplicated code fragment (X lines long)”。

此时心里要默念,不慌不慌,遇事不慌。这种需求很常见,把相同的逻辑抽象,做成工具类,实现多处服用,能有效提供摸鱼时间。下面工具类将一对多模型拆分成插入的inserList,删除的deletrList,更新的updateList,不做改动的sameList。

优化

第一步,创建枚举类型,用于标记动作

image.png

第二步,我们的目的是抽象,让这个工具类能服务所有的对象,但对象的结构不一样,这种时候可以考虑用范型或者创建中间对象。本案例结合两种方式,创建ContentValue,它是一个<K,V>结构,但不是Map。key用于区分是否插入,value存储存储的是具体的值,用于区分是否更新,type标记动作类型

image.png

第三步,工具类编写。t1是数据库集合,t2是前端参数。先把数据库集合t1全部标记成delete状态塞到map中,遍历参数集合t2,如果map没有,说明是新增元素,如果有,通过ContentValue判断是update还是same。值得注意的是,else里面value的比较用到equals方法,所以你的对象需要重写equals方法

image.png

第四步,调用工具类

image.png

image.png 四步走到这里就完了。此时你定睛一看,写了这么多,调用工具类也没省功夫啊!!!花了这么多时间,工作量不减反增???身为摸鱼办主任的我早已忍不住了,连忙把第二个优化甩在隔壁脸上。。。

回归正题,祭上函数式接口,使其完美。

第五步,创建新的doDiff,相比老doDiff增加一个Map,map的key是动作,value是BiConsumer接收两个参数,这两个参数在一对多模型里,分别对应了一方的id,和多方的集合。新的doDiff调用老的doDiff,然后创建三个list,分别对应insert,update,delete,然后遍历拆分,最后调用BiConsumer完成CURD

image.png 调用工具类。注意最下面attrRepository三个方法,需要调用工具类的地方,代码简洁很多。

image.png 总结一下用法:

1.创建用于hash的key对象,存储用于比较出insert,update,delete的fields信息,并且需要重写hashCode和equals方法

2.构建actionMap

3.调用

贴代码

DiffUtils

/**
 * @author robotto
 * @version 1.0
 * @date 2022/8/13 21:21
 **/
public class DiffUtils {

    public static <K,V> Map<K, ContentValue<K,V>> doDiff(List<ContentValue<K,V>> t1, List<ContentValue<K,V>> t2) {
        Map<K, ContentValue<K,V>> map = new HashMap<>(t1.size() + t2.size());
        t1.forEach(t -> map.put(t.getKey(), ContentValue.of(t.getValue(), Type.DELETE)));
        t2.forEach(t -> {
            ContentValue<K,V> model = map.get(t.getKey());
            if (null == model) {
                map.put(t.getKey(), ContentValue.of(t.getValue(), Type.INSERT));
            } else {
                if (model.getValue().equals(t.getValue())) {
                    map.put(t.getKey(), ContentValue.of(t.getValue(), Type.SAME));
                } else {
                    map.put(t.getKey(), ContentValue.of(t.getValue(), Type.UPDATE));
                }
            }
        });
        return map;
    }

    public static <K,V,O> void doDiff(List<ContentValue<K,V>> t1, List<ContentValue<K,V>> t2, O id,
                                                         Map<DiffUtils.Type, BiConsumer<List<V>, O>> actionMap) {
        Map<K, ContentValue<K,V>> map = doDiff(t1, t2);

        List<V> batchInsert = new ArrayList<>(6);
        List<V> batchUpdate = new ArrayList<>(4);
        List<V> batchDelete = new ArrayList<>(4);
        for (Map.Entry<K, ContentValue<K,V>> entry : map.entrySet()) {
            if (DiffUtils.Type.INSERT.equals(entry.getValue().getType())) {
                batchInsert.add(entry.getValue().getValue());
            } else if (DiffUtils.Type.UPDATE.equals(entry.getValue().getType())) {
                batchUpdate.add(entry.getValue().getValue());
            } else if (Type.DELETE.equals(entry.getValue().getType())) {
                batchDelete.add(entry.getValue().getValue());
            }
        }
        Optional.ofNullable(actionMap.get(Type.INSERT)).ifPresent(e -> e.accept(batchInsert, id));
        Optional.ofNullable(actionMap.get(Type.UPDATE)).ifPresent(e -> e.accept(batchUpdate, id));
        Optional.ofNullable(actionMap.get(Type.DELETE)).ifPresent(e -> e.accept(batchDelete, id));
    }

    public static class ContentValue<K,V> {
        private K key;
        private V value;
        private Type type;

        ContentValue(K key, V value, Type type) {
            this.key = key;
            this.value = value;
            this.type = type;
        }

        public static <K,V> ContentValue<K,V> of(K k, V v) {
            return new ContentValue<>(k, v, null);
        }

        public static <K,V> ContentValue<K,V> of(V v, Type type) {
            return new ContentValue<>(null, v, type);
        }

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }

        public Type getType() {
            return type;
        }
    }

    public enum Type {
        SAME,
        INSERT,
        UPDATE,
        DELETE,
        IGNORE
    }
}

AttrOptInnerReqCmd

@Data
@EqualsAndHashCode(callSuper = false)
public static class AttrOptInnerReqCmd extends BaseReqCmd {
    private static final long serialVersionUID = 1493735637701078497L;
    private Integer id;
    private Integer sort;
    private Boolean hidden;

    public AttrOptInnerReqCmd(Integer id, Integer sort, Boolean hidden) {
        this.id = id;
        this.sort = sort;
        this.hidden = hidden;
    }

    public static AttrReqCmd.AttrOptInnerReqCmd of(Integer id, Integer sort, Boolean hidden) {
        return new AttrReqCmd.AttrOptInnerReqCmd(id, sort, hidden);
    }
}

调用代码,repository替换自己的逻辑

private void connectAttrOpt(AttrPo attrPo, List<AttrReqCmd.AttrOptInnerReqCmd> paramAttrOpts) {
    Integer attrId = attrPo.getId();
    // 前端参数空或文本框删除远关联数据
    if (null == paramAttrOpts || paramAttrOpts.isEmpty() || !attrPo.getFieldType().connectable()) {
        // 批量删除关联关系
        attrRepository.attr2OptDel(Collections.singletonList(attrId));
        return;
    }

    List<DiffUtils.ContentValue<Integer, AttrReqCmd.AttrOptInnerReqCmd>> attrOptsFromDb = attrRepository.attrOptListByAttr(attrId).stream().map(attr -> {
        AttrReqCmd.AttrOptInnerReqCmd attrInnerReqCmd = AttrReqCmd.AttrOptInnerReqCmd.of(attr.getId(), attr.getSort(), attr.getHidden());
        return DiffUtils.ContentValue.of(attrInnerReqCmd.getId(), attrInnerReqCmd);
    }).collect(Collectors.toList());

    List<DiffUtils.ContentValue<Integer, AttrReqCmd.AttrOptInnerReqCmd>> attrOptsFromParams = paramAttrOpts.stream().map(attr -> {
        AttrReqCmd.AttrOptInnerReqCmd attrInnerReqCmd = AttrReqCmd.AttrOptInnerReqCmd.of(attr.getId(), attr.getSort(), attr.getHidden());
        return DiffUtils.ContentValue.of(attrInnerReqCmd.getId(), attrInnerReqCmd);
    }).collect(Collectors.toList());

    Map<DiffUtils.Type, BiConsumer<List<AttrReqCmd.AttrOptInnerReqCmd>, Integer>> actionMap = new EnumMap<>(DiffUtils.Type.class);
    actionMap.put(DiffUtils.Type.INSERT, (k,v) -> attrRepository.attr2OptBatchInsert(this.exchange2Attr2Opt(k,v)));
    actionMap.put(DiffUtils.Type.UPDATE, (k,v) -> attrRepository.attr2OptBatchUpdate(this.exchange2Attr2Opt(k,v)));
    actionMap.put(DiffUtils.Type.DELETE, (k,v) -> attrRepository.attr2OptBatchDelete(this.exchange2Attr2Opt(k,v)));
    DiffUtils.doDiff(attrOptsFromDb, attrOptsFromParams, attrId, actionMap);
}

`