1. 简介
在 SQL 语句中,我们常用 IN
操作符来判断某个表达式是否匹配列表中的任意一个值。相比写一堆 OR
条件,IN
的写法更简洁、可读性更强。
本文将介绍如何在 Spring 的 JdbcTemplate
查询中,向 SQL 的 IN
子句传入一个值列表。这是日常开发中非常常见的需求,比如根据用户 ID 列表批量查询用户信息,但处理不当容易踩坑,尤其当列表很长时。
2. 向 IN 子句传递 List 参数
IN
子句的核心作用是在 WHERE
条件中匹配多个值。例如,查询 ID 在指定列表中的所有员工:
SELECT * FROM EMPLOYEE WHERE id IN (1, 2, 3)
实际开发中,这个列表的长度通常是动态的。因此,我们需要一种机制来动态生成占位符,并安全地绑定参数,避免 SQL 注入。
2.1 使用 JdbcTemplate
JdbcTemplate
支持使用 ?
作为占位符。关键点是:占位符的数量必须与列表长度一致。
我们可以通过 String.join
和 Collections.nCopies
动态生成逗号分隔的 ?
占位符:
List<Employee> getEmployeesFromIdList(List<Integer> ids) {
String inSql = String.join(",", Collections.nCopies(ids.size(), "?"));
List<Employee> employees = jdbcTemplate.query(
String.format("SELECT * FROM EMPLOYEE WHERE id IN (%s)", inSql),
ids.toArray(),
(rs, rowNum) -> new Employee(rs.getInt("id"), rs.getString("first_name"),
rs.getString("last_name")));
return employees;
}
✅ 关键逻辑解析:
Collections.nCopies(ids.size(), "?")
:生成与ids
长度相同的?
列表String.join(",", ...)
:拼接成?,?,?
这样的字符串String.format
:将拼接后的占位符插入 SQLids.toArray()
:将列表转为数组,供JdbcTemplate
按顺序绑定参数
例如,当 ids = [1, 2, 3]
时,最终执行的 SQL 是:
SELECT * FROM EMPLOYEE WHERE id IN (?,?,?)
⚠️ 注意: 如果 ids
为空列表,inSql
会变成空字符串,导致 SQL 语法错误(IN ()
不合法)。生产环境需额外处理空列表场景。
2.2 使用 NamedParameterJdbcTemplate
如果你觉得手动拼占位符太原始,NamedParameterJdbcTemplate
提供了更优雅的解决方案 —— 支持直接传入集合类型的命名参数。
示例代码:
List<Employee> getEmployeesFromIdListNamed(List<Integer> ids) {
SqlParameterSource parameters = new MapSqlParameterSource("ids", ids);
List<Employee> employees = namedJdbcTemplate.query(
"SELECT * FROM EMPLOYEE WHERE id IN (:ids)",
parameters,
(rs, rowNum) -> new Employee(rs.getInt("id"), rs.getString("first_name"),
rs.getString("last_name")));
return employees;
}
✅ 优势:
- 无需手动拼接
?
,代码更简洁 - 使用
:ids
命名参数,SQL 可读性高 MapSqlParameterSource
自动处理集合展开为多个?
- 底层仍由
JdbcTemplate
执行,性能无损
📌 原理: NamedParameterJdbcTemplate
会在执行前将 :ids
替换为对应数量的 ?
,并将集合参数拆解绑定。
3. 处理超长列表的场景
上面的方法在列表较短时表现良好,但当列表过长时会遇到数据库限制:
❌ Oracle:IN
子句最多支持 1000 个字面量
❌ 某些数据库:占位符过多影响 SQL 解析性能
❌ 极端情况:SQL 长度超出数据库限制
此时,简单粗暴拼 ?
就不适用了。一个通用且高效的解决方案是:使用临时表(Temporary Table)。
✅ 推荐方案:创建临时表存储列表数据
以 H2 数据库为例,我们将 ID 列表先插入临时表,再通过子查询关联:
List<Employee> getEmployeesFromLargeIdList(List<Integer> ids) {
// 创建临时表(如果不存在)
jdbcTemplate.execute("CREATE TEMPORARY TABLE IF NOT EXISTS employee_tmp (id INT NOT NULL)");
// 批量插入 ID 列表
List<Object[]> employeeIds = new ArrayList<>();
for (Integer id : ids) {
employeeIds.add(new Object[] { id });
}
jdbcTemplate.batchUpdate("INSERT INTO employee_tmp VALUES(?)", employeeIds);
// 查询主表,IN 子句从临时表取值
List<Employee> employees = jdbcTemplate.query(
"SELECT * FROM EMPLOYEE WHERE id IN (SELECT id FROM employee_tmp)",
(rs, rowNum) -> new Employee(rs.getInt("id"), rs.getString("first_name"),
rs.getString("last_name")));
// 清理临时表,避免残留数据影响后续调用
jdbcTemplate.update("DELETE FROM employee_tmp");
return employees;
}
✅ 该方案优势:
- 不受
IN
子句长度限制 - 批量插入性能高(
batchUpdate
) - SQL 简洁,逻辑清晰
- 适用于大多数支持临时表的关系型数据库
⚠️ 注意事项:
- 临时表生命周期通常为会话级,但不同数据库行为略有差异
- 建议每次使用后清空数据,避免脏数据
- 若并发调用,可考虑使用唯一会话 ID 或临时表名做隔离
📌 替代方案参考:
- 分批查询:将大列表拆成多个 ≤1000 的小批次,循环查询后合并结果
- 内存过滤:全量查出再用 Java Stream 过滤(仅适用于数据量极小场景)
- 使用 EXISTS:某些场景可用
EXISTS
+ 临时表替代IN
4. 总结
本文系统介绍了在 Spring JdbcTemplate
中处理 IN
子句传参的几种方式:
方案 | 适用场景 | 推荐指数 |
---|---|---|
JdbcTemplate + ? 拼接 |
列表较短(< 1000) | ⭐⭐⭐⭐ |
NamedParameterJdbcTemplate |
偏好命名参数,代码整洁 | ⭐⭐⭐⭐⭐ |
临时表方案 | 列表超长(> 1000)或性能敏感 | ⭐⭐⭐⭐⭐ |
📌 核心建议:
- 日常开发优先使用
NamedParameterJdbcTemplate
,简洁且不易出错 - 遇到长列表果断切临时表方案,别在
IN
上硬扛 - 记得处理空列表边界情况,避免生产事故
所有示例代码已上传至 GitHub:https://github.com/techblog-tutorials/spring-jdbc-in-clause