1. 概述
本文将探讨如何高效使用 PreparedStatement。PreparedStatement 是一个预编译 SQL 语句的对象,我们可以重复使用它来执行相同的 SQL 操作。通过不同的实现方式,我们可以显著提升代码质量和应用性能。接下来会分析三种典型用法,并指出其中的关键优化点。
2. 环境准备
首先需要准备数据库连接并创建测试表。我们使用 H2 内存数据库创建一个包含 id
、first_name
和 last_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();
}
}
这段代码在每次循环中都:
- 创建新的 PreparedStatement
- 设置参数
- 执行更新
- 关闭对象
验证数据是否插入成功:
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);
}
}
核心优化策略:
- ✅ 禁用自动提交 (
setAutoCommit(false)
) - ✅ 循环内使用
addBatch()
收集操作 - ✅ 最后统一执行批处理 (
executeBatch()
) - ✅ 显式提交事务,失败时回滚
批处理的优势:
- ✅ 大幅减少数据库交互次数
- ✅ 保证事务原子性(要么全部成功,要么全部回滚)
- ✅ 中断后可精确恢复(通过记录批处理进度)
💡 实际应用中可分批执行(如每 5000 条提交一次),配合进度日志实现任务恢复。
6. 总结
PreparedStatement 的使用方式直接影响应用性能,最佳实践总结如下:
实现方式 | 适用场景 | 优势 | 劣势 |
---|---|---|---|
低效方式 | 单次操作 | 代码简单 | 性能极差 |
简单重用 | 中小批量 | 减少对象开销 | 事务控制弱 |
批处理 | 大批量 | 高性能+事务安全 | 实现稍复杂 |
核心原则:
- ✅ 只创建一次 PreparedStatement
- ✅ 在循环中重复使用
- ✅ 大批量操作时使用批处理
遵循这些原则,既能避免常见的性能踩坑,又能保证数据操作的安全性和可靠性。