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();
}

核心步骤:

  1. 动态生成 ?,?,? 占位符字符串
  2. 拼接到 SQL 语句中
  3. 循环设置参数值

测试验证:

@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();
}

⚠️ 注意事项:

  1. SQL 结构变为子查询形式
  2. 需将 List 转为 java.sql.Array 并指定类型
  3. 数据库需支持数组类型(如 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 获取。


原始标题:JDBC PreparedStatement SQL IN clause | Baeldung