1. 概述

当处理与数据库交互的 Java 应用程序时,我们经常需要将查询结果导出为易于消费或共享的格式。CSV(逗号分隔值)是常用的格式之一。为此,我们可以将 JDBC ResultSet(用于保存数据库查询结果的数据结构)转换为 CSV 文件。

本文将探讨两种将 ResultSet 转换为 CSV 格式的方法。

2. 环境设置

为了测试,我们将使用具有以下架构的 Employees 表:

CREATE TABLE EMPLOYEES (
    id SERIAL PRIMARY KEY ,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    salary DECIMAL(10, 2)
);

3. 使用自定义逻辑

在这种方法中,我们首先查询 Employees 表获取记录:

Employee table records

如上所示,Employees 表记录包含特殊字符。然后我们遍历 ResultSet 并将每条记录转换为 CSV:

List<String> toCsv(Connection connection) throws SQLException {
    List<String> csvRecords = new ArrayList<>();
    PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM employees");
    ResultSet resultSet = preparedStatement.executeQuery();
    while (resultSet.next()) {
        String eachRecord = formatForCsv(resultSet.getString("first_name")) + "," + 
          formatForCsv(resultSet.getString("last_name")) + "," + 
          "\"" + resultSet.getDouble("salary") + "\"";
        csvRecords.add(eachRecord);
    }
    return csvRecords;
}

对于每条记录,我们将每个列(如 first_namelast_name 等)作为字符串读取。我们创建了 formatForCSV() 辅助方法,用于将换行符(\n)转义为 \\n,回车符(\r)转义为 \\r,双引号(")转义为 ""

String formatForCsv(String value) {
    return "\"" + value
      .replace("\n", "\\n")
      .replace("\r", "\\r")
      .replace("\"", "\"\"")
      + "\"";
}

如果执行我们的逻辑,我们将获得 ResultSet 转换的 CSV 结果:

CSV records of employee table

我们可以编写单元测试来验证逻辑是否按预期工作:

@Test
void givenEmployeeRecordsInEmployeeTable_whenResultSetToCSVInvoked_thenReturnsCSV() throws SQLException, IOException {
    insertRecords();
    ResultSetToCSV logic = new ResultSetToCSV();
    List<String> csvRecords = logic.toCsv(connection);
    Files.write(Paths.get("/Users/surajmishra/Documents/work-space/employee.csv"), csvRecords);

    assertThat(csvRecords.size()).isEqualTo(3);

    for (int i = 1; i <= 2; i++) {
        assertThat(csvRecords.get(i - 1))
          .isEqualTo("\"first" + i + "\"," + "\"last" + i + "\"," + "\"" + String.format("%.1f", 100.00 * i) + "\"");
    }

    assertThat(csvRecords.get(2))
      .isEqualTo("\"\"\"first\\nfirst1\\nfirst2!\"\"1\"," + "\"\"\"last!\\nlast1!\"\"1\"," + "\"100.0\"");
}

4. 使用第三方依赖

有多种开源库可以帮助我们将 ResultSet 转换为 CSV。其中流行的库之一是 OpenCSV。

我们可以在 pom.xml 文件中添加它:

<dependency>
    <groupId>com.opencsv</groupId>
    <artifactId>opencsv</artifactId>
    <version>5.9</version>
</dependency>

在下面的逻辑中,我们从数据库读取所有 Employees 记录作为 ResultSet,然后使用 OpenCSV 将它们转换为 CSV:

String toCsvWithOpenCsv(Connection connection) throws SQLException {
    PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM employees");
    ResultSet resultSet = preparedStatement.executeQuery();
    StringWriter stringWriter = new StringWriter();
    CSVWriter csvWriter = new CSVWriter(stringWriter,
      CSVWriter.DEFAULT_SEPARATOR,
      CSVWriter.DEFAULT_QUOTE_CHARACTER,  // 默认引号字符是双引号
      CSVWriter.DEFAULT_ESCAPE_CHARACTER, // 默认转义字符是双引号
      CSVWriter.DEFAULT_LINE_END);

    ResultSetMetaData metaData = resultSet.getMetaData();
    int columnCount = metaData.getColumnCount();
    String[] row = new String[columnCount-1];

    while (resultSet.next()) {
        row[0] = resultSet.getString("first_name")
          .replace("\n", "\\n")
          .replace("\r", "\\r");

        row[1] = resultSet.getString("last_name")
          .replace("\n", "\\n")
          .replace("\r", "\\r");

        row[2] = String.valueOf(resultSet.getDouble("salary"));

        csvWriter.writeNext(row);
    }
    
    return stringWriter.toString();
}

在上述逻辑中,我们还对每行进行转义处理,处理列值中的任何特殊字符,如换行符、回车符或双引号。我们使用 csvWriter 为 CSV 写入行数组。然后为了获取 String,我们使用 toString() 方法将包含 CSV 数据的 stringWriter 转换为 String

我们可以编写单元测试来验证逻辑是否按预期工作:

@Test
void givenEmployeeRecordsInEmployeeTable_whenResultSetToCSVWithOpenCsvInvoked_thenReturnsCSV() throws SQLException {
    insertRecords();
    ResultSetToCSV logic = new ResultSetToCSV();
    String csvRecords = logic.toCsvWithOpenCsv(connection);
    String[] split = csvRecords.split("\n");
    assertThat(split.length).isEqualTo(3);
    for (int i = 1; i <= 2; i++) {
        assertThat(split[i - 1])
          .isEqualTo("\"first" + i + "\"," + "\"last" + i + "\"," + "\"" + String.format("%.1f", 100.00 * i) + "\"");
    }

    assertThat(split[2])
      .isEqualTo("\"\"\"first\\nfirst1\\nfirst2!\"\"1\"," + "\"\"\"last!\\nlast1!\"\"1\"," + "\"100.0\"");
}

5. 总结

在本教程中,我们学习了使用 OpenCSV 和自定义逻辑将 JDBC ResultSet 转换为 CSV 的两种方法。虽然我们可以始终使用第三方依赖(如 OpenCSV)来执行转换,但编写自定义逻辑也并不复杂。