如何在 jOOQ 中惰性读取、拆分记录并流式导出 CSV?
可以保留 fetchLazy(),用一个可复用、大小有上限的 Result 收集拆分后的记录,再分批调用 formatCSV()。表头单独写一次,后续批次关闭表头。这样可以继续使用 jOOQ 的 CSV 格式化逻辑,也能避免为每条输出记录创建一个 Result。
为什么复用 Result 能解决问题
Result 可以作为输出缓冲区,不需要装下全部查询结果。DSLContext.newResult(...) 可以创建带字段信息的空结果集。CSV 表头根据字段信息生成,因此可以用空结果集单独写出表头。DSLContext API、jOOQ CSV 格式化实现
整个过程只创建一个用于输出的 Result:装入一批记录后写出并清空,再继续处理下一批。记录对象和格式化过程仍会产生其他分配;这个做法消除的是逐条创建 Result 的开销。
实现示例
下面假设拆分后的记录与查询结果具有相同的列数、列顺序和兼容的字段类型。splitter 负责业务拆分,每产生一条输出记录,就调用一次 emit.accept(...)。
import java.io.Writer;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.jooq.CSVFormat;
import org.jooq.Record;
import org.jooq.Result;
import org.jooq.ResultQuery;
import org.jooq.impl.DSL;
public final class CsvExport {
public static <R extends Record> void exportCsv(
ResultQuery<R> query,
Writer out,
BiConsumer<R, Consumer<Record>> splitter) {
final int outputBatchSize = 1000;
final CSVFormat headerFormat = CSVFormat.DEFAULT.header(true);
final CSVFormat dataFormat = CSVFormat.DEFAULT.header(false);
try (var cursor = query.fetchSize(1000).fetchLazy()) {
Result<Record> batch = DSL.using(query.configuration())
.newResult(cursor.fields());
// 空 Result 只输出表头,即使没有数据也保留列名。
batch.formatCSV(out, headerFormat);
Consumer<Record> emit = record -> {
batch.add(record);
if (batch.size() >= outputBatchSize) {
batch.formatCSV(out, dataFormat);
batch.clear();
}
};
for (R record : cursor) {
splitter.accept(record, emit);
}
// 写出不足一个批次的剩余记录。
if (!batch.isEmpty()) {
batch.formatCSV(out, dataFormat);
batch.clear();
}
}
}
}
调用时传入实际的拆分逻辑,例如:
CsvExport.exportCsv(query(), out, (record, emit) -> {
// 在这里执行业务拆分。
// 每产生一条输出记录,就调用 emit.accept(outputRecord)。
// 此处仅演示原样输出一条记录。
emit.accept(record);
});
这个回调与 mapMulti() 中的逐条发射思路相同。处理一条输入记录时,可以多次调用 emit.accept(...),无需先收集完整的转换结果。
表头和格式如何保持一致
示例只在开始时使用 header(true),后续批次都使用 header(false)。CSVFormat.header(boolean) 控制是否输出列名表头。CSVFormat API
如果原来配置了分隔符、换行符、引号或 null 表示方式,两份格式配置应使用相同的设置,只有 header 不同。继续调用 jOOQ 的 formatCSV(),也能保留它对字段值的格式化处理。
如果拆分改变了输出列结构,需要用实际输出字段创建 Result,不能继续直接使用 cursor.fields()。
大数据量导出时的注意事项
- 输出缓冲区最多保留
outputBatchSize条记录,但整个程序的内存占用不止于此。JDBC 驱动缓冲、单条记录大小和拆分逻辑都会影响实际内存占用。 - 输入端的
fetchSize(1000)与输出端的outputBatchSize是两个独立设置。前者影响数据库读取,具体效果取决于数据库和 JDBC 驱动;后者控制应用保留多少条待输出记录。 - 拆分时应边生成边调用
emit.accept(...)。如果先构造一个巨大的集合,再逐条发射,仍可能占用大量内存。 batch.add(record)保存的是记录引用。不要反复修改同一个Record并将它作为多条结果发射;应为各条输出提供独立记录,或者在加入缓冲区前复制数据。- 在所引用的 jOOQ 3.20.0 实现中,每次
formatCSV()结束都会调用Writer.flush()。按批次输出可以减少格式化调用和刷新次数,但具体性能需要结合实际版本、数据和输出目标测量。CSV 格式化实现
保留 try-with-resources 来关闭游标。jOOQ 的惰性读取会持有底层 ResultSet,不应只依赖遍历完成后自动关闭;示例中的 Writer 由调用方管理生命周期。惰性读取说明
备注:内容仅供参考。
