easyExcel使用场景

在项目开发中往往需要使用到数据的导入和导出,导入就是从Excel中导入到DB中,而导出就是从DB中查询数据然后使用POI写到Excel上。

大数据的导入和导出,相信大家在日常的开发、面试中都会遇到。

很多问题只要这一次解决了,总给复盘记录,后期遇到同样的问题就好解决了。好啦,废话不多说开始正文!

1.传统POI的的版本优缺点比较

其实想到数据的导入导出,理所当然的会想到apache的poi技术,以及Excel的版本问题。

  • HSSFWorkbook

这个实现类是我们早期使用最多的对象,它可以操作Excel2003以前(包含2003)的所有Excel版本。在2003以前Excel的版本后缀还是.xls

  • XSSFWorkbook

这个实现类现在在很多公司都可以发现还在使用,它是操作的Excel2003–Excel2007之间的版本,Excel的扩展名是.xlsx

  • SXSSFWorkbook

这个实现类是POI3.8之后的版本才有的,它可以操作Excel2007以后的所有版本Excel,扩展名是.xlsx

HSSFWorkbook

它是POI版本中最常用的方式,不过:

  • 它的缺点是 最多只能导出 65535行,也就是导出的数据函数超过这个数据就会报错;

  • 它的优点是 不会报内存溢出。(因为数据量还不到7w所以内存一般都够用,首先你得明确知道这种方式是将数据先读取到内存中,然后再操作)

XSSFWorkbook
  • 优点:这种形式的出现是为了突破HSSFWorkbook的65535行局限,是为了针对Excel2007版本的1048576行,16384列,最多可以导出104w条数据;

  • 缺点:伴随的问题来了,虽然导出数据行数增加了好多倍,但是随之而来的内存溢出问题也成了噩梦。因为你所创建的book,Sheet,row,cell等在写入到Excel之前,都是存放在内存中的(这还没有算Excel的一些样式格式等等),可想而知,内存不溢出就有点不科学了!!!

SXSSFWorkbook

从POI 3.8版本开始,提供了一种基于XSSF的低内存占用的SXSSF方式:

优点:

  • 这种方式不会一般不会出现内存溢出(它使用了硬盘来换取内存空间,

  • 也就是当内存中数据达到一定程度这些数据会被持久化到硬盘中存储起来,而内存中存的都是最新的数据),

  • 并且支持大型Excel文件的创建(存储百万条数据绰绰有余)。

缺点:

  • 既然一部分数据持久化到了硬盘中,且不能被查看和访问那么就会导致,

  • 在同一时间点我们只能访问一定数量的数据,也就是内存中存储的数据;

  • sheet.clone()方法将不再支持,还是因为持久化的原因;

  • 不再支持对公式的求值,还是因为持久化的原因,在硬盘中的数据没法读取到内存中进行计算;

  • 在使用模板方式下载数据的时候,不能改动表头,还是因为持久化的问题,写到了硬盘里就不能改变了;

2.使用方式哪种看情况

经过了解也知道了这三种Workbook的优点和缺点,那么具体使用哪种方式还是需要看情况的:

我一般会根据这样几种情况做分析选择:

1、当我们经常导入导出的数据不超过7w的情况下,可以使用 HSSFWorkbook 或者 XSSFWorkbook都行;

2、当数据量查过7w并且导出的Excel中不牵扯对Excel的样式,公式,格式等操作的情况下,推荐使用SXSSFWorkbook;

3、当数据量查过7w,并且我们需要操做Excel中的表头,样式,公式等,这时候我们可以使用 XSSFWorkbook 配合进行分批查询,分批写入Excel的方式来做;

3.百万数据导入导出

想要解决问题我们首先要明白自己遇到的问题是什么?

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 :

其实问题所在就是内存溢出,我们只要使用对上面介绍的POI方式即可,主要问题就是原生的POI解决起来相当麻烦。

经过查阅资料翻看到阿里的一款POI封装工具EasyExcel,上面问题等到解决;

针对2:

不能一次性查询出全部数据,我们可以分批进行查询,只不过时多查询几次的问题,况且市面上分页插件很多。此问题好解决。

针对3:

可以将300w条数据写到不同的Sheet中,每一个Sheet写一百万即可。

针对4:

不能一行一行的写入到Excel上,我们可以将分批查询的数据分批写入到Excel中。

针对5:

导入到DB时我们可以将Excel中读取的数据存储到集合中,到了一定数量,直接批量插入到DB中。

针对6:

不能使用Mybatis的批量插入,我们可以使用JDBC的批量插入,配合事务来完成批量插入到DB。即 Excel读取分批+JDBC分批插入+事务。

3.1 模拟500w数据导出

需求:使用EasyExcel完成500w数据的导出。

500w数据的导出解决思路:

  • 首先在查询数据库层面,需要分批进行查询(比如每次查询20w)

  • 每查询一次结束,就使用EasyExcel工具将这些数据写入一次;

  • 当一个Sheet写满了100w条数据,开始将查询的数据写入到另一个Sheet中;

  • 如此循环直到数据全部导出到Excel完毕。

ps:我们需要计算Sheet个数,以及循环写入次数。特别是最后一个Sheet的写入次数

因为你不知道最后一个Sheet会写入多少数据,可能是100w,也可能是25w因为我们这里的500w只是模拟数据,有可能导出的数据比500w多也可能少

ps:我们需要计算写入次数,因为我们使用的分页查询,所以需要注意写入的次数。

其实查询数据库多少次就是写入多少次

准备工作

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

<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 {

    @ExcelProperty(value = "员工编号")

    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 {

    @ExcelProperty(value = "员工编号")

    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;

}

导出核心代码

 

@Resource

private EmpService empService;

/**

 * 分批次导出

 */

@GetMapping("/export")

public void export() throws IOException {

    StopWatch stopWatch = new StopWatch();

    stopWatch.start();

    empService.export();

    stopWatch.stop();

    System.out.println("共计耗时: " + stopWatch.getTotalTimeSeconds()+"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 {

        //记录总数:实际中需要根据查询条件进行统计即可

        //LambdaQueryWrapper<Emp> lambdaQueryWrapper = new QueryWrapper<Emp>().lambda().eq(Emp::getEmpno, 1000001);

        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;

        //计算最后一个sheet需要写入的次数

        Integer lastSheetWriteCount = totalCount % sheetDataRows == 0 ? oneSheetWriteCount : (totalCount % sheetDataRows % writeDataRows == 0 ? (totalCount / sheetDataRows / writeDataRows) : (totalCount / sheetDataRows / 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();

                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();

        }

    }

}

这是我电脑测试时内存占用和CPU使用情况,当然开了其他一些应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/141271.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL如何查找删除重复行?

如何查找重复行 第一步是定义什么样的行才是重复行。多数情况下很简单&#xff1a;它们某几列具有相同的值。本例采用这种定义&#xff0c;或许你对“重复”的定义得很复杂&#xff0c;你需要对sql做些修改。本例要用到的数据样本&#xff1a; create table test(id int not …

mmap如何运用

mmap是什么 mmap是C语言中的一个系统调用&#xff0c;它允许程序将一个文件或者其他的对象映射到进程的地址空间。这种机制使得进程可以直接读取和修改被映射的对象&#xff0c;而不需要通过I/O系统调用来访问。 mmap的主要用途包括&#xff1a; * 将一个普通文件映射到内存中…

解析JSON字符串:属性值为null的时候不被序列化

如果希望属性值为null及不序列化&#xff0c;只序列化不为null的值。 1、测试代码 配置代码&#xff1a; mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); 或者通过注解JsonInclude(JsonInclude.Include.NON_NULL) //常见问题2&#xff1a;属性为null&a…

C++——gcc、clang和cmake以及make

文章目录 1. CMake和make1.1 生成内容区别1.2 CMakeLists.txt和Makefile内容比较2. clang, gcc和make2.1 基本概念2.2 改进历史(gcc,make,cmake,Ninja)1. CMake和make 1.1 生成内容区别 环境CMake生成最终生成WindowsXXX.slnVisual Studio(MSBuild)处理.sln生成.exe可执行二进…

【数据结构】树与二叉树(七):二叉树的遍历(先序、中序、后序及其C语言实现)

文章目录 5.2.1 二叉树二叉树性质引理5.1&#xff1a;二叉树中层数为i的结点至多有 2 i 2^i 2i个&#xff0c;其中 i ≥ 0 i \geq 0 i≥0。引理5.2&#xff1a;高度为k的二叉树中至多有 2 k 1 − 1 2^{k1}-1 2k1−1个结点&#xff0c;其中 k ≥ 0 k \geq 0 k≥0。引理5.3&…

Oracle Unifier 22.12 ~ 23.10 功能改进清单表

序言 时隔近一年&#xff0c;Oracle Unifier 22还没握熟&#xff0c;新版本23便已迭代到23.10&#xff0c;根据甲骨文常规的发布规律&#xff0c;相信不久之后便会正式迎来正式本地版V23&#xff0c;了解Unfier的朋友或许知晓&#xff0c;本地版是云版迭代一年后的版本&#x…

【PG】PostgreSQL 目录结构

目录 1 软件安装目录 2 数据文件目录 base/&#xff1a;存储每个数据库的基本数据文件 global/&#xff1a;包含了全局性质的系统表空间文件 pg_tblspc/&#xff1a;包含了表空间的符号链接 pg_twophase/&#xff1a;包含了两阶段提交中使用的文件 pg_stat_tmp/&#xff…

【系统架构设计】架构核心知识: 3.3 DSSA和ABSD

目录 一 特定领域软件架构DSSA 1 DSSA 2 DSSA的基本活动和产物 3 参与DSSA的人员 4 建立过程

Power Automate-创建计划的云端流

选择第三个计划的云端流 根据实际需求选择开始日期和间隔时间 点击创建 测试运行 点击右上角的测试 选择手动&#xff0c;点击测试&#xff0c;然后运行流 运行失败会有报错原因

springboot引入外部jar,package打包报错找不到程序包XXX

springboot引入外包jar包有两种方法&#xff1a; 一、第一种&#xff1a; 点击idea左上角file&#xff0c;然后点击project选择Modules&#xff0c;点击右侧Dependencies&#xff0c;点击右侧加号选择JARs or directories,然后选择要导入的jar包。这种方式&#xff0c;引入ja…

java网络编程之UDP协议

文章目录 UDP简介一发一收客户端&#xff1a;服务端&#xff1a; 多发多收实现多开客户端&#xff1a;服务端 UDP简介 UDP&#xff08;User Datagram Protocol&#xff09; DatagramSocket 用于创建客户端、服务端DatagramSocket() :创建客户端的Socket对象&#xff0c;系统随…

异常、堆内存溢出、OOM的几种情况【转载】

1、堆内存溢出 【情况一】&#xff1a;   java.lang.OutOfMemoryError: Java heap space&#xff1a;这种是java堆内存不够&#xff0c;一个原因是真不够&#xff0c;另一个原因是程序中有死循环&#xff1b;   如果是java堆内存不够的话&#xff0c;可以通过调整JVM下面…

ubuntu安装mysql8.0.35过程和报错处理

ubuntu安装mysql8.0.35过程 1.更新包列表&#xff1a;首先&#xff0c;确保您的系统已更新到最新状态。运行以下命令来更新包列表和安装最新的软件包&#xff1a; sudo apt update sudo apt upgrade2.安装MySQL服务器&#xff1a;运行以下命令来安装MySQL服务器&#xff1a; …

深度学习模型基于Python+TensorFlow+Django的垃圾识别系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 要使用Python、TensorFlow和Django构建一个垃圾识别系统&#xff0c;您可以按照以下步骤进行操作&#xff1a; 安装…

vite + electron引入itk报错

代码 import { readImageArrayBuffer } from itk-wasm console.log(readImageArrayBuffer)通过itk-wasm官网&#xff0c;创建新的项目vitevue&#xff08;vue2或者vue3&#xff09;&#xff0c;都没问题。加入electeon后包此错。通过排查&#xff0c;意外找到原因&#xff0c;…

ES常用查询命令

一、基本命令 1、获取所有_cat命令 curl -X GET localhost:9200/_cat 2、获取es集群服务健康状态 curl -X GET localhost:9200/_cat/health?v epoch: 时间戳的 Unix 时间戳格式&#xff0c;表示快照生成的时间。 timestamp: 可读性更强的时间戳格式&#xff0c;表示快照生…

105.am40刷机(linux)折腾记1-前期的准备工作1

前段时间在某鱼上逛的时候&#xff0c;发现一款3399的盒子只要150大洋&#xff0c;内心就开始澎拜&#xff0c;一激动就下手了3台&#xff0c;花了450大洋&#xff08;现在想想&#xff0c;心都碎了一地&#xff09;。 然后自己又来来回回折腾了几天&#xff0c;目前能跑上fire…

数据跨领域应用实例—车辆通行大数据应用场景(二)

2023年10月25日&#xff0c;国家数据局正式揭牌。标志着我国数据基础制度正在不断完善&#xff0c;数据资源使用水平稳步提升&#xff0c;数据要素市场将进入发展快车道。当前&#xff0c;数字经济已成为我国经济高质量发展的新动能&#xff0c;国家数据局的成立&#xff0c;在…

【Orangepi Zero2 全志H616】驱动串口实现Tik Tok—VUI(语音交互)

一、编程实现语音和开发板通信 wiringpi库源码demo.c 二、基于前面串口的代码修改实现 uartTool.huartTool.cuartTest.c 三、ADB adb控制指令 四、手机接入Linux热拔插相关 a. 把手机接入开发板 b. 安装adb工具&#xff0c;在终端输入adb安装指令&#xff1a; sudo apt-g…

抢疯了!OpenAI开出1000万美元天价年薪 北大AI博士未毕业拿百万offer

图片 抢疯了&#xff0c;抢疯了&#xff01;OpenAI和谷歌的抢人大战&#xff0c;已经进入白热化。 OpenAI给谷歌员工抛出了终极诱惑——500万到1000万美元的年薪&#xff01;以及来自微软的用不完的算力&#xff01; 图片 这不&#xff0c;谷歌复仇神器Gemini模型的关键研究…