EasyExcel实现复杂数据的导入

shigen日更文章的博客写手,擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长,分享认知,留住感动。

在我们常使用的系统中,难免会遇到数据导入的情况。其实导入做起来并不是很难,直接用到easyexcel读取数据写入到数据库即可。看似好简单的样子,是的,现在这些开源的框架已经帮我们把所有能遇到的问题都给考虑到了。那我们需要考虑到什么呢?shigen觉得最重要的是实际的业务场景。

我们在正式写代码之前,先去思考这样的几个问题:

前置思考

  • 系统的最大数据承载量是多少?我一下子解析1w+数据会不会有影响
  • 单行数据的验证怎么做
  • 数据的插入怎么插入,我一下子导入1w+数据到数据库吗
  • 我单条数据校验错了,我怎么保存给用户提示

......


这些都是要去思考的问题呀。shigen绝对没有危言耸听的意思哈,如果觉得简单点也行,那下文就不需要再看了。

记得shigen之前写过excel导入导出百万级数据的优化,这里提到了从excel导入100w数据到mysql的注意点:

从excel导入100万数据到mysql

  • 首先是easyExcel分批读取Excel中的100w数据 EasyExcelGeneralDataListener按照sheet页一行行的数据读取
  • 其次就是往DB里插入,怎么去插入这20w条数据,批量插入 同样也不能使用Mybatis的批量插入,会读取数据到内存中,事务整体提交
  • 使用JDBC+事务的批量操作将数据插入到数据库(分批读取+JDBC分批插入+手动事务控制)

当时的代码也在这里:

那这次的修改也是基于上次的修改,我们先来看下修改之后的效果:

我们调用接口:

很好的显示了第几行什么数据的什么问题。其实我原始的数据是这样的:

注:姓名、电话都是随机生成,并无实际参考价值。

我故意的写错了那个电话,最后我们看看数据库,数据是否是一致的。

代码中,我也涉及到了批量导入的策略,这个我们来看下代码运行之后的日志输出:

发现结果还是很符合预期的,完美的实现。那接下来就是我如何实现的问题,感兴趣的伙伴可以先去我的gitee相关代码,本次的代码也参考了文章SpringBoot整合EasyExcel实现复杂Excel表格的导入&导出功能, 感谢原作者提供的案例参考。

发现代码其实写起来就是实现了easyexcelListener接口,我先展示全部的代码吧:

java 复制代码
 /**
  * 事件监听
  *
  * @author shigenfu
  * @date 2023/8/20 11:14 下午
  */
 @RequiredArgsConstructor
 public class EasyExcelGeneralDataListener extends AnalysisEventListener<UserVo> {
 ​
     private UserService userService;
     /**
      * 批量保存的数据行数
      */
     private static final Integer BATCH_SIZE = 5;
     /**
      * 单次导入最大的数据量
      */
     private static final Integer MAX_SIZE = 10000;
     /**
      * 电话验证正则
      */
     private static final Pattern PHONE_REGEX = Pattern.compile("^1[0-9]{10}$");
     /**
      * 错误信息
      */
     private final List<String> errorMsgList = new ArrayList<>();
     /**
      * 用于存储读取的数据
      */
     private final List<UserVo> dataList = new ArrayList<>();
 ​
     public EasyExcelGeneralDataListener(UserService userService) {
         this.userService = userService;
     }
 ​
     @Override
     public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
         int totalRows = context.readRowHolder().getRowIndex() + 1;
         if (totalRows > MAX_SIZE) {
             errorMsgList.add("数据量过大,最多导入 " + MAX_SIZE + " 条数据");
             throw new RuntimeException("数据量过大,最多导入 " + MAX_SIZE + " 条数据");
         }
     }
 ​
     @Override
     public void invoke(UserVo user, AnalysisContext context) {
         Integer rowIndex = context.readRowHolder().getRowIndex();
         // 数据验证add进入集合
         if (dataChecked(rowIndex, user)) {
             dataList.add(user);
         }
         // size是否为200000条:这里其实就是分批.当数据等于20w的时候执行一次插入
         if (dataList.size() >= BATCH_SIZE) {
             // 存入数据库:数据小于1w条使用Mybatis的批量插入即可
             saveData();
             // 清理集合便于GC回收
             dataList.clear();
         }
     }
 ​
     @Override
     public void onException(Exception exception, AnalysisContext context) throws Exception {
         if (exception instanceof RuntimeException) {
             throw exception;
         }
         int rowIndex = context.readRowHolder().getRowIndex() + 1;
         errorMsgList.add("第" + rowIndex + "行数据异常,请检查后重新导入");
     }
 ​
     private boolean dataChecked(Integer rowIndex, UserVo user) {
         return usernameValid(rowIndex, user.getUsername()) && phoneValid(rowIndex, user.getPhone());
     }
 ​
     public List<String> getErrorMsgList() {
         return errorMsgList;
     }
 ​
     /**
      * 保存数据到DB
      */
     private void saveData() {
         userService.importDBFromExcel10w(dataList);
         dataList.clear();
     }
 ​
     /**
      * Excel中所有数据解析完毕会调用此方法
      *
      * @param context 上下文
      */
     @Override
     public void doAfterAllAnalysed(AnalysisContext context) {
         // 保存最后的数据
         saveData();
         dataList.clear();
     }
 ​
 ​
     private boolean usernameValid(Integer rowIndex, String username) {
         if (StrUtil.isEmpty(username)) {
             errorMsgList.add("第" + rowIndex + "行'用户名'为空");
             return false;
         }
         return true;
     }
 ​
     private boolean phoneValid(Integer rowIndex, String phone) {
         // 根据正则校验
         if (!ReUtil.isMatch(PHONE_REGEX, phone)) {
             errorMsgList.add("第" + rowIndex + "行'手机号'格式错误");
             return false;
         }
         return true;
     }
 ​
 }

整体的一个实现关系是这样的:

在我们处理数据的时候,需要去实现一下对应的方法,做到数据的验证和分批次的导入。

需要注意的是:

在分批次导入的时候,我们应该尽量避免使用ORM框架,而是自己写导入的sql语句:

另外,关于每行数据的字段校验,我们可以写的更加详细一些,或者放在另外的一个专门校验字段的类中。


以上就是今天分享的全部内容了,觉得不错的话,记得点赞 在看 关注支持一下哈,您的鼓励和支持将是shigen坚持日更的动力。同时,shigen在多个平台都有文章的同步,也可以同步的浏览和订阅:

平台 账号 链接
CSDN shigen01 shigen的CSDN主页
知乎 gen-2019 shigen的知乎主页
掘金 shigen01 shigen的掘金主页
腾讯云开发者社区 shigen shigen的腾讯云开发者社区主页
微信公众平台 shigen 公众号名:shigen

shigen一起,每天不一样!

相关推荐
短剑重铸之日2 分钟前
《设计模式》第八篇:三大类型之创建型模式
java·后端·设计模式·创建型设计模式
野犬寒鸦1 小时前
从零起步学习并发编程 || 第四章:synchronized底层源码级讲解及项目实战应用案例
java·服务器·开发语言·jvm·后端·学习·面试
计算机毕设VX:Fegn08959 小时前
计算机毕业设计|基于springboot + vue蛋糕店管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
没差c10 小时前
springboot集成flyway
java·spring boot·后端
三水不滴10 小时前
Redis 过期删除与内存淘汰机制
数据库·经验分享·redis·笔记·后端·缓存
笨蛋不要掉眼泪11 小时前
Spring Boot集成LangChain4j:与大模型对话的极速入门
java·人工智能·后端·spring·langchain
sheji341614 小时前
【开题答辩全过程】以 基于SpringBoot的疗养院管理系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
短剑重铸之日14 小时前
《设计模式》第六篇:装饰器模式
java·后端·设计模式·装饰器模式
码界奇点15 小时前
基于Flask与OpenSSL的自签证书管理系统设计与实现
后端·python·flask·毕业设计·飞书·源代码管理
代码匠心16 小时前
从零开始学Flink:状态管理与容错机制
java·大数据·后端·flink·大数据处理