前言
最近有个项目里面中有大量的Excel文档导入导出需求,数据量最多的文档有上百万条数据,之前的导入导出都是用apache的POI,于是这次也决定使用POI,结果导入一个四十多万的文档就GG了,内存溢出... 于是找到EasyExcel的文档,学习了一番,解决了大数据量导入导出的痛点。
由于项目中很多接口都需要用到导入导出,部分文档都是根据日期区分,部分文档是需要全表覆盖,于是抽出一个工具类,简化下重复代码,在此把实现过程记录一下。
测试结果
数据量100W
导入
测试了几次,读取完加保存到数据库总耗时都是在140秒左右
导出
由于在业务中不涉及到大数据量的导出,最多只有10W+数据的导出,所以用的是最简单的写,测试二十万的数据量五十秒左右
依赖
官方文档:https://easyexcel.opensource.alibaba.com/
1 2 3 4 5 |
3.1 . 2
|
具体实现
实体类
@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{ 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 context) { 25 return new WriteCellData<>(DateUtil.localDateToDayString(context.getValue())); 26 } 27 28 }
工具类
由于项目中很多接口都有使用到导入导出,且持久层框架是Mybatis Plus,在此封装成通用的方法。
如果数据量不大,那么一行代码就可以解决了,直接用Mybatis Plus的批量插入:
EasyExcel.read(file.getInputStream(), TestUserEntity.class, new PageReadListener
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 MapgetMethodMap = 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 > valueList = dataList.stream().map(v -> { 33 List
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 ListcolumnList; 17 18 //value集合 19 private List > 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 2 34 5 insert into ${dto.tableName} 6 18 19 207 `${item}` 8 9 values 1011 ( 12 1713 #{value} 14 15 ) 1621 truncate table ${tableName} 22 23 24
调用
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 RRtestImport(@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 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 }