把报表导出从分页查询中拆出来

Oct 9, 2026 min read

报表能正常显示一页筛选结果,导出相同范围却明显更慢。流程已经使用流式工作簿写入器,批次大小和写入器配置自然成为排查方向。

沿数据提供层继续检查,发现每个输出批次都会重新进入列表分页流程:计算总数、执行分页查询、转换这一批数据,再追加到工作簿。文件在逐批写入,数据库执行却不断重新开始。

本次本地修复让导出拥有独立的执行生命周期。下文依据本地测试和受控只读比对说明已完成的部分;修复包尚未部署,生产 HTTP 提速也尚未验收。

先统计语句执行次数

列表页需要一段结果,有时还需要总数来计算页码。全量下载需要读完本次选中的结果,再生成完整文件。

旧实现把每个写入批次绑定到新的分页操作,反复支付计数和分页执行的成本。原本用于控制处理内存的小批次,同时决定了数据库重复工作的次数。

检查时分别记录:

  • 数据 SELECT 执行次数。
  • 结果总数 SELECT 执行次数。

修复后,两者分别为一次和零次。这里统计的是导出结果的取数与计数语句,权限、报表定义和列配置仍可能有自己的查询。

驱动从正在执行的结果中继续取行,也与重新执行 SELECT 有区别。JDBC 把 fetch size 定义为给驱动的取行数量提示;仅修改这个提示,无法消除代码中重新执行查询的循环。

消费一个游标,保留外围契约

新增入口限定到目标报表,继续使用既有权限检查、当前用户范围、业务筛选、导出列和值转换。只在导出结果选择中去除分页参数。

数据提供层使用 MyBatis 游标,即可以关闭、逐步读取查询结果的惰性迭代器。这里的游标与列表接口传给客户端的翻页令牌不同。MyBatis 文档说明了这份契约。消费期间,数据库会话保持有效。

处理顺序为:

  1. 校验权限,确定报表和列配置。
  2. 用本次业务筛选打开一个结果游标。
  3. 读取有界批次,调用原转换器并写入单元格。
  4. 持续消费同一个游标。
  5. 完成并检查工作簿,再发送文件。

转换和写入仍然分批,批次不再触发新的分页查询或总数查询。

分别检查读取与写入的资源占用

修复继续使用 Apache POI 的 SXSSF 流式 XLSX 写入器。POI 文档说明,它只保留一个滑动行窗口,较早的行会写入磁盘;临时文件需要显式清理,共享字符串等功能仍可能消耗较多内存。

链路两端都需要检查。若数据提供层先加载所有行,流式写入器无法消除此前的内存占用;若消费方把游标结果重新收集成完整列表,也会失去增量读取的收益。

数据库会话、游标、工作簿、输出流及临时文件都有明确的关闭责任。测试检查了成功、查询失败和模拟客户端断线后的清理,范围同时包括写入器临时文件与最终 XLSX。

生成有效文件后再开始交付

实现先生成并检查临时 XLSX,再发送下载字节。暂存增加了 I/O,也推迟了传输开始时间,同时让生成阶段的失败可以在成功下载响应提交前处理。

Servlet 响应契约规定,响应提交后,状态和响应头已经发出,不能再重置。文件暂存无法阻止后续网络失败,它分开了有效文件生成与文件交付。

实现还限制了工作表容量。空结果仍输出原配置的完整表头。

核验等价性,说明计时范围

检查覆盖单条结果、空结果,以及较短和较长的时间范围。重复的本地比较使用相同筛选和受控只读快照,新路径的中位耗时更低。

验收同时检查:

  • 业务字段的多重集合,即保留每条记录出现次数的集合,以及相关汇总值。
  • 表头、配置列顺序、单元格类型和值。
  • 原有权限和参数处理。
  • 一次数据查询、零次结果总数查询。
  • 异常后的资源关闭及临时文件删除。
  • 实际本地 HTTP 下载与重复请求。

独立生成的展示序号,也就是行编号,被明确排除在等价性比较之外,业务字段仍必须一致。

本地计时包含数据库读取、转换及文件生成,没有覆盖生产路由、真实服务并发或用户浏览器的完整下载。部署、请求级日志和生产耗时仍需分别验收。

全量导出适合拥有独立的执行生命周期:沿用已验证的权限与转换规则,一次消费本次结果,并把有效文件生成、交付和清理作为明确的检查点。