把单个 ID 改成一组 ID 之后,SQL 还没执行就出现 Parameter 'ids' not found;或者列表为空时数据库报语法错误,甚至查询范围突然变大。排查 foreach,先把 Mapper 入参、XML 中的集合名和最终 SQL 对上。
本文按 MyBatis 3.5.x 的 XML Mapper 说明。示例是带租户条件的只读查询,假设测试表 app_user 已有 id、tenant_id、display_name 字段。包名、字段类型和分页方案要按项目调整,未连接本站生产数据库。
先看失败发生在哪一层
| 现象 | 优先核对 |
|---|---|
| Parameter not found | @Param 名称、collection、对象属性路径 |
| expression evaluated to null | 集合是否允许为 null、实际传入对象 |
| 数据库提示 IN 附近语法错误 | 空列表、括号、分隔符和最终 SQL |
| 查询结果超出预期 | 空集合是否让过滤条件消失、租户条件是否保留 |
| 参数数量或报文过大 | 集合规模、驱动与数据库限制、批次设计 |
先记录 Mapper 方法签名、对应 statement ID,以及失败输入是 null、空列表还是几个元素。不要只截取一行数据库错误,也不要把真实业务参数完整打印到公共日志。
如果错误是 Invalid bound statement,优先检查 XML 是否加载、namespace 和方法 ID 是否一致,foreach 还未必是问题所在;如果 SQL 有结果但实体字段为 null,可参考MyBatis 查询字段为 null。
给集合明确命名,避免依赖隐式参数名
示例 Mapper 如下,放在项目已配置的 Mapper 扫描范围中:
package demo.mapper;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
public interface AppUserMapper {
List<Map<String, Object>> findByIds(
@Param("tenantId") long tenantId,
@Param("ids") List<Long> ids);
}
对应的 XML 使用 collection="ids",租户参数使用 #{tenantId}。名字来自 MyBatis 的 @Param,不是数据库列名,也不是随便写的临时变量。
单个未标注的 List 参数、数组、Map 和包装对象,暴露给 XML 的名字规则并不相同,还可能受参数名保留等配置影响。多参数时更不应假设 collection="list" 总能工作。看异常列出的 available parameters,再对照实际签名;新代码用明确的 @Param 能减少这类歧义。MyBatis Java API说明了参数命名方式。
如果方法接收的是一个查询对象,集合是它的字段,就核对对象属性路径与 getter;不能因为字段在 Java 方法附近出现,就认定它已成为顶层 XML 参数。
空集合要有业务语义
假设这个接口约定“没有 ID 就没有结果”,可以先在服务层返回空结果,也可以在查询中保留明确的空结果分支。下面是完整 mapper 结构示例,查询不会因为列表为空而丢掉 ID 限制:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="demo.mapper.AppUserMapper">
<select id="findByIds" resultType="map">
SELECT id, tenant_id, display_name
FROM app_user
WHERE tenant_id = #{tenantId}
<choose>
<when test="ids != null and ids.size() > 0">
AND id IN
<foreach collection="ids" item="id"
open="(" separator="," close=")">
#{id}
</foreach>
</when>
<otherwise>
AND 1 = 0
</otherwise>
</choose>
ORDER BY id
</select>
</mapper>
这份示例将 null 与空列表统一视为无结果。如果你的接口认为 null 是缺参错误,应在服务入口拒绝它,并保持测试与接口约定一致。不要默默把“未传 ID”和“查询所有 ID”当成同一件事。
nullable="true" 解决的是特定版本下 null 集合的处理方式,不会替你决定“空列表应该查全部还是查不到”。foreach 没有输出内容时,外面的 SQL 仍需成立,过滤范围也要正确。动态 SQL 文档展示了 foreach 的集合与分隔机制。
看 SQL 和绑定参数,不只看 XML
在测试环境为具体 Mapper 开启合适的日志,核对执行 SQL 中有几个 ?,以及绑定参数的数量、顺序和类型。对于两个 ID,预期结构类似:
SELECT id, tenant_id, display_name
FROM app_user
WHERE tenant_id = ? AND id IN (?, ?)
ORDER BY id
这是示意 SQL,不是生产日志。#{id} 是参数绑定;${id} 是文本拼接,不能为了消除占位符或类型报错把二者互换。Mapper XML 参数文档解释了两种表达式的差异。
手工拼接 IN (...) 字符串还会把类型处理、空值与转义问题带到应用层。先明确列表元素到底是 Long、String 还是对象;对象列表通常要绑定 #{item.someField},不能把整个对象当标量 ID。
排查顺序可以固定为:参数名正确,再看集合内容;SQL 形状正确,再看参数类型;两者都对,才看数据库执行层的异常。
大列表、查询和批量写入分开处理
一个 foreach 不会自动变成数据库的高效批处理。超大 IN 列表会增加 SQL 长度和绑定参数数量,具体限制取决于数据库、驱动和语句形状。
需要分批查询时,先限定允许输入的最大规模,再根据环境测试批次。合并结果还要处理重复 ID、稳定排序、分页语义和事务一致性;把列表随意切开并拼接结果,不等于原来的单次分页查询。
批量插入和更新则要单独设计事务、提交与失败回滚。不要为了让分号拼起来的多条 SQL 运行,就直接打开驱动的多语句开关;那不能修复参数名、空集合或批处理语义。
用五组输入验收
至少覆盖 null、空列表、一个 ID、多个 ID、包含重复或不存在 ID 的列表。对每组核对返回结果、SQL 条件和参数绑定;带租户或权限过滤的查询,还要确认这些条件始终保留。
不要仅验证“语法不报错”。一条在空列表时悄悄查询全表的 SQL,比明确报错更容易造成误判。只读查询验证通过后,若要复用到修改语句,应重新检查影响行数和空输入策略。
参考资料
更新记录
- 2026-10-29:初稿;按 MyBatis 官方资料核对,示例仅用于测试库设计与复测,不作为生产实测记录。
MyBatis foreach 查询报错怎么排查
https://java.li/archives/mybatis-foreach-query-error
评论