Spring Boot 通用对象列表比较和去重
1、前言
在Excel批量导入数据时,常常需要与数据库中已存在数据的比较,并且需要考虑导入数据重复的可能性。
导入的行数据,一般有一个实体类(对象)与之对应,往往这个实体类在数据库中的字段要比导入数据更多,如主键ID字段,这个ID字段一般不会出现在导入行数据中,此时导入的对象使用其它的唯一键来识别,如导入某个单位的用户数据,用用户名或手机号来唯一识别用户,而不会有用户ID字段(此时新用户的用户ID还有待系统来分配)。
批量导入数据,往往需要与已存在对象数据进行比较,新的对象使用insert操作,已有对象使用update操作。但如果逐条数据,都先查询数据库,再决定是insert,还是update,是非常低效的,代码也会很臃肿。使用类似Mysql的ON DUPLICATE KEY UPDATE ,是一种解决方案,但在使用全局ID时,可能存在ID主键与其它唯一主键冲突的情况,导致update失败。
因此,稳妥的作法,是比较导入对象集与存在对象集,比较的结果产生了4个集合:新增数据集合、属性值完全相同的对象集合、对象键值相同但属性有变化的对象集合、剩余对象集合。
一般批量导入数据,会有一个限定,如导入用户数据,限定导入某一个单位的,这样比较的数据集就不会太大。
因此,对象列表的比较,是批量数据导入的基础性功能。另外,导入数据,首先要消除数据中重复的对象,这些重复的对象数据是异常数据。
2、解决方案
对象列表比较,应支持通用实体类对象,这样可实现代码的复用,消除低效重复的针对具体实体类的代码。因此需要支持泛型,且使用反射机制。
另外,对象比较,是一组属性字段值比较,包括键值与普通属性值,键值用于识别对象。
代码如下:
package com.abc.example.common.utils;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
/**
* @className : ObjectListUtil
* @description : 对象列表工具类
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/08/15 1.0.0 sheng.zheng 初版
*
*/
@Slf4j
public class ObjectListUtil {
/**
*
* @methodName : compareTwoList
* @description : 比较两个对象列表,新列表与旧列表对比,根据比较的属性字段字典,比较结果
* 1、新的对象;2、键值相同,属性不同,需要修改的对象;3、属性完全相同
* 这样有新增对象列表,修改对象列表,相同对象列表,剩余对象列表
* @param <T> : 泛型类型T
* @param fieldMap : 比较字段字典,key为字段名称,value为是否键值,1表示键值字段,0为普通字段。
* @param newList : 新列表,要求键值无重复
* @param oldList : 旧列表,要求键值无重复,在方法中将被改变
* @param addList : 新增对象列表
* @param sameList : 相同对象列表
* @param updateList: 修改对象列表
* @param remainderList: 剩余对象列表
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/08/15 1.0.0 sheng.zheng 初版
*
*/
public static <T> void compareTwoList(Map<String,Integer> fieldMap,
List<T> newList,List<T> oldList,
List<T> addList, List<T> sameList,
List<T> updateList, List<T> remainderList) {
// 遍历新列表
for(int i = 0; i < newList.size(); i++) {
T newItem = newList.get(i);
// 标记对象是否匹配
boolean found = false;
// 遍历旧列表,倒序
for (int j = oldList.size() - 1; j >= 0; j--) {
T oldItem = oldList.get(j);
// 比较两个对象
int compare = compareTwoItem(fieldMap,newItem,oldItem);
if (compare == 1) {
// 如果两个对象相同,加入相同列表中
sameList.add(newItem);
// 从列表中移除
oldList.remove(j);
found = true;
// 结束本轮遍历
break;
}else if(compare == 2) {
// pass
}else if(compare == 3) {
// 匹配对象,但属性不同,加入修改表中
updateList.add(newItem);
// 从列表中移除
oldList.remove(j);
found = true;
// 结束本轮遍历
break;
}else {
// 发生异常
return;
}
}
if (found == false) {
// 如果本轮遍历,未找到匹配项,加入新增列表中
addList.add(newItem);
}
}
// oldList中剩余的项,加入剩余列表中
for(int i = 0; i < oldList.size(); i++) {
T oldItem = oldList.get(i);
remainderList.add(oldItem);
}
}
/**
*
* @methodName : compareTwoItem
* @description : 比较两个相同类型的对象
* @param <T> : 泛型类型T
* @param fieldMap : 比较字段字典,key为字段名称,value为是否键值,1表示键值字段。
* @param newItem : 新的对象
* @param oldItem : 旧的对象
* @return : 返回值定义如下:
* 0 : 数据处理异常
* 1 : 对象完全相同(比较字段的字段值都相同)
* 2 : 对象不同(键值字段的字段值存在不相同)
* 3 : 对象相同,属性不同(键值字段的字段值相同,但属性值不同)
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/08/15 1.0.0 sheng.zheng 初版
*
*/
@SuppressWarnings("unchecked")
public static <T> int compareTwoItem(Map<String,Integer> fieldMap,T newItem, T oldItem) {
int retCode = 1;
try {
for (Map.Entry<String,Integer> entry : fieldMap.entrySet()) {
// 取得字段名称
String fieldName = entry.getKey();
Integer keyFlag = entry.getValue();
// 取得新对象的当前字段值
Class<T> newClazz = (Class<T>) newItem.getClass();
Field newField = newClazz.getDeclaredField(fieldName);
newField.setAccessible(true);
Object newValue = newField.get(newItem);
// 取得旧对象的当前字段值
Class<T> oldClazz = (Class<T>) oldItem.getClass();
Field oldField = oldClazz.getDeclaredField(fieldName);
oldField.setAccessible(true);
Object oldValue = oldField.get(oldItem);
// 比较两个属性字段的值
if (!newValue.equals(oldValue)) {
// 如果字段值不相等
if (keyFlag == 1) {
// 如果为键值字段,表示两个对象
return 2;
}else {
// 非键值字段,表示有属性发生改变
retCode = 3;
}
}
}
}catch (NoSuchFieldException e) {
e.printStackTrace();
log.error(e.getMessage());
return 0;
}catch (IllegalAccessException e) {
e.printStackTrace();
log.error(e.getMessage());
return 0;
}
return retCode;
}
/**
*
* @methodName : removeDuplicate
* @description : 对象列表去重
* @param <T> : 泛型类型T
* @param fieldMap : 比较字段字典,key为字段名称,value为是否键值,1表示键值字段。
* @param inputList : 对象列表,将被去重
* @param dupList : 重复的多余对象列表,将被去重
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/08/15 1.0.0 sheng.zheng 初版
*
*/
public static <T> void removeDuplicate(Map<String,Integer> fieldMap,List<T> inputList,List<T> dupList){
// 开始比较下标
int pos = 0;
while (true) {
if (inputList.size() -1 < pos) {
break;
}
// 标记对象是否重复
boolean found = false;
// 被比较对象
T compItem = inputList.get(pos);
// 遍历列表
for (int i = pos + 1; i < inputList.size(); i++) {
// 比较对象
T newItem = inputList.get(i);
int compare = compareTwoItem(fieldMap,newItem,compItem);
if (compare == 1 || compare == 3) {
// 键值相同,为重复对象
found = true;
// 结束本次比较
break;
}
}
if (found == true) {
// 对象重复
dupList.add(compItem);
// 移除重复项
inputList.remove(pos);
// 注意,移除后,当前位置不变
}else {
// 不重复,处理下一个
pos ++;
}
}
}
}
3、调用方法
调用方法如下:
// 去重处理
// 字段字典
Map<String,Integer> fieldMap = new HashMap<String,Integer>();
// 手机号和用户名作为对象识别属性
fieldMap.put("phoneNumber", 1);
fieldMap.put("userName", 1);
List<UserInfo> dupList = new ArrayList<UserInfo>();
ObjectListUtil.removeDuplicate(fieldMap,importUserList,dupList);
// dupList可以作为导入错误数据输出用
// 查询数据库中存在的数据,orgId为导入附加参数,表示单位ID
List<UserInfo> dbUserList = userDao.selectItemsByOrgId(orgId);
// 对比新旧数据列表
List<UserInfo> addList = new ArrayList<UserInfo>();
List<UserInfo> updateList = new ArrayList<UserInfo>();
List<UserInfo> sameList = new ArrayList<UserInfo>();
List<UserInfo> remainderList = new ArrayList<UserInfo>();
// 对象的其它属性字段,非键值
fieldMap.put("age", 0);
fieldMap.put("address", 0);
fieldMap.put("height", 0);
ObjectListUtil.compareTwoList(fieldMap, importUserList, dbUserList,
addList, sameList, updateList, remainderList);
// 对addList执行insert操作,可以批量insert
// 对updateList执行update操作,逐个update
Spring Boot 通用对象列表比较和去重的更多相关文章
- spring boot的对象注入
1 需求 现在我们的项目中需要引入一个java类库,我想要很方便的使用该类库中的一个类,并且我想要创建这个类的一个单例对象.然后可以很方便的在各个模块中用@AutoWired进行对象注入. 比如一个配 ...
- spring boot 将对象转换为json返回
Spring Boot默认使用Jackson将对象转换为json,需要配置以下依赖: compile group: 'com.fasterxml.jackson.core', name: 'jacks ...
- Spring boot 通用配置文件模板
# =================================================================== # COMMON SPRING BOOT PROPERTIE ...
- Spring Boot注解(annotation)列表
(1)@SpringBootApplication 申明让spring boot自动给程序进行必要的配置,这个配置等同于: @Configuration ,@EnableAutoConfigurati ...
- 73. Spring Boot注解(annotation)列表【从零开始学Spring Boot】
[从零开始学习Spirng Boot-常见异常汇总] 针对于Spring Boot提供的注解,如果没有好好研究一下的话,那么想应用自如Spring Boot的话,还是有点困难的,所以我们这小节,说说S ...
- spring boot -thymeleaf-域对象操作
后台代码
- Spring Boot 学习(3)
文 by / 林本托 Tips 做一个终身学习的人. Tips 代码路径:https://github.com/iqcz/Springbootdemo/tree/master/code01/ch3 W ...
- Spring Boot 验证表单
在实际工作中,得到数据后的第一步就是验证数据的正确性,如果存在录入上的问题,一般会通过注解校验,发现错误后返回给用户,但是对于逻辑上的错误,很难使用注解方式进行验证了,这个使用可以使用Spring所提 ...
- 重磅:Spring Boot 2.0 正式发布!
Spring Boot 2.0 正式发布! 2018/03/01最新消息,传得沸沸扬扬的Spring Boot 2.0 正式发布了. 小编去看了下Spring Boot的官网,正式版本已经释放出来了! ...
- spring boot cli 知识点
spring boot cli 版本列表: https://repo.spring.io/snapshot/org/springframework/boot/spring-boot-cli/ spri ...
随机推荐
- 剑指 offer 第 1 天
第 1 天 栈与队列(简单) 剑指 Offer 09. 用两个栈实现队列 用两个栈实现一个队列.队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部 ...
- PTA题目总结
(1)前言:第一次题目集主要考察JAVA的一些语法知识,比如,控制台的输入,输出时保留两位小数,数组的使用,第十题有点难度,当时没写出来,现在想想 也还好,就是读懂题目有点费劲,第一次题目的题量比较大 ...
- 最新版本 Stable Diffusion 开源AI绘画工具之部署篇
目录 AI绘画 本地环境要求 下载 Stable Diffusion 运行启动 AI绘画 关于 AI 绘画最近有多火,既然你有缘能看到这篇文章,那么相信也不需要我过多赘述了吧? 随着 AI 绘画技术的 ...
- 免费1年服务器,部署个ChatGPT专属网页版
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 白皮袄个免费1年服务器,部署个ChatGPT专属网页版! api.openai.com por ...
- [VMware/CENOTS/Linux]VMware设置CentOS7共享文件夹[转载]
0 环境信息 VMWare: Linux CENTOS: 7.9.2009(Core) CPU: x86_64 / amd64 待共享的共享文件夹的物理路径: E:\VirtualMachine\sh ...
- .NET Core MongoDB数据仓储和工作单元模式封装
前言 上一章我们把系统所需要的MongoDB集合设计好了,这一章我们的主要任务是使用.NET Core应用程序连接MongoDB并且封装MongoDB数据仓储和工作单元模式,因为本章内容涵盖的有点多关 ...
- 让代码变得优雅简洁的神器:Java8 Stream流式编程
原创/朱季谦 本文主要基于实际项目常用的Stream Api流式处理总结. 因笔者主要从事风控反欺诈相关工作,故而此文使用比较熟悉的三要素之一的[手机号]黑名单作代码案例说明. 我在项目当中,很早就开 ...
- 详解C++中的extern与static关键字
本章通过问答方式明晰两个关键字及其作用. Q1:对于int x:,不加extern关键字他就是个未赋初值的定义,但是如果加了static或者extern都可以表示这仅是一个声明吗? A:不是的,具体情 ...
- 保姆级教程:用GPU云主机搭建AI大语言模型并用Flask封装成API,实现用户与模型对话
导读 在当今的人工智能时代,大型AI模型已成为获得人工智能应用程序的关键.但是,这些巨大的模型需要庞大的计算资源和存储空间,因此搭建这些模型并对它们进行交互需要强大的计算能力,这通常需要使用云计算服务 ...
- node服务端
一,node起服务+数据交互+中间件 什么是node express koa node是js在后端运行时的一个环境 express,koa是基于node的框架,快速构建web应用 前后端交互方式 1. ...