前言

最近有个项目里面中有大量的Excel文档导入导出需求,数据量最多的文档有上百万条数据,之前的导入导出都是用apache的POI,于是这次也决定使用POI,结果导入一个四十多万的文档就GG了,内存溢出...  于是找到EasyExcel的文档,学习了一番,解决了大数据量导入导出的痛点。

由于项目中很多接口都需要用到导入导出,部分文档都是根据日期区分,部分文档是需要全表覆盖,于是抽出一个工具类,简化下重复代码,在此把实现过程记录一下。

测试结果

数据量100W

导入

测试了几次,读取完加保存到数据库总耗时都是在140秒左右

导出

由于在业务中不涉及到大数据量的导出,最多只有10W+数据的导出,所以用的是最简单的写,测试二十万的数据量五十秒左右

依赖

官方文档:https://easyexcel.opensource.alibaba.com/

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.2</version>
</dependency>

  

具体实现

实体类

@ExcelProperty注解对应Excel文档中的表头,其中默认属性是value,对应文字,也有index属性,可以对应下标。converter属性是指定一个转换器,这个转换器中实现了把Excel内容转换成java对象(导入使用),Java对象转Excel内容(导出使用),我这里实现的是LocalDateTime和文本的转换。

@ExcelIgnoreUnannotated注解的意思就是在导入导出的时候忽略掉未加@ExcelProperty注解的字段

 1 @Data
2 @TableName("t_test_user")
3 @ApiModel(value = "TestUserEntity对象", description = "测试表")
4 @ExcelIgnoreUnannotated
5 public class TestUserEntity implements Serializable {
6
7 private static final long serialVersionUID = 1L;
8
9 @TableId(value = "id", type = IdType.AUTO)
10 private Long id;
11
12 @ExcelProperty("用户名")
13 @ApiModelProperty("用户名")
14 @TableField("user_name")
15 private String userName;
16
17 @ExcelProperty("账号")
18 @ApiModelProperty("账号")
19 @TableField("account")
20 private String account;
21
22 @ExcelProperty("性别")
23 @ApiModelProperty("性别")
24 @TableField("sex")
25 private String sex;
26
27 @ExcelProperty(value = "注册时间", converter = StringToLocalDateTimeConverter.class)
28 @ApiModelProperty("注册时间")
29 @TableField("registered_time")
30 private LocalDateTime registeredTime;
31
32 @ApiModelProperty("数据日期")
33 @TableField("data_date")
34 private Integer dataDate;
35
36 @ApiModelProperty("创建人")
37 @TableField("create_user")
38 private String createUser;
39
40 @ApiModelProperty("创建时间")
41 @TableField("create_time")
42 private LocalDateTime createTime;
43 }

转换器

在这里实现导入导出的数据格式转换

 1 /**
2 * @author Tang
3 * @describe easyExcel格式转换器
4 * @date 2022年08月29日 09:41:03
5 */
6 public class StringToLocalDateTimeConverter implements Converter<LocalDateTime> {
7 /**
8 * 这里读的时候会调用
9 */
10 @Override
11 public LocalDateTime convertToJavaData(ReadConverterContext<?> context) {
12 String stringValue = context.getReadCellData().getStringValue();
13 return StringUtils.isBlank(stringValue) ? null : DateUtil.stringToLocalDatetime(stringValue);
14 }
15
16 /**
17 * @describe 写的时候调用
18 * @Param context
19 * @return com.alibaba.excel.metadata.data.WriteCellData<?>
20 * @date 2022年11月17日 16:03:39
21 * @author Tang
22 */
23 @Override
24 public WriteCellData<?> convertToExcelData(WriteConverterContext<LocalDateTime> context) {
25 return new WriteCellData<>(DateUtil.localDateToDayString(context.getValue()));
26 }
27
28 }

工具类

由于项目中很多接口都有使用到导入导出,且持久层框架是Mybatis Plus,在此封装成通用的方法。

如果数据量不大,那么一行代码就可以解决了,直接用Mybatis Plus的批量插入:

EasyExcel.read(file.getInputStream(), TestUserEntity.class, new PageReadListener<TestUserEntity>(TestUserService::saveBatch)).sheet().doRead();

PageReadListener是默认的监听器,在此监听器中传入一个Consumer接口的实现,由此来保存数据。它具体实现原理是从文件中分批次读取,然后在此监听器中实现保存到数据库,当然也可以重写监听器,定义自己想要实现的业务,如数据校验等。BATCH_COUNT参数是每次读取的数据条数,3.1.2的版本默认是100条,建议修改为3000。

导出也是一行代码:EasyExcel.write(response.getOutputStream(), clazz).sheet().doWrite(() -> testUserService.list());

数据量大的话用Mybatis Plus的批量插入还是会很慢,因为这个批量插入实际上还是一条条数据插入的,需要把所有数据拼接成insert into table(field1,field2) values(value1,value2),(value1,value2),(value,value2)...,配合数据库的rewriteBatchedStatements=true参数配置,可以实现快速批量插入,在下文中的114行调用实现。

  1 /**
2 * @author Tang
3 * @describe EasyExcel工具类
4 * @date 2022年11月02日 17:56:45
5 */
6 public class EasyExcelUtil {
7
8 /**
9 * @describe 封装成批量插入的参数对象
10 * @Param clazz
11 * @Param dataList
12 * @date 2022年11月17日 18:00:31
13 * @author Tang
14 */
15 public static DynamicSqlDTO dynamicSql(Class<?> clazz, List<?> dataList) {
16 //字段集合 key=数据库列名 value=实体类get方法
17 Map<String, Method> getMethodMap = new LinkedHashMap<>();
18 //获取所有字段
19 Field[] declaredFields = clazz.getDeclaredFields();
20 for (Field field : declaredFields) {
21 field.setAccessible(true);
22 //获取注解为TableField的字段
23 TableField annotation = field.getAnnotation(TableField.class);
24 if (annotation != null && annotation.exist()) {
25 String column = annotation.value();
26 Method getMethod = getGetMethod(clazz, field.getName());
27 getMethodMap.put(column, getMethod);
28 }
29 }
30
31 //value集合
32 List<List<Object>> valueList = dataList.stream().map(v -> {
33 List<Object> tempList = new ArrayList<>();
34 getMethodMap.forEach((key, value) -> {
35 try {
36 tempList.add(value.invoke(v));
37 } catch (IllegalAccessException | InvocationTargetException e) {
38 tempList.add(null);
39 }
40 });
41 return tempList;
42 }).collect(Collectors.toList());
43
44 return DynamicSqlDTO.builder()
45 .tableName(clazz.getAnnotation(TableName.class).value())
46 .columnList(new ArrayList<>(getMethodMap.keySet()))
47 .valueList(valueList)
48 .build();
49 }
50
51
52 /**
53 * @describe java反射bean的get方法
54 * @Param objectClass
55 * @Param fieldName
56 * @date 2022年11月02日 17:52:03
57 * @author Tang
58 */
59 private static Method getGetMethod(Class<?> objectClass, String fieldName) {
60 StringBuilder sb = new StringBuilder();
61 sb.append("get");
62 sb.append(fieldName.substring(0, 1).toUpperCase(Locale.ROOT));
63 sb.append(fieldName.substring(1));
64 try {
65 return objectClass.getMethod(sb.toString());
66 } catch (NoSuchMethodException e) {
67 throw new RuntimeException("Reflect error!");
68 }
69 }
70
71
72 /**
73 * @return boolean
74 * @describe EasyExcel公用导入方法(按日期覆盖)
75 * @Param file excel文件
76 * @Param date 数据日期
77 * @Param function 数据日期字段的get方法 如传入了date,则需要设置
78 * @Param setCreateDate 数据日期set方法 如传入了date,则需要设置
79 * @Param mapper 实体类对应的mapper对象 如传入了date,则需要设置
80 * @Param entityClass 实体类class
81 * @date 2022年11月11日 15:10:19
82 * @author Tang
83 */
84 public static <T> Boolean importExcel(MultipartFile file, Integer date, SFunction<T, Integer> getCreateDate, BiConsumer<T, Integer> setCreateDate, BaseMapper<T> mapper, Class<T> entityClass) {
85 String userName = SecurityAuthorHolder.getSecurityUser().getUsername();
86 LocalDateTime now = LocalDateTime.now();
87 CustomSqlService customSqlService = ApplicationConfig.getBean(CustomSqlService.class);
88
89 //根据date来判断 为null则需要删除全表数据 否则删除当天数据
90 if (date == null) {
91 customSqlService.truncateTable(entityClass.getAnnotation(TableName.class).value());
92 } else {
93 mapper.delete(Wrappers.lambdaQuery(entityClass).eq(getCreateDate, date));
94 }
95
96 try {
97 Method setCreateUser = entityClass.getMethod("setCreateUser", String.class);
98 Method setCreateTime = entityClass.getMethod("setCreateTime", LocalDateTime.class);
99
100 EasyExcel.read(file.getInputStream(), entityClass, new PageReadListener<T>(
101 dataList -> {
102 dataList.forEach(v -> {
103 try {
104 setCreateUser.invoke(v, userName);
105 setCreateTime.invoke(v, now);
106 if (setCreateDate != null) {
107 setCreateDate.accept(v, date);
108 }
109 } catch (IllegalAccessException | InvocationTargetException e) {
110 e.printStackTrace();
111 }
112 });
113 if (CollectionUtil.isNotEmpty(dataList)) {
114 customSqlService.executeCustomSql(dynamicSql(entityClass, dataList));
115 }
116 }
117 )).sheet().doRead();
118 } catch (Exception e) {
119 e.printStackTrace();
120 throw new ServerException("读取异常");
121 }
122 return true;
123 }
124
125 /**
126 * @return boolean
127 * @describe EasyExcel公用导入方法(全表覆盖)
128 * @Param file
129 * @Param entityClass
130 * @date 2022年11月11日 15:33:07
131 * @author Tang
132 */
133 public static <T> Boolean importExcel(MultipartFile file, Class<T> entityClass) {
134 return importExcel(file, null, null, null, null, entityClass);
135 }
136
137 /**
138 * @return void
139 * @describe EasyExcel公用导出方法
140 * @Param clazz
141 * @Param dataList
142 * @date 2022年11月11日 15:56:45
143 * @author Tang
144 */
145 public static <T> void exportExcel(Class<T> clazz, List<T> dataList) {
146 HttpServletResponse response = ServletRequestUtil.getHttpServletResponse();
147 try {
148 EasyExcel.write(response.getOutputStream(), clazz)
149 .sheet()
150 .doWrite(() -> dataList);
151 } catch (Exception e) {
152 e.printStackTrace();
153 throw new ServerException("导出失败");
154 }
155 }
156 }

DTO

 1 /**
2 * @author Tang
3 * @describe 生成批量插入sqlDTO
4 * @date 2022年11月02日 17:53:33
5 */
6 @Data
7 @Builder
8 @AllArgsConstructor
9 @NoArgsConstructor
10 public class DynamicSqlDTO {
11
12 //表名
13 private String tableName;
14
15 //列名集合
16 private List<String> columnList;
17
18 //value集合
19 private List<List<Object>> valueList;
20 }

Mapper

根据业务实现了两个方法,一个是批量插入,一个是全表覆盖删除

 1 @Mapper
2 public interface CustomSqlMapper {
3
4 /**
5 * @describe 执行动态批量插入语句
6 * @Param dynamicSql
7 * @date 2022年11月03日 09:59:22
8 * @author Tang
9 */
10 void executeCustomSql(@Param("dto") DynamicSqlDTO dto);
11
12 /**
13 * @describe 快速清空表
14 * @Param tableName
15 * @date 2022年11月08日 17:47:45
16 * @author Tang
17 */
18 void truncateTable(@Param("tableName") String tableName);
19 }

XML

 1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3 <mapper namespace="**.**.CustomSqlMapper">
4 <insert id="executeCustomSql">
5 insert into ${dto.tableName}
6 <foreach collection="dto.columnList" item="item" separator="," open="(" close=")">
7 `${item}`
8 </foreach>
9 values
10 <foreach collection="dto.valueList" item="item" separator=",">
11 (
12 <foreach collection="item" item="value" separator=",">
13 #{value}
14 </foreach>
15 )
16 </foreach>
17 </insert>
18
19
20 <insert id="truncateTable">
21 truncate table ${tableName}
22 </insert>
23
24 </mapper>

调用

 1 @RestController
2 @Api(value = "测试-测试", tags = "测试-测试")
3 @RequestMapping("/test")
4 public class TestUserController {
5
6 @Resource
7 private TestUserMapper testUserMapper;
8
9 @PostMapping(value = "/import", produces = BaseConstant.REQUEST_HEADERS_CONTENT_TYPE)
10 @ApiOperation(value = "测试-导入(全表覆盖)", notes = "测试-导入(全表覆盖)")
11 public RR<Boolean> testImport(@RequestParam(value = "file") @ApiParam("上传文件") MultipartFile file) {
12 return RR.success(
13 EasyExcelUtil.importExcel(
14 file,
15 TestUserEntity.class
16 )
17 );
18 }
19
20 @PostMapping(value = "/import", produces = BaseConstant.REQUEST_HEADERS_CONTENT_TYPE)
21 @ApiOperation(value = "测试-导入(按日期覆盖)", notes = "测试-导入(按日期覆盖)")
22 public RR<Boolean> testImport(@RequestParam(value = "file") @ApiParam("上传文件") MultipartFile file, @ApiParam("日期 20110101") @RequestParam(value = "date") Integer date) {
23 return RR.success(
24 EasyExcelUtil.importExcel(
25 file,
26 date,
27 TestUserEntity::getDataDate,
28 TestUserEntity::setDataDate,
29 testUserMapper,
30 TestUserEntity.class
31 )
32 );
33 }
34
35 @PostMapping(value = "/export", produces = BaseConstant.REQUEST_HEADERS_CONTENT_TYPE)
36 @ApiOperation(value = "测试-导出", notes = "测试-导出")
37 public void testExport() {
38 EasyExcelUtil.exportExcel(
39 TestUserEntity.class,
40 testUserMapper.selectList(null)
41 );
42 }
43 }

EasyExcel对大数据量表格操作导入导出的更多相关文章

  1. NPOI大数据量多个sheet导出源码(原)

    代码如下: #region NPOI大数据量多个sheet导出 /// <summary> /// 大数据量多个sheet导出 /// </summary> /// <t ...

  2. 使用OPENROWSET、Microsoft.ACE.OLEDB实现大数据量的高效导入

    首先说明使用的环境是:java和Sqlserver. 最近公司需要进行大数据量的导入操作.原来使用的是Apache POI,虽然可以实现功能,但是因为逻辑处理中需要进行许多校验,处理速度太慢,使用多线 ...

  3. 基于EasyExcel的大数据量导入并去重

    源码:https://gitee.com/antia11/excel-data-import-demo 背景:客户需要每周会将上传一个 Excel 数据文件,数据量单次为 20W 以上,作为其他模块和 ...

  4. Dapper学习(四)之Dapper Plus的大数据量的操作

    这篇文章主要讲 Dapper Plus,它使用用来操作大数量的一些操作的.比如插入1000条,或者10000条的数据时,再使用Dapper的Execute方法,就会比较慢了.这时候,可以使用Dappe ...

  5. 大数据量.csv文件导入SQLServer数据库

    前几天拿到了一个400多M的.csv文件,在电脑上打开要好长时间,打开后里面的数据都是乱码.因此,做了一个先转码再导入数据库的程序.100多万条的数据转码+导入在本地电脑上花了4分钟,感觉效率还可以. ...

  6. MERGE INTO 解决大数据量复杂操作更新慢的问题

    现我系统中有一条复杂SQL,由于业务复杂需要关联人员的工作离职三个表,并进行分支判断,再计算人员的字段信息,由于人员多,分支多,计算复杂等原因,一次执行需要5min,容易卡死,现在使用MERGE IN ...

  7. MYSQL数据库导入大数据量sql文件失败的解决方案

    1.在讨论这个问题之前首先介绍一下什么是"大数据量sql文件". 导出sql文件.选择数据库-----右击选择"转储SQL文件"-----选择"结构和 ...

  8. Mysql 大数据量导入程序

    Mysql 大数据量导入程序<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" ...

  9. SQL Server 使用bcp进行大数据量导出导入

    转载:http://www.cnblogs.com/gaizai/archive/2010/04/17/1714389.html SQL Server的导出导入方式有: 在SQL Server中提供了 ...

  10. c#中@标志的作用 C#通过序列化实现深表复制 细说并发编程-TPL 大数据量下DataTable To List效率对比 【转载】C#工具类:实现文件操作File的工具类 异步多线程 Async .net 多线程 Thread ThreadPool Task .Net 反射学习

    c#中@标志的作用   参考微软官方文档-特殊字符@,地址 https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/toke ...

随机推荐

  1. KingbaseES 的闪回查询

    KingbaseES V008R006C006B0013版本新增支持闪回查询,闪回版本查询.闪回表到指定时间点.旧版本已支持闪回回收站技术. 闪回技术(闪回查询和闪回表到指定时间点)可以通过时间戳和C ...

  2. KingbaseES R3集群在线删除数据节点案例

    案例说明: kingbaseES R3集群一主多从的架构,一般有两个节点是集群的管理节点,所有的节点都可以为数据节点:对于非管理节点的数据节点可以在线删除:但是对于管理节点,无法在线删除,如果删除管理 ...

  3. 一文搞懂mysql索引底层逻辑,干货满满!

    一.什么是索引 在mysql中,索引是一种特殊的数据库结构,由数据表中的一列或多列组合而成,可以用来快速查询数据表中有某一特定值的记录.通过索引,查询数据时不用读完记录的所有信息,而只是查询索引列即可 ...

  4. JAVA中方法的调用主要有以下几种

    JAVA中方法的调用主要有以下几种: 1.非静态方法 非静态方法就是没有 static 修饰的方法,对于非静态方法的调用,是通过对 象来调用的,表现形式如下. 对象名.方法() eg: public ...

  5. 容器化|自建 MySQL 集群迁移到 Kubernetes

    背景 如果你有自建的 MySQL 集群,并且已经感受到了云原生的春风拂面,想将数据迁移到 Kubernetes 上,那么这篇文章可以给你一些思路. 文中将自建 MySQL 集群数据,在线迁移到 Kub ...

  6. Object.keys的‘诡异’特性,你值得收藏!

    先从'诡异'的问题入手 例1: 纯Number类型的属性 const obj = { 1: 1, 6: 6, 3: 3, 2: 2 } console.log('keys', Object.keys( ...

  7. 跟我学Python图像处理丨带你掌握傅里叶变换原理及实现

    摘要:傅里叶变换主要是将时间域上的信号转变为频率域上的信号,用来进行图像除噪.图像增强等处理. 本文分享自华为云社区<[Python图像处理] 二十二.Python图像傅里叶变换原理及实现> ...

  8. Opengl ES之纹理贴图

    纹理可以理解为一个二维数组,它可以存储大量的数据,这些数据可以发送到着色器上.一般情况下我们所说的纹理是表示一副2D图,此时纹理存储的数据就是这个图的像素数据. 所谓的纹理贴图,就是使用Opengl将 ...

  9. Opengl ES之四边形绘制

    四边形的绘制在Opengl ES是很重要的一项技巧,比如做视频播放器时视频的渲染就需要使用到Opengl ES绘制四边形的相关知识.然而在Opengl ES却没有直接提供 绘制四边形的相关函数,那么如 ...

  10. Python(一)转义字符及操作符

    转义字符 描述 \(在行尾时) 续航符 \\ 反斜杠符号 \' 单引号 \'' 双引号 \a 响铃 \b 退格(Backspace) \e 转义 \000 空 \n 转行 \v 纵向制表符 \t 横向 ...