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.joinCollections.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:将拼接后的占位符插入 SQL
  • ids.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. 处理超长列表的场景

上面的方法在列表较短时表现良好,但当列表过长时会遇到数据库限制:

OracleIN 子句最多支持 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


原始标题:Using a List of Values in a JdbcTemplate IN Clause