这是一个非常重要且常见的问题,因为它直接关系到SQL 注入的安全风险。

核心问题:为什么 IN 子句不能用单个占位符?
在 JDBC 中,一个标准的占位符()代表一个单一的值,当 SQL 语句被预编译时,JDBC 驱动程序会将每个 替换为一个参数,并在执行时单独绑定这个参数的值。
对于 WHERE id = ?,驱动程序知道 应该被替换成一个整数或字符串。
IN 子句的语法是 WHERE column IN (value1, value2, value3),这里的 (value1, value2, value3) 是一个值的列表,而不是单个值,如果你尝试这样写:
// 错误且危险的做法! String sql = "SELECT * FROM users WHERE id IN (?)"; List<Integer> ids = Arrays.asList(1, 2, 3); pstmt = conn.prepareStatement(sql); pstmt.setObject(1, ids); // 尝试将整个列表作为单个参数
这会导致 SQL 语句变成 SELECT * FROM users WHERE id IN ([Ljava.lang.Integer;@15db9742),这显然是错误的,数据库无法解析 [Ljava.lang.Integer;@... 这个 Java 对象的地址。

正确的解决方案:动态生成占位符
为了安全地使用 IN 子句,我们需要根据传入的列表大小动态地生成 SQL 语句,并相应地设置占位符数量。
如果传入的 ID 列表是 [1, 2, 3],我们应该生成这样的 SQL:
SELECT * FROM users WHERE id IN (?, ?, ?)
我们遍历这个列表,为每个 设置一个值。
实现方法与代码示例
下面提供几种主流的实现方法,从手动构建到使用第三方库。

手动构建(最基础,需要自己处理所有细节)
这是最直接的方法,需要你手动创建 SQL 字符串并循环设置参数。
优点:
- 不依赖任何外部库。
- 容易理解底层原理。
缺点:
- 代码冗长。
- 容易出错,尤其是在处理字符串拼接时。
示例代码:
import java.sql.*;
import java.util.Arrays;
import java.util.List;
public class InClauseExample {
private static final String DB_URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASS = "your_password";
public static void main(String[] args) {
List<Integer> userIds = Arrays.asList(1, 2, 3, 5);
// 1. 动态生成 IN 子句的占位符部分 (?,?,?,?)
String inClausePlaceholders = String.join(",", Collections.nCopied(userIds.size(), "?"));
// 2. 构建完整的 SQL 语句
String sql = "SELECT id, name, email FROM users WHERE id IN (" + inClausePlaceholders + ")";
System.out.println("Generated SQL: " + sql);
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 3. 循环设置参数
for (int i = 0; i < userIds.size(); i++) {
pstmt.setInt(i + 1, userIds.get(i)); // PreparedStatement 索引从 1 开始
}
// 4. 执行查询
ResultSet rs = pstmt.executeQuery();
System.out.println("\nQuery Results:");
while (rs.next()) {
System.out.printf("ID: %d, Name: %s, Email: %s%n",
rs.getInt("id"),
rs.getString("name"),
rs.getString("email"));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
注意:如果列表为空,生成的 SQL 会是 ... WHERE id IN (),这在 SQL 中是无效的,你需要处理这种情况,例如将 IN 子句改为 WHERE 1=0 或 WHERE id = -1 (确保没有记录匹配)。
使用 JPA / Hibernate(推荐)
如果你使用的是 JPA (如 Hibernate),它会自动为你处理这些细节,你只需要使用 IN 关键字即可。
优点:
- 代码非常简洁,可读性高。
- 自动处理参数绑定和 SQL 生成。
- 防止 SQL 注入。
示例代码 (JPQL):
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import java.util.List;
public class UserRepository {
@PersistenceContext
private EntityManager entityManager;
public List<User> findUsersByIds(List<Long> userIds) {
if (userIds == null || userIds.isEmpty()) {
return List.of(); // 返回空列表
}
// JPA/Hibernate 会自动处理 IN 子句和参数绑定
String jpql = "SELECT u FROM User u WHERE u.id IN :ids";
TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
query.setParameter("ids", userIds); // 直接传入集合
return query.getResultList();
}
}
注意:对于非常大的列表(例如超过 1000 个元素),一些数据库(如 Oracle)可能会因为 IN 子句过长而报错,JPA 提供了 IN 子句分页的解决方案,例如使用 Hibernate.IN_CLAUSE_PARAMETER_PADDING。
使用第三方库(如 Apache Commons Lang / Spring)
如果你没有使用 JPA,但又不想写繁琐的代码,可以使用工具库来辅助生成占位符字符串。
示例代码 (使用 Apache Commons Lang 的 StringUtils.repeat):
import org.apache.commons.lang3.StringUtils;
import java.sql.*;
import java.util.Arrays;
import java.util.List;
public class CommonsLangInClauseExample {
// ... (DB_URL, USER, PASS 同上)
public static void main(String[] args) {
List<Integer> userIds = Arrays.asList(1, 2, 3, 5);
// 使用 StringUtils.repeat 生成 "?,?,?,?"
String inClausePlaceholders = StringUtils.repeat("?", ",", userIds.size());
String sql = "SELECT id, name, email FROM users WHERE id IN (" + inClausePlaceholders + ")";
System.out.println("Generated SQL: " + sql);
// ... (后面的参数设置和执行逻辑与手动构建方法相同)
}
}
示例代码 (使用 Spring JDBCTemplate - 更高级的封装):
Spring 的 JdbcTemplate 让操作更简单,但它本身不直接支持 IN 子句的便捷方法,通常你仍然需要手动构建占位符,但 JdbcTemplate 的 update 或 query 方法会让参数绑定更清晰。
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import javax.sql.DataSource;
import java.util.List;
public class SpringJdbcTemplateExample {
private final JdbcTemplate jdbcTemplate;
public SpringJdbcTemplateExample(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public List<User> findUsersByIds(List<Integer> userIds) {
if (userIds == null || userIds.isEmpty()) {
return List.of();
}
// 动态构建占位符
String placeholders = String.join(",", Collections.nCopied(userIds.size(), "?"));
String sql = "SELECT id, name, email FROM users WHERE id IN (" + placeholders + ")";
// 使用 RowMapper 映射结果
RowMapper<User> rowMapper = (rs, rowNum) -> new User(rs.getInt("id"), rs.getString("name"), rs.getString("email"));
// 使用可变参数列表传递参数
// 注意:Java 8+ 可以使用 stream: userIds.stream().toArray(Integer[]::new)
return jdbcTemplate.query(sql, userIds.toArray(), rowMapper);
}
}
总结与最佳实践
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 手动构建 | 无依赖,原理清晰 | 代码冗长,易出错 | 简单项目,学习 JDBC 原理 |
| JPA/Hibernate | 代码最简洁,最安全,自动处理一切 | 需要 JPA 环境,有一定学习曲线 | 强烈推荐,任何使用 JPA/Hibernate 的项目 |
| 第三方库 | 简化占位符生成 | 增加项目依赖 | 不想用 JPA,但希望代码比纯手动更优雅 |
| Spring JDBCTemplate | 简化 JDBC 操作,类型安全 | 仍需手动构建 IN 子句 |
使用 Spring 生态,但不想引入 JPA 的项目 |
核心安全要点:
- 永远不要使用字符串拼接来构建
IN子句的值,sql = "SELECT ... WHERE id IN (" + ids + ")",这是 SQL 注入的温床。 - 始终使用
PreparedStatement,这是防止 SQL 注入的黄金法则。 - 动态生成占位符,然后循环设置参数,是处理
IN子句的正确且安全的方式。
对于现代 Java 项目,如果使用 ORM 框架(如 Hibernate),优先使用框架提供的功能,如果使用原生 JDBC,推荐使用 Spring JdbcTemplate 或类似的工具来简化代码,并严格遵循动态生成占位符的原则。
