【jeecg大数据导出Excel以及优化】

jeecg大数据导出Excel以及优化

控制层优化

版本1(初始版本)

   @ApiOperation(value = "导出", notes = "导出")
   @GetMapping("/exportTruckNoReportList")
   public ModelAndView exportTruckNoReportList(@RequestParam(name = "sap_type", required = false) String sap_type,
                                               @RequestParam(name = "start_time", required = false) String start_time,
                                               @RequestParam(name = "end_time", required = false) String end_time,
                                               @RequestParam(name = "gascode", required = false) String gas_code,
                                               @RequestParam(name = "truck_no", required = false) String truck_no,
                                               @RequestParam(name = "is_all_org", defaultValue = "false") String isAllOrg,
                                               HttpServletRequest req) {
       UserInfo userInfo = BaseContextHandler.getUserInfo();

       QueryWrapper<TruckNoReportExport> queryWrapper = new QueryWrapper<>();
       if (StringUtils.isNotBlank(start_time) && StringUtils.isNotBlank(end_time)) {
           queryWrapper.between("trade_at", start_time, end_time);
       } else if (StringUtils.isBlank(start_time) && StringUtils.isNotBlank(end_time)) {
           queryWrapper.le("trade_at", end_time);
       } else if (StringUtils.isNotBlank(start_time) && StringUtils.isBlank(end_time)) {
           queryWrapper.ge("trade_at", start_time);
       }

       queryWrapper.like(StringUtils.isNotBlank(truck_no), "truck_no", truck_no);
       queryWrapper.ne("trade_type","1200");

       if ("true".equals(isAllOrg)) {
           queryWrapper.eq((StringUtils.isNotBlank(gas_code)), "gas_code", gas_code);
       }else {
           queryWrapper.like((StringUtils.isNotBlank(gas_code)), "gas_code", gas_code);
       }
       queryWrapper.groupBy("truck_no");
       queryWrapper.groupBy("gas_code");
       queryWrapper.orderByDesc("trade_at");

       long startTime = System.currentTimeMillis();

       List<TruckNoReportExport> list = tradesOrderPayService.exportTruckNoReportList(queryWrapper);

       long endTime1 = System.currentTimeMillis();
       long elapsedTime1 = endTime1 - startTime;
       log.info("从数据库查询时间{}", String.valueOf(elapsedTime1));

       long endTime2 = System.currentTimeMillis();

       log.info("导出数据量:{}", list.size());
       long elapsedTime = endTime2 - endTime1;
       log.info("循环处理时间:{}", String.valueOf(elapsedTime));
       ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
       //此处设置的filename无效 ,前端会重更新设置一下
       mv.addObject(NormalExcelConstants.FILE_NAME, "统计");
       mv.addObject(NormalExcelConstants.CLASS, TruckNoReportExport.class);
       //update-begin--Author:liusq  Date:20210126 for:图片导出报错,ImageBasePath未设置--------------------
       ExportParams exportParams = new ExportParams("统计", "导出人:" + userInfo.getTrueName(), "统计");
//        exportParams.setImageBasePath(upLoadPath);
       //update-end--Author:liusq  Date:20210126 for:图片导出报错,ImageBasePath未设置----------------------
       mv.addObject(NormalExcelConstants.PARAMS, exportParams);
       mv.addObject(NormalExcelConstants.DATA_LIST, list);

       return mv;
   }

版本2(通过分页查询,优化了查询时内存的开销)

   @ApiOperation(value = "导出", notes = "导出")
   @GetMapping("/exportTruckNoReportList")
   public ModelAndView exportTruckNoReportList(@RequestParam(name = "sap_type", required = false) String sap_type,
                                               @RequestParam(name = "start_time", required = false) String start_time,
                                               @RequestParam(name = "end_time", required = false) String end_time,
                                               @RequestParam(name = "gascode", required = false) String gas_code,
                                               @RequestParam(name = "truck_no", required = false) String truck_no,
                                               @RequestParam(name = "is_all_org", defaultValue = "false") String isAllOrg,
                                               HttpServletRequest req) {
       UserInfo userInfo = BaseContextHandler.getUserInfo();

       QueryWrapper<TruckNoReportExport> queryWrapper = new QueryWrapper<>();
       if (StringUtils.isNotBlank(start_time) && StringUtils.isNotBlank(end_time)) {
           queryWrapper.between("trade_at", start_time, end_time);
       } else if (StringUtils.isBlank(start_time) && StringUtils.isNotBlank(end_time)) {
           queryWrapper.le("trade_at", end_time);
       } else if (StringUtils.isNotBlank(start_time) && StringUtils.isBlank(end_time)) {
           queryWrapper.ge("trade_at", start_time);
       }

       queryWrapper.like(StringUtils.isNotBlank(truck_no), "truck_no", truck_no);
       queryWrapper.ne("trade_type","1200");

       if ("true".equals(isAllOrg)) {
           queryWrapper.eq((StringUtils.isNotBlank(gas_code)), "gas_code", gas_code);
       }else {
           queryWrapper.like((StringUtils.isNotBlank(gas_code)), "gas_code", gas_code);
       }
       queryWrapper.groupBy("truck_no");
       queryWrapper.groupBy("gas_code");
       queryWrapper.orderByDesc("trade_at");

       long startTime = System.currentTimeMillis();

       Page<TruckNoReportExport> pages = new Page<>(1, 3000);
       Page<TruckNoReportExport> pageList = tradesOrderPayService.exportTruckNoReportList(pages,queryWrapper);

       List<TruckNoReportExport> list = new ArrayList<>();

       for (int pageNo = 1; pageNo <= pageList.getPages(); pageNo++) {
           Page<TruckNoReportExport> page1 = new Page<>(pageNo, 3000);
           // 设置不执行 count 查询,每次page查询都会先执行一遍count太花费时间了,这里已经知道有数据了所以不需要
           page1.setSearchCount(false);
           Page<TruckNoReportExport> pageList1 = tradesOrderPayService.exportTruckNoReportList(page1,queryWrapper);
           list.addAll(pageList1.getRecords());
       }

       long endTime1 = System.currentTimeMillis();
       long elapsedTime1 = endTime1 - startTime;
       log.info("从数据库查询时间{}", String.valueOf(elapsedTime1));

       long endTime2 = System.currentTimeMillis();

       log.info("导出数据量:{}", list.size());
       long elapsedTime = endTime2 - endTime1;
       log.info("循环处理时间:{}", String.valueOf(elapsedTime));
       ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
       //此处设置的filename无效 ,前端会重更新设置一下
       mv.addObject(NormalExcelConstants.FILE_NAME, "统计");
       mv.addObject(NormalExcelConstants.CLASS, TruckNoReportExport.class);
       //update-begin--Author:liusq  Date:20210126 for:图片导出报错,ImageBasePath未设置--------------------
       ExportParams exportParams = new ExportParams("统计", "导出人:" + userInfo.getTrueName(), "统计");
//        exportParams.setImageBasePath(upLoadPath);
       //update-end--Author:liusq  Date:20210126 for:图片导出报错,ImageBasePath未设置----------------------
       mv.addObject(NormalExcelConstants.PARAMS, exportParams);
       mv.addObject(NormalExcelConstants.DATA_LIST, list);

       return mv;
   }

版本3(把数据存入OOS,用户通过OOS下载,节省内存开销)

**部分代码实例**
(略)//oos存表-导出中
 long startTime = System.currentTimeMillis();

        ByteArrayOutputStream byteArrayOutputStream = null;
        ByteArrayInputStream byteArrayInputStream = null;
        try {

            byteArrayOutputStream = new ByteArrayOutputStream();
            long count  = tradesOrderPayService.selectCount(queryWrapper);// 总记录数
            long pageSize = 3000;// 每页记录数
            int totalPages = (int) Math.ceil((double) count / pageSize); // 计算总页数


            HSSFWorkbook workbook = null;
            for (int pageNo = 1; pageNo <= totalPages; pageNo++) {
                List<TradesOrderPayExport> lists = tradesOrderPayService.exportOrderPayList((pageNo - 1) * pageSize,pageSize, queryWrapper);

                log.info("获取当前{}", System.currentTimeMillis() - startTime);

                ExcelUtil<TradesOrderPayExport> util = new ExcelUtil<>(TradesOrderPayExport.class);
                workbook = util.convert2WorkbookPage(lists, workbook,"导出",pageNo,3000);

            }

            long endTime1 = System.currentTimeMillis();
            long elapsedTime1 = endTime1 - startTime;
            log.info("从数据库查询时间{}", String.valueOf(elapsedTime1));

            long endTime2 = System.currentTimeMillis();

            log.info("导出数据量:{}", count);

            workbook.write(byteArrayOutputStream);
            byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());

            String bizPath = "total/prod/";
            String format = DateUtil.format(new Date(), "yyyy-MM-dd");
            long l = System.currentTimeMillis();
            bizPath = bizPath + format + "/" + l + "导出.xls";

            String upload = OssBootUtil.upload(byteArrayInputStream, bizPath);
            System.err.println(upload);

            fossUploadUrl1.setUrl(upload);
            fossUploadUrl1.setUpdateTime(new Date());
            fossUploadUrl1.setUpdateName(userInfo.getTrueName());
            fossUploadUrl1.setStatus("2");//oos存表-导出成功
            fossApi.updateUploadUrl(fossUploadUrl1);

            long elapsedTime = endTime2 - endTime1;
            log.info("上传成功时间:{}", String.valueOf(elapsedTime));

        } catch (IOException e) {

            fossUploadUrl1.setUpdateTime(new Date());
            fossUploadUrl1.setUpdateName(userInfo.getTrueName());
            fossUploadUrl1.setStatus("3");//oos存表-导出失败
            fossApi.updateUploadUrl(fossUploadUrl1);
            throw new RuntimeException(e);
        } finally {
            if(byteArrayOutputStream != null){
                try {
                    byteArrayOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(byteArrayInputStream != null){
                try {
                    byteArrayInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
总体优化思路:

1.建立基础的连表查询,要求达到需求
2.去除不必要的字段,尽量`减少连表`,将数据量比较少切不变动的表使用case替换
3.表里建议`索引优化`
4.发现查询速度还是很慢,通过`建立视图`,优化连表查询速度,同时视图查询会导致索引失效
5.查询数据量比较大,几百名用户同时导出会造成内存溢出的问题,那么改成`分页查询`,测试到以数万条的数据量,每次分页以3000时间空间上都比较可以接受
6.page分页查询执行sql时,内部会自动执行一遍count()查询,这里页数已经查询出来了,可以`去掉count()`减少查询时间
7.数据量太大时还是会造成内存溢出的问题,所以将查询出来的数据写入excel里再`放入oos里`,建立个表存放下载地址,时间,用户,前端直接通过存放地址在oos下载,考虑到节省内存,可以分页的时候`循环写入excel`里,存入流,再上传到oos,修改`保留地址`
8.查询速度还是慢了,发现sql可以继续优化,通过`将主表进行子查询嵌套`的方式,可以先查询主表,再添加其余的连表字段,那么就是说`查询条件可以也放入到主表`里通过子查询先查出来,再进行连表,优化了不少的查询时间,==但是这里发现排序放到主表失效了==
9.发现mybatisplus的分页条查询,会把查询条件替换到${ew.customSqlSegment}位置,但是分页仍然会放到连表之后,那么把`分页也放到主表`里把。那么需要传入#{pageNum},#{pageSize}两个字段,增加查询一个`查询总个数的count`方法(通过`查询主表`个数就行了,这里我是直接查询视图,因为字段重命名了),
10.最后把`排序`加到连表的最后面

**这里给出最终优化方案的查询sql实例**

   <select id="exportOrderPayList" resultType="com.bee.trades.api.vo.TradesOrderPayExport">
       select
           v1.*,
           `info`.`oil_name` AS `oil_total_name`,
           round( ( `info`.`mac_price` / 100 ), 2 ) AS `mac_price`,
           `info`.`mileage` AS `mileage`,
           `info`.`remark` AS `remark`,
           `org1`.`org_name` AS `gas_name`,
           `org1`.`org_name` AS `org_name`,
           `org1`.`org_root_name` AS `org_root_name`,
           `station`.`station_name` AS `station_name`,
           ( CASE WHEN ( `station`.`source` = 1 ) THEN '自营' WHEN ( `station`.`source` = 2 ) THEN '合作对接' ELSE '' END ) AS `station_source_name`,
           `supplier`.`supplier_name` AS `pname`

       FROM   (
          select * from v_order_pay_view //这里是主表的视图,这里有两千行放入到mapper层会影响到控制台查看数据
           ${ew.customSqlSegment}//mybatisplus取查询条件
           limit #{pageNum},#{pageSize}//分页
       ) AS v1

       LEFT JOIN `is_trades`.`trades_order_pay_info` `info` ON  `v1`.`id` = `info`.`order_pay_id`
       LEFT JOIN `is_base`.`base_org` `org1` ON  `v1`.`gas_code` =  `org1`.`org_code`
       LEFT JOIN `is_gas`.`gas_station` `station` ON  `v1`.`station_id` =  `station`.`id`
       LEFT JOIN `is_base`.`base_supplier` `supplier` ON `v1`.`pcode` =  `supplier`.`scode`
       ORDER BY v1.trade_at DESC
   </select>
   <select id="getCount" resultType="java.lang.Long"> //查询总条数
       select count(0) from v_order_pay_view
       ${ew.customSqlSegment}
   </select>

sql优化

1.表里建立索引优化

2.建立视图查询

3.连表获取所有的字段

4.尽量减少连表,数据量比较少的表使用case替换

5.去掉不必要的字段

注意:这里有一个问题

假如使用了SUM等统计字段,后面一定要接GROUP BY进行分组

如果是先根据条件查询,再分组那么就不能根据视图查询,下面代码实例

    <select id="exportTruckNoReportList" resultType="com.bee.trades.api.vo.TruckNoReportExport">
        SELECT
            v1.*,
            `org1`.`org_name` AS gas_name,
            `org2`.`org_name` AS org_root_name
        FROM
        (
            SELECT
                `trades`.`trade_at` AS `trade_at`,
                `trades`.`trade_type` AS `trade_type`,
                ( CASE WHEN ( `trades`.`trade_type` LIKE '11%' ) THEN '加油' WHEN ( `trades`.`trade_type` LIKE '12%' ) THEN '撤销' END ) AS `trade_type_name`,
                round( ( sum( `trades`.`pay_money` ) / 100 ), 2 ) AS `pay_money`,
                `trades`.`is_oil` AS `is_oil`,
                `trades`.`oil_num` AS `oil_num`,
                `trades`.`oil_name` AS `oil_name`,
                sum( IF ( ( `trades`.`is_oil` = 11 ), `trades`.`oil_num`, 0 ) ) AS `oil_num_count`,
                round( ( sum( IF ( ( `trades`.`is_oil` = 11 ), `trades`.`pay_money`, 0 ) ) / 100 ), 2 ) AS `oil_pay_money`,
                round( ( sum( IF ( ( `trades`.`is_oil` = 12 ), `trades`.`pay_money`, 0 ) ) / 100 ), 2 ) AS `not_oil_pay_money`,
                round( ( sum( IF ( ( `trades`.`oil_name` = 'e6d070d8-f95c-11e8-bfa7-02abba8d56d2' ), `trades`.`pay_money`, 0 ) ) / 100 ), 2 ) AS `diesel_oil_pay_money`,
                sum( IF ( ( `trades`.`oil_name` = 'e6d070d8-f95c-11e8-bfa7-02abba8d56d2' ), `trades`.`oil_num`, 0 ) ) AS `diesel_oil_num`,
                round( ( sum( IF ( ( `trades`.`oil_name` = 'e07bc2be-f95c-11e8-8f4b-02abba978494' ), `trades`.`pay_money`, 0 ) ) / 100 ), 2 ) AS `gasoline_pay_money`,
                sum( IF ( ( `trades`.`oil_name` = 'e07bc2be-f95c-11e8-8f4b-02abba978494' ), `trades`.`oil_num`, 0 ) ) AS `gasoline_oil_num`,
                `trades`.`truck_no` AS `truck_no`,
                `trades`.`gas_code` AS `gas_code`,
                CASE
                WHEN LENGTH( `trades`.`gas_code` ) > 0 THEN
                SUBSTRING( `trades`.`gas_code`, 1, 6 ) ELSE NULL
                END AS org_root
            FROM
                `is_trades`.`trades_order_pay` `trades`

                ${ew.customSqlSegment}
            ) AS v1
        LEFT JOIN v_base_org_name_view `org1` ON v1.gas_code = org1.org_code
        LEFT JOIN v_base_org_name_view `org2` ON v1.gas_code = org2.org_code
    </select>
//这里通过连表查询了gas_code对应的机构名称gas_name,和对应的顶级机构名称org_root_name
//通过查询之后再连表,优化了查询时间

${ew.customSqlSegment} 是mybatisplus的参数接收,内部是使用ew接收的参数

mapper层

 Page<TruckNoReportExport> exportTruckNoReportList(@Param(Constants.WRAPPER) QueryWrapper<TruckNoReportExport> queryWrapper, Page<TruckNoReportExport> pages);

entity实体类

package com.bee.trades.api.vo;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;

/**
 * 功能描述
 *
 * @author: scott
 * @date: 2023年12月01日 20:23
 */
@Data
public class TruckNoReportExport implements Serializable {
    private static final long serialVersionUID = 1L;


    /**
     * 车牌号
     */
    @Excel(name = "车牌号", width = 15)
    @ApiModelProperty(value = "车牌号")
    @JsonProperty("truck_no")
    private String truckNo;
    /**
     * 所属机构
     */
    @ApiModelProperty(value = "所属机构")
    @JsonProperty("gas_code")
    private String gasCode;

    @Excel(name = "所属机构", width = 25)
    @JsonProperty("gas_name")
    private String gasName;
    /**
     * 顶级机构编码
     */
    @ApiModelProperty(value = "顶级机构编码")
    @JsonProperty("org_root")
    private String orgRoot;

    @Excel(name = "顶级机构", width = 25)
    @JsonProperty("org_root_name")
    private String orgRootName;
    /**
     * 消费总额
     */
    @Excel(name = "消费总额", width = 15, type = 4)
    @ApiModelProperty(value = "消费总额")
    @JsonProperty("pay_money")
    private BigDecimal payMoney;
    /**
     * 油品总量
     */
    @Excel(name = "油品总量", width = 15, type = 4)
    @ApiModelProperty(value = "油品总量")
    @JsonProperty("oil_num_count")
    private BigDecimal oilNumCount;
    /**
     * 油品总额
     */
    @Excel(name = "油品总额", width = 15, type = 4)
    @ApiModelProperty(value = "油品总额")
    @JsonProperty("oil_pay_money")
    private BigDecimal oilPayMoney;
    /**
     * 柴油总量
     */
    @Excel(name = "柴油总量", width = 15, type = 4)
    @ApiModelProperty(value = "柴油总量")
    @JsonProperty("diesel_oil_num")
    private BigDecimal dieselOilNum;
    /**
     * 柴油总额
     */
    @Excel(name = "柴油总额", width = 15, type = 4)
    @ApiModelProperty(value = "柴油总额")
    @JsonProperty("diesel_oil_pay_money")
    private BigDecimal dieselOilPayMoney;
    /**
     * 汽油总量
     */
    @Excel(name = "汽油总量", width = 15, type = 4)
    @ApiModelProperty(value = "汽油总量")
    @JsonProperty("gasoline_oil_num")
    private BigDecimal gasolineOilNum;
    /**
     * 汽油总额
     */
    @Excel(name = "汽油总额", width = 15, type = 4)
    @ApiModelProperty(value = "汽油总额")
    @JsonProperty("gasoline_pay_money")
    private BigDecimal gasolinePayMoney;
    /**
     * 非油总额
     */
    @Excel(name = "非油总额", width = 15, type = 4)
    @ApiModelProperty(value = "非油总额")
    @JsonProperty("not_oil_pay_money")
    private BigDecimal notOilPayMoney;


    /**
     * 交易时间
     */
    @Excel(name = "交易时间", width = 25, format = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
    @ApiModelProperty(value = "交易时间")
    @JsonProperty("trade_at")
    private Date tradeAt;
    /**
     * 交易类型 1开头代表加油,正常加油1111,修改后的1112,加油补录 1113 撤销 1200 撤销中 1201
     */
    @ApiModelProperty(value = "交易类型")
    @JsonProperty("trade_type")
    private String tradeType;

    @Excel(name = "交易类型", width = 25)
    @JsonProperty("trade_type_name")
    private String tradeTypeName;
}
  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值