需求说明
非常常见的需求,一对多的模型中,前端会更新“多”端,后台常规做法是removeAll then insertAll!但有些时候就不能这样。
场景说明
电商的规格设计方案中,规格表attr,规格选项表attr_opt,映射表attr_2_opt。在这里我把规格attr和规格选项attr_opt做成池,规格和规格选项是两个池子里的鱼,需要两鱼交配的时候,就拿一个证书,记下两条鱼的名字id等信息,记在映射表attr_2_opt,然后简单给它俩走一下洞房仪式,最后鱼归鱼池,鳖归鳖池。
有人会问,鱼三和鳖五成亲,鱼三还和鳖六成亲,那鳖五和鳖六谁做大谁小。为了解决这个问题,在成婚证书里记下双方的一些信息。下图可见sort和hidden字段。。。
代码说明
常规做法,三下两除二
整体逻辑分三步走
1.判断前端参数是否为空,为空删除数据库映射表,并返回;
2.查出数据库已经存在的映射关系,循环遍历参数id,如果该id不存在数据库,放到insertList
3.第三步和第二步一样,找出要删除的id,放到deleteList
代码应付当前需求没啥问题,但项目内如果还有其他地方需要用到一对多逻辑,同样的代码就得复制一次,次数多了就触发SonarLint “Duplicated code fragment (X lines long)”。
此时心里要默念,不慌不慌,遇事不慌。这种需求很常见,把相同的逻辑抽象,做成工具类,实现多处服用,能有效提供摸鱼时间。下面工具类将一对多模型拆分成插入的inserList,删除的deletrList,更新的updateList,不做改动的sameList。
优化
第一步,创建枚举类型,用于标记动作
第二步,我们的目的是抽象,让这个工具类能服务所有的对象,但对象的结构不一样,这种时候可以考虑用范型或者创建中间对象。本案例结合两种方式,创建ContentValue,它是一个<K,V>结构,但不是Map。key用于区分是否插入,value存储存储的是具体的值,用于区分是否更新,type标记动作类型
第三步,工具类编写。t1是数据库集合,t2是前端参数。先把数据库集合t1全部标记成delete状态塞到map中,遍历参数集合t2,如果map没有,说明是新增元素,如果有,通过ContentValue判断是update还是same。值得注意的是,else里面value的比较用到equals方法,所以你的对象需要重写equals方法
第四步,调用工具类
四步走到这里就完了。此时你定睛一看,写了这么多,调用工具类也没省功夫啊!!!花了这么多时间,工作量不减反增???身为摸鱼办主任的我早已忍不住了,连忙把第二个优化甩在隔壁脸上。。。
回归正题,祭上函数式接口,使其完美。
第五步,创建新的doDiff,相比老doDiff增加一个Map,map的key是动作,value是BiConsumer接收两个参数,这两个参数在一对多模型里,分别对应了一方的id,和多方的集合。新的doDiff调用老的doDiff,然后创建三个list,分别对应insert,update,delete,然后遍历拆分,最后调用BiConsumer完成CURD
调用工具类。注意最下面attrRepository三个方法,需要调用工具类的地方,代码简洁很多。
总结一下用法:
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);
}
`