Java 使用 Easyexcel 导出大量数据

读Excel | Easy Excel

1、 我遇到的数据量超级大,使用传统的POI方式来完成导入导出很明显会内存溢出,并且效率会非常低;

2、 数据量大直接使用select * from tableName肯定不行,一下子查出来300w条数据肯定会很慢;

3、 300w 数据导出到Excel时肯定不能都写在一个Sheet中,这样效率会非常低;估计打开都得几分钟;

4、 300w数据导出到Excel中肯定不能一行一行的导出到Excel中。频繁IO操作绝对不行;

5、 导入时300万数据存储到DB如果循环一条条插入也肯定不行;

6、导入时300w数据如果使用Mybatis的批量插入肯定不行,因为Mybatis的批量插入其实就是SQL的循环;一样很慢。
准备工作

1.基于maven搭建springboot工程,引入easyexcel依赖,这里我是用的时3.0版本

   <!--EasyExcel相关依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.0.5</version>
        </dependency>

2.创建海量数据的sql脚本

CREATE TABLE dept( /*部门表*/
deptno MEDIUMINT   UNSIGNED  NOT NULL  DEFAULT 0,
dname VARCHAR(20)  NOT NULL  DEFAULT "",
loc VARCHAR(13) NOT NULL DEFAULT ""
) ;
 
#创建表EMP雇员
CREATE TABLE emp
(empno  MEDIUMINT UNSIGNED  NOT NULL  DEFAULT 0, /*编号*/
ename VARCHAR(20) NOT NULL DEFAULT "", /*名字*/
job VARCHAR(9) NOT NULL DEFAULT "",/*工作*/
mgr MEDIUMINT UNSIGNED NOT NULL DEFAULT 0,/*上级编号*/
hiredate DATE NOT NULL,/*入职时间*/
sal DECIMAL(7,2)  NOT NULL,/*薪水*/
comm DECIMAL(7,2) NOT NULL,/*红利*/
deptno MEDIUMINT UNSIGNED NOT NULL DEFAULT 0 /*部门编号*/
) ;
 
#工资级别表
CREATE TABLE salgrade
(
grade MEDIUMINT UNSIGNED NOT NULL DEFAULT 0,
losal DECIMAL(17,2)  NOT NULL,
hisal DECIMAL(17,2)  NOT NULL
);
 
#测试数据
INSERT INTO salgrade VALUES (1,700,1200);
INSERT INTO salgrade VALUES (2,1201,1400);
INSERT INTO salgrade VALUES (3,1401,2000);
INSERT INTO salgrade VALUES (4,2001,3000);
INSERT INTO salgrade VALUES (5,3001,9999);
 
delimiter $$
 
#创建一个函数,名字 rand_string,可以随机返回我指定的个数字符串
create function rand_string(n INT)
returns varchar(255) #该函数会返回一个字符串
begin
#定义了一个变量 chars_str, 类型  varchar(100)
#默认给 chars_str 初始值   'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ'
 declare chars_str varchar(100) default
   'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ'; 
 declare return_str varchar(255) default '';
 declare i int default 0; 
 while i < n do
    # concat 函数 : 连接函数mysql函数
   set return_str =concat(return_str,substring(chars_str,floor(1+rand()*52),1));
   set i = i + 1;
   end while;
  return return_str;
  end $$
 
 
 #这里我们又自定了一个函数,返回一个随机的部门号
create function rand_num( )
returns int(5)
begin
declare i int default 0;
set i = floor(10+rand()*500);
return i;
end $$
 
 #创建一个存储过程, 可以添加雇员
create procedure insert_emp(in start int(10),in max_num int(10))
begin
declare i int default 0;
#set autocommit =0 把autocommit设置成0
 #autocommit = 0 含义: 不要自动提交
 set autocommit = 0; #默认不提交sql语句
 repeat
 set i = i + 1;
 #通过前面写的函数随机产生字符串和部门编号,然后加入到emp表
 insert into emp values ((start+i) ,rand_string(6),'SALESMAN',0001,curdate(),2000,400,rand_num());
  until i = max_num
 end repeat;
 #commit整体提交所有sql语句,提高效率
   commit;
 end $$
 
 #添加8000000数据
call insert_emp(100001,8000000)$$
 
#命令结束符,再重新设置为;
delimiter ;

3.实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp implements Serializable {
    @ExcelIgnore
    private Integer empno;
 
    @ExcelProperty(value = "员工名称")
    private String ename;
 
    @ExcelProperty(value = "工作")
    private String job;
 
    @ExcelProperty(value = "主管编号")
    private Integer mgr;
 
    @ExcelProperty(value = "入职日期")
    private Date hiredate;
 
    @ExcelProperty(value = "薪资")
    private BigDecimal sal;
 
    @ExcelProperty(value = "奖金")
    private BigDecimal comm;
 
    @ExcelProperty(value = "所属部门")
    private Integer deptno;
 
}
4.vo类
@Data
public class EmpVo {
 
     @ExcelIgnore
    private Integer empno;
 
    @ExcelProperty(value = "员工名称")
    private String ename;
 
    @ExcelProperty(value = "工作")
    private String job;
 
    @ExcelProperty(value = "主管编号")
    private Integer mgr;
 
    @ExcelProperty(value = "入职日期")
    private Date hiredate;
 
    @ExcelProperty(value = "薪资")
    private BigDecimal sal;
 
    @ExcelProperty(value = "奖金")
    private BigDecimal comm;
 
    @ExcelProperty(value = "所属部门")
    private Integer deptno;
 
}
5、导出核心代码
@Resource
private EmpService empService;
/**
 * 分批次导出
 */
@GetMapping("/export")
public void export() throws IOException {
   
     Long startTime = System.currentTimeMillis();
     empService.export(); //导出
     Long endTime = System.currentTimeMillis();
     Long elapsedTime = (endTime - startTime) / 1000;
     System.out.println("导出_方式耗时:" + elapsedTime + "s");
 
}

public class ExcelConstants {
 //一个sheet装100w数据
    public static final Integer PER_SHEET_ROW_COUNT = 1000000;
    //每次查询20w数据,每次写入20w数据
    public static final Integer PER_WRITE_ROW_COUNT = 200000;
}
实现类中:
@Override
public void export() throws IOException {
    OutputStream outputStream =null;
    try {
        //记录总数:实际中需要根据查询条件进行统计即可     
        Integer totalCount = empMapper.selectCount(null);
        //每一个Sheet存放100w条数据
        Integer sheetDataRows = ExcelConstants.PER_SHEET_ROW_COUNT;
        //每次写入的数据量20w,每页查询20W
        Integer writeDataRows = ExcelConstants.PER_WRITE_ROW_COUNT;
        //计算需要的Sheet数量
        Integer sheetNum = totalCount % sheetDataRows == 0 ? (totalCount / sheetDataRows) : (totalCount / sheetDataRows + 1);
        //计算一般情况下每一个Sheet需要写入的次数(一般情况不包含最后一个sheet,因为最后一个sheet不确定会写入多少条数据)
            Integer oneSheetWriteCount = sheetDataRows % writeDataRows == 0 ? (sheetDataRows / writeDataRows) : (sheetDataRows / writeDataRows + 1);
            //计算最后一个sheet需要写入的次数
            Integer lastCountAll = totalCount - (sheetNum-1)*sheetDataRows;
            Integer lastSheetWriteCount = lastCountAll % writeDataRows == 0 ? (lastCountAll / writeDataRows) : (lastCountAll / writeDataRows + 1);
 
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = requestAttributes.getResponse();
        outputStream = response.getOutputStream();
        //必须放到循环外,否则会刷新流
        ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
 
        //开始分批查询分次写入
        for (int i = 0; i < sheetNum; i++) {
            //创建Sheet
            WriteSheet sheet = new WriteSheet();
            sheet.setSheetName("测试Sheet1"+i);
            sheet.setSheetNo(i);
            //循环写入次数: j的自增条件是当不是最后一个Sheet的时候写入次数为正常的每个Sheet写入的次数,如果是最后一个就需要使用计算的次数lastSheetWriteCount
            for (int j = 0; j < (i != sheetNum - 1 ? oneSheetWriteCount : lastSheetWriteCount); j++) {
                //分页查询一次20w
                Page<Emp> page = empMapper.selectPage(new Page(j + 1 + oneSheetWriteCount * i, writeDataRows), null);
                List<Emp> empList = page.getRecords();

//使用pagehelper的如下
// int  pageNum = j + 1 + oneSheetWriteCount * i;                    //PageHelper.startPage(pageNum,writeDataRows,getOrderBy(pageable.getSort())).setReasonable(true);
//List<DqtbsHiddenDangerQuery> list = mapper.expmortByPage(dto);


//封装成可以导出实体类
                List<EmpVo> empVoList = new ArrayList<>();
                for (Emp emp : empList) {
                    EmpVo empVo = new EmpVo();
                    BeanUtils.copyProperties(emp, empVo);
                    empVoList.add(empVo);
                }
                WriteSheet writeSheet = EasyExcel.writerSheet(i, "员工信息" + (i + 1)).head(EmpVo.class)
                        .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
                //写数据
                excelWriter.write(empVoList, writeSheet);
            }
        }
        // 下载EXCEL
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止浏览器端导出excel文件名中文乱码 当然和easyexcel没有关系
        String fileName = URLEncoder.encode("员工信息", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        excelWriter.finish();
        outputStream.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (BeansException e) {
        e.printStackTrace();
    }finally {
        if (outputStream != null) {
            outputStream.close();
        }
    }
}

前端vue的:

  //导出
  exportExcel(values) {
    let this_ = this
    this_.spinningExport = true
    this.$api.export(BASE_URL + 'exportNew', values, `数据查询_${moment(new Date()).format('YYYY-MM-DD')}.xlsx`, {
      success() {
        this_.spinningExport = false
      },
      fail() {
        this_.spinningExport = false
      }
    })
  },

分批量查询,例如,一个sheet 页 存储 10000条,分页每次查1000条,每个sheet需要查10次。总数据50000条,就分了5个sheet页来显示,大批量数据来说,easyExcel还是很好用的。

相关推荐
Winston Wood1 分钟前
Java线程池详解
java·线程池·多线程·性能
Alive~o.03 分钟前
Go语言进阶&依赖管理
开发语言·后端·golang
斗-匕3 分钟前
MySQL 三大日志详解
数据库·mysql·oracle
花海少爷5 分钟前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
手握风云-6 分钟前
数据结构(Java版)第二期:包装类和泛型
java·开发语言·数据结构
代码中の快捷键9 分钟前
MySQL数据库存储引擎
数据库·mysql
只因在人海中多看了你一眼9 分钟前
数据库体系
数据库
喵叔哟25 分钟前
重构代码中引入外部方法和引入本地扩展的区别
java·开发语言·重构
尘浮生31 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
六月闻君1 小时前
MySQL 报错:1137 - Can‘t reopen table
数据库·mysql