1. 概述

本文将探讨如何高效使用 PreparedStatement。PreparedStatement 是一个预编译 SQL 语句的对象,我们可以重复使用它来执行相同的 SQL 操作。通过不同的实现方式,我们可以显著提升代码质量和应用性能。接下来会分析三种典型用法,并指出其中的关键优化点。

2. 环境准备

首先需要准备数据库连接并创建测试表。我们使用 H2 内存数据库创建一个包含 idfirst_namelast_name 三列的 CUSTOMER 表:

Connection connection = null;

void setupDatabaseAndConnect() throws SQLException {
    connection = DriverManager.getConnection("jdbc:h2:mem:testDB", "dbUser", "dbPassword");
    String createTable = "CREATE TABLE CUSTOMER (id INT, first_name TEXT, last_name TEXT)";
    connection.createStatement().execute(createTable);
}

这里使用 H2 内存数据库建立连接,并将 Connection 对象保存为类变量供后续使用。表的具体结构和数据库类型不影响核心示例,仅作为演示基础。

3. 低效的 PreparedStatement 使用方式

先看一个基础但低效的实现方式:

String SQL = "INSERT INTO CUSTOMER (id, first_name, last_name) VALUES(?,?,?)";
void inefficientUsage() throws SQLException {
    for (int i = 0; i < 10000; i++) {
        PreparedStatement preparedStatement = connection.prepareStatement(SQL);
        preparedStatement.setInt(1, i);
        preparedStatement.setString(2, "firstname" + i);
        preparedStatement.setString(3, "secondname" + i);
        preparedStatement.executeUpdate();
        preparedStatement.close();
    }
}

这段代码在每次循环中都:

  1. 创建新的 PreparedStatement
  2. 设置参数
  3. 执行更新
  4. 关闭对象

验证数据是否插入成功:

int checkRowCount() {
    try (PreparedStatement counter = connection.prepareStatement("SELECT COUNT(*) AS customers FROM CUSTOMER")) {
        ResultSet resultSet = counter.executeQuery();
        resultSet.next();
        int count = resultSet.getInt("customers");
        resultSet.close();
        return count;
    } catch (SQLException e) {
        return -1;
    }
}

测试用例:

@Test
void whenCallingInefficientPreparedStatementMethod_thenRowsAreCreatedAsExpected() throws SQLException {
    ReusePreparedStatement service = new ReusePreparedStatement();
    service.setupDatabaseAndConnect();
    service.inefficientUsage();
    int rowsCreated = service.checkRowCount();
    assertEquals(10000, rowsCreated);
}

虽然功能正常插入了 10,000 条数据,但这种实现存在严重性能问题

  • ❌ 创建了 10,000 次 PreparedStatement 对象
  • ❌ 每次操作都涉及对象创建和销毁的开销
  • ❌ 频繁的数据库交互导致效率低下

4. 简单重用 PreparedStatement

明显的优化是将 PreparedStatement 创建移出循环:

void betterUsage() {
     try (PreparedStatement preparedStatement = connection.prepareStatement(SQL)) {
        for (int i = 0; i < 10000; i++) {
            preparedStatement.setInt(1, i);
            preparedStatement.setString(2, "firstname" + i);
            preparedStatement.setString(3, "secondname" + i);
            preparedStatement.executeUpdate();
        }
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
}

关键改进点

  • ✅ PreparedStatement 只创建一次
  • ✅ 使用 try-with-resources 自动管理资源生命周期
  • ✅ 避免了重复创建/销毁对象的开销

测试用例:

@Test
void whenCallingBetterPreparedStatementMethod_thenRowsAreCreatedAsExpected() throws SQLException {
    ReusePreparedStatement service = new ReusePreparedStatement();
    service.setupDatabaseAndConnect();
    service.betterUsage();
    int rowsCreated = service.checkRowCount();
    assertEquals(10000, rowsCreated);
}

虽然性能提升明显,但仍有不足:

  • ⚠️ 每次循环都单独执行 SQL,数据库交互频繁
  • ⚠️ 任务中断后难以恢复进度
  • ⚠️ 无法保证事务完整性

5. 使用批处理提升效率

最优解是结合批处理(batch)机制:

void bestUsage() {
    try (PreparedStatement preparedStatement = connection.prepareStatement(SQL)) {
        connection.setAutoCommit(false);
        for (int i = 0; i < 10000; i++) {
            preparedStatement.setInt(1, i);
            preparedStatement.setString(2, "firstname" + i);
            preparedStatement.setString(3, "secondname" + i);
            preparedStatement.addBatch();
        }
        preparedStatement.executeBatch();
        try {
           connection.commit();
        } catch (SQLException e) {
            connection.rollback();
            throw e;
        }
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
}

核心优化策略

  1. ✅ 禁用自动提交 (setAutoCommit(false))
  2. ✅ 循环内使用 addBatch() 收集操作
  3. ✅ 最后统一执行批处理 (executeBatch())
  4. ✅ 显式提交事务,失败时回滚

批处理的优势

  • ✅ 大幅减少数据库交互次数
  • ✅ 保证事务原子性(要么全部成功,要么全部回滚)
  • ✅ 中断后可精确恢复(通过记录批处理进度)

💡 实际应用中可分批执行(如每 5000 条提交一次),配合进度日志实现任务恢复。

6. 总结

PreparedStatement 的使用方式直接影响应用性能,最佳实践总结如下:

实现方式 适用场景 优势 劣势
低效方式 单次操作 代码简单 性能极差
简单重用 中小批量 减少对象开销 事务控制弱
批处理 大批量 高性能+事务安全 实现稍复杂

核心原则

  1. 只创建一次 PreparedStatement
  2. 在循环中重复使用
  3. 大批量操作时使用批处理

遵循这些原则,既能避免常见的性能踩坑,又能保证数据操作的安全性和可靠性。