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一起,每天不一样!

相关推荐
TT哇3 分钟前
Spring Boot 项目中关于文件上传与访问的配置方案
java·spring boot·后端
程序员阿周4 分钟前
boost、websocketpp、curl 编译(Windows)
后端
踏浪无痕4 分钟前
信不信?一天让你从Java工程师变成Go开发者
后端·go
浪里行舟5 分钟前
使用亚马逊云科技 Elemental MediaConvert 实现 HLS 标准加密
后端
韩立学长6 分钟前
Springboot考研自习室预约管理系统1wdeuxh6(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
残花月伴7 分钟前
天机学堂-day5(互动问答)
java·spring boot·后端
BingoGo13 分钟前
再推荐 10 个低调但非常实用的 PHP 包
后端·php
KD4 小时前
设计模式——责任链模式实战,优雅处理Kafka消息
后端·设计模式·kafka
没逻辑10 小时前
gocron - 分布式定时任务管理系统
后端