1. 引言
在数据库查询中,经常需要根据一组输入值匹配某列数据。实现方式有多种,IN 子句就是其中一种高效解决方案。本文将探讨如何结合 JDBC 的 PreparedStatement 使用 IN 子句,并对比不同实现方式的优劣。
2. 环境准备
先创建一个 Customer
表并插入测试数据,方便后续演示 IN 子句查询:
void populateDB() throws SQLException {
String createTable = "CREATE TABLE CUSTOMER (id INT, first_name VARCHAR(50), last_name VARCHAR(50))";
connection.createStatement().execute(createTable);
String load = "INSERT INTO CUSTOMER (id, first_name, last_name) VALUES(?,?,?)";
IntStream.rangeClosed(1, 100)
.forEach(i -> {
PreparedStatement preparedStatement1 = null;
try {
preparedStatement1 = connection.prepareStatement(load);
preparedStatement1.setInt(1, i);
preparedStatement1.setString(2, "firstname" + i);
preparedStatement1.setString(3, "lastname" + i);
preparedStatement1.execute();
} catch (SQLException e) {
throw new RuntimeException(e);
}
});
}
3. PreparedStatement 使用技巧
PreparedStatement 是预编译的 SQL 语句,可通过不同参数高效复用。下面介绍三种在 PreparedStatement 中使用 IN 子句的方案:
3.1. 基于 StringBuilder 的 IN 子句
最简单粗暴的方式:手动拼接占位符。利用 StringBuilder 高效拼接字符串,避免创建过多临时对象:
ResultSet populateParamsWithStringBuilder(Connection connection, List<Integer> ids)
throws SQLException {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < ids.size(); i++) {
stringBuilder.append("?,");
}
String placeHolders = stringBuilder.deleteCharAt(stringBuilder.length() - 1)
.toString();
String sql = "select * from customer where id in (" + placeHolders + ")";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
for (int i = 1; i <= ids.size(); i++) {
preparedStatement.setInt(i, ids.get(i - 1));
}
return preparedStatement.executeQuery();
}
核心步骤:
- 动态生成
?,?,?
占位符字符串 - 拼接到 SQL 语句中
- 循环设置参数值
测试验证:
@Test
void whenPopulatingINClauseWithStringBuilder_thenIsSuccess() throws SQLException {
ResultSet resultSet = PreparedStatementInClause
.populateParamsWithStringBuilder(connection, List.of(1, 2, 3, 4, 55));
Assertions.assertNotNull(resultSet);
resultSet.last();
int size = resultSet.getRow();
Assertions.assertEquals(5, size);
}
3.2. 基于 Stream API 的 IN 子句
更优雅的方案:使用 Java 8 Stream API。将列表值映射为占位符,再通过 String.format()
拼接:
ResultSet populateParamsWithStream(Connection connection, List<Integer> ids) throws SQLException {
var sql = String.format("select * from customer where id IN (%s)", ids.stream()
.map(v -> "?")
.collect(Collectors.joining(", ")));
PreparedStatement preparedStatement = connection.prepareStatement(sql);
for (int i = 1; i <= ids.size(); i++) {
preparedStatement.setInt(i, ids.get(i - 1));
}
return preparedStatement.executeQuery();
}
关键点:
ids.stream().map(v -> "?")
生成占位符流Collectors.joining(", ")
拼接为逗号分隔字符串- 后续参数设置与方案1相同
测试用例:
@Test
void whenPopulatingINClauseWithStream_thenIsSuccess() throws SQLException {
ResultSet resultSet = PreparedStatementInClause
.populateParamsWithStream(connection, List.of(1, 2, 3, 4, 55));
Assertions.assertNotNull(resultSet);
resultSet.last();
int size = resultSet.getRow();
Assertions.assertEquals(5, size);
}
3.3. 基于 setArray() 的 IN 子句
数据库原生方案:使用 setArray()
方法。需修改 SQL 结构,通过子查询处理数组参数:
ResultSet populateParamsWithArray(Connection connection, List<Integer> ids) throws SQLException {
String sql = "SELECT * FROM customer where id IN (select * from table(x int = ?))";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
Array array = preparedStatement.getConnection()
.createArrayOf("int", ids.toArray());
preparedStatement.setArray(1, array);
return preparedStatement.executeQuery();
}
⚠️ 注意事项:
- SQL 结构变为子查询形式
- 需将
List
转为java.sql.Array
并指定类型 - 数据库需支持数组类型(如 Oracle 的
TABLE()
函数)
测试验证:
@Test
void whenPopulatingINClauseWithArray_thenIsSuccess() throws SQLException {
ResultSet resultSet = PreparedStatementInClause
.populateParamsWithArray(connection, List.of(1, 2, 3, 4, 55));
Assertions.assertNotNull(resultSet);
resultSet.last();
int size = resultSet.getRow();
Assertions.assertEquals(5, size);
}
4. 总结
本文对比了三种在 PreparedStatement 中使用 IN 子句的实现方式:
- ✅ StringBuilder 方案:简单直接,适合小型列表
- ✅ Stream API 方案:代码简洁,可读性强,推荐首选
- ⚠️ setArray 方案:依赖数据库特性,移植性较差
最终推荐使用 Stream API 方案:它既保持了代码的简洁性,又具备良好的数据库兼容性。所有示例代码可在 GitHub 获取。