☰
MyBatis批量查询:Collection参数、@Param与foreach全解析
2026/10/21 9:19:41 网站建设 项目流程

上个月我在一个老项目里改批量查询逻辑,用 MyBatis-Plus 自带的 selectBatchIds 用得挺顺,传入一个List<Long>就能把一批订单数据捞回来。后来另一个模块要按一批订单号做状态查询,我照着 selectBatchIds 的样子自己写了一个 electBatchIds 方法,结果第一次联调就报错:Parameter 'ids' not found. Available parameters are [collection, list]。顺着这个报错我把 MyBatis 底层对 Collection 类型参数的解析规则翻了一遍,才发现这里面的门道比想象中多得多。这篇就把 electBatchIds 这类批量查询方法里,Collection 类型参数到底怎么传、底层怎么命名、XML 里怎么取值的关键点拆开讲清楚,适合刚接触 MyBatis 或 MyBatis-Plus 的开发者,也适合那些已经踩过坑但没深究原因的同行。

1. 从 selectBatchIds 引出 Collection 传值

1.1 selectBatchIds 方法在框架里做了什么

MyBatis-Plus 的 BaseMapper 里预置了一个批量主键查询方法,方法签名大致是这样的:

List<T> selectBatchIds(@Param(Constants.COLL) Collection<? extends Serializable> idList);

这里有一个很容易被忽略的细节:框架在参数上主动加了@Param(Constants.COLL)注解。Constants.COLL的值就是字符串"coll",所以框架内置 SQL 里用的才是collection="coll"。这说明它并不是靠什么魔法,而是提前给 Collection 参数固定了一个别名。

日常使用很简单:

List<Long> orderIds = orderService.getTodayOrderIds(); List<Order> orders = orderMapper.selectBatchIds(orderIds);

底层生成的 SQL 大致是:

SELECT id, order_no, status FROM order WHERE id IN (?,?,?)

注意这个 IN 条件不是写死的,而是 MyBatis 在运行时根据传入的 List 大小动态拼出来的。selectBatchIds 的语义很清晰:你给我一堆主键,我一次性把这批数据查出来。但问题也藏在这里——框架把复杂参数解析都封装好了,很多人在自己动手写类似方法时,才会第一次直面 Collection 类型参数传值的坑。

1.2 自定义批量查询踩坑现场

我在项目里写的自定义方法一开始是这样定义的:

List<Order> electBatchIds(List<Long> orderIds);

对应的 XML 里我这样写:

<select id="electBatchIds" resultType="com.example.order.Order"> SELECT id, order_no, status FROM order WHERE id IN <foreach collection="orderIds" item="id" open="(" separator="," close=")"> #{id} </foreach> </select>

看着没什么问题是吧?实际跑起来直接报Parameter 'orderIds' not found。原因很简单:MyBatis 解析参数时,不会因为你 Java 方法里把变量名叫 orderIds,就以 orderIds 作为 XML 的引用 key。我后来做了两个改动,都能解决:

第一种,在接口方法参数上加注解:

List<Order> electBatchIds(@Param("ids") List<Long> ids);

XML 里把collection="orderIds"改成collection="ids"。

第二种,不改接口方法,把 XML 里的collection="orderIds"改成collection="list",因为 MyBatis 对单个 List 参数默认绑定了 list 这个 key。

这两种做法的背后,就是 MyBatis 的 Collection 类型参数解析机制。

2. MyBatis 如何解析 Collection 类型参数

2.1 参数解析器与参数 Map

任何一个 Mapper 接口方法被调用时,最终都会通过 MapperMethod 进入 Executor,而 XML 或注解 SQL 里引用的参数名,全部来自一个内部生成的参数 Map。这个 Map 由参数解析器根据方法入参构建。

用生活场景来类比:MyBatis 会把你的方法入参打包成一个礼盒,礼盒里的每个物品都贴了标签。XML 里的#{xxx}和 foreach 的collection属性,就是根据这个标签去礼盒里找对应的东西。你不知道标签是什么,自然取不到值。

针对 Collection 类型的单参数,MyBatis 有一个默认规则。以List<Long> ids这个单参数为例,实际生成的参数 Map 大致是:

{ "collection" -> ids 对象, "list" -> ids 对象, "param1" -> ids 对象 }

所以你说collection="list"没问题,说collection="collection"也可以。数组参数则是另一个规则,Long[] ids生成的 key 是array和param1,XML 里该写collection="array"。

常被人忽略的一点:如果你只有一个 Collection 参数且不加 @Param,方法是能跑的,问题只在于 key 名不是你以为的那个。很多人因为平时习惯了List<User> list = mapper.selectByIds(ids)这种写法,就默认 XML 里也可以直接用方法参数名 ids,结果被 MyBatis 的默认规则狠狠教育了一次。

2.2 @Param 注解如何改变规则

在方法参数上加了 @Param 之后,参数 Map 的 key 就会变成注解里指定的值。比如:

List<Order> electBatchIds(@Param("ids") List<Long> ids);

生成的 Map 里就会多出一个"ids" -> ids 对象,同时默认的"list"、"collection"也依然存在。这解释了为什么加 @Param 是最稳妥的做法:你不需要去猜 MyBatis 的默认规则,直接用自己定义的名字就行。

那是不是说,只要接口方法上有 @Param,XML 里写collection="list"也能用?严格说,单 List 参数时确实还能取到值,因为 list 这个默认 key 还在。但我不推荐这样混着用,原因有两个。

第一,可读性差。别人看接口方法时看到 @Param("ids"),再看 XML 里写的却是 list,会一头雾水。第二,多参数场景下默认 key 很可能互相覆盖。比如方法里有两个 List 参数,都叫 collection 和 list,后一个会把前一个覆盖掉,结果完全不可预期。

所以,凡是 Collection 类型参数,建议一律用 @Param 固定别名,别把希望寄托在默认规则上。

2.3 方法参数名都去哪了

还有一个常见疑问:Java 方法里明明叫 ids,MyBatis 为什么就是不认?这要从参数名的获取方式说起。Java 编译成字节码时,如果不开启-parameters编译参数,方法参数名在源码里是一个回事,在运行时拿到的可能是arg0、arg1这种占位名。而且不同版本、不同构建配置下名字也不一样。

MyBatis 为了稳定,干脆不依赖编译期参数名,主要用位置索引加注解来定位参数。那意思就是:如果我不加 @Param,XML 里只能用param1、param2这种位置名,或者list、collection这种类型名。写业务代码时谁会愿意用 param1 这种鬼名字?所以最佳实践很明确:只要 XML 里要用到参数名,就给对应的 Java 参数加 @Param。

3. XML 里 foreach 的写法和参数引用

3.1 foreach 标签属性逐个说

批量查询的核心就是把 Collection 遍历成 IN 条件,这一操作在 MyBatis 里由 foreach 标签完成。下面是一个典型的写法:

<select id="electBatchIds" resultType="com.example.order.Order"> SELECT id, order_no, status FROM order WHERE status = 1 <if test="ids != null and ids.size() > 0"> AND id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </if> </select>

各个属性的含义分别是:

  • collection:指定要遍历的 Collection 在参数 Map 中的 key,这里是 @Param 里指定的 ids。
  • item:循环中的变量名,循环体内用#{id}引用当前元素。
  • open:循环结果的前缀,一般是左括号。
  • separator:元素之间的分隔符,一般是逗号。
  • close:循环结果的后缀,一般是右括号。

为什么不能直接写WHERE id IN #{ids}?因为#{ids}是取值操作,它会把这个 List 对象整体交给 JDBC 去处理,而 JDBC 的 PreparedStatement 不支持直接把一个 List 绑定成一个参数。foreach 的作用相当于一个生成器,先把 List 展开成(?, ?, ?)这种占位符序列,再来一个一个绑定值。可以理解为#{}是“取包里的东西”,foreach 是“把包里的东西拆开摆到桌面上”。

3.2 空集合与 IN 条件的边界问题

批量查询最容易出现的另一个问题是:传入的集合为空。如果 ids 是空集合,上面的 foreach 会生成一个IN (),这在 MySQL 里就是语法错误。所以我一般在 SQL 里加if test="ids != null and ids.size() > 0"做保护,或者干脆在 Service 层先判断,集合为空直接返回空列表,连 SQL 都不用发。

想要更严谨一点,可以用 choose 标签把查询分支包起来:

<choose> <when test="ids != null and ids.size() > 0"> SELECT id, order_no, status FROM order WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </when> <otherwise> SELECT id, order_no, status FROM order WHERE 1 = 0 </otherwise> </choose>

除了空集合,还有一批数据量过大带来的隐患。比如一次传入上万条 ID,IN 条件会变得非常长,这会带来几个问题:SQL 文本可能超过数据库或驱动允许的最大长度;某些数据库对 IN 列表项数有硬性上限;长 IN 列表可能导致执行计划不理想,索引没走成。我的做法是大集合按每批 500 到 1000 个拆分,分批查询,最后在 Java 层合并结果。虽然多发了几个 SQL,但整体可控、可排查,比一条超大 SQL 挂在线上半天强。

4. 多参数场景下的 Collection 传值

4.1 多个参数混用时必须用 @Param

实际业务里,批量查询往往不只是传一个 ID 集合,还要带筛选条件。比如批量更新订单状态:

int batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") Integer status);

对应 XML:

<update id="batchUpdateStatus"> UPDATE order SET status = #{status} WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </update>

这里每个参数都加了 @Param,所以 XML 里可以清晰引用 ids 和 status。如果我不加 @Param,就会看到这样的报错:

Parameter 'status' not found. Available parameters are [collection, list, param1, param2]

报错信息已经把可用的 key 列出来了。第一个 List 参数被注册成了 collection 和 list,两个参数又按位置注册成 param1、param2,但业务语义上的 status 根本不存在。这时候要么把 status 改成 param2,要么在 foreach 里用 collection="list" 代替 ids,然后再把 id 元素取出。两种办法都很丑,而且很容易写错。为什么多参数场景下默认规则更不靠谱?因为一旦有两个 List 参数,它们的 collection、list 默认 key 会互相覆盖,取到的可能是第二个 List,而不是你期望的第一个。加 @Param 是唯一清晰可控的方案。

4.2 Service 层传值时容易忽视的细节

Collection 参数传值的问题不只在 Mapper 接口和 XML 之间,Service 层到底传了什么,也很关键。

先看空集合和 null 的区别。foreach 遍历 null 会直接抛异常,空集合则可能拼出非法 SQL。我现在的习惯是,凡是调用 electBatchIds 这类方法,Service 层先做一次防御:

public List<Order> queryOrders(List<Long> ids) { if (ids == null || ids.isEmpty()) { return Collections.emptyList(); } return orderMapper.electBatchIds(ids); }

再看元素类型。foreach 里的 item 类型就是集合元素类型,如果我误传了一个List<Order>,那#{id}实际绑定的会是整个 Order 对象,查询条件就变成WHERE id IN (com.example.order.Order@xxx),运行时数据直接出错。所以传参时一定要保证集合泛型跟 SQL 语义一致。

如果用 Set 代替 List 也可以,MyBatis-Plus 的 selectBatchIds 参数定义就是Collection<? extends Serializable>,说明 Collection 的各种子类基本都能用。不过要注意 Set 不保证有序,如果业务上要求查询结果按传入顺序展示,应该在 Java 层用 LinkedHashMap 或者手动排序,不要指望 SQL 的 IN 顺序能保持。

还有个容易被忽略的点:当前端通过 Controller 传List<String> ids时,接口层接收没问题,真正到了 MyBatis 这一层,规则没有任何变化,依然是上面的 Collection 解析逻辑。换句话说,传值问题贯穿整个调用链,但最终的关键点始终在 Mapper 方法参数和 XML 的 collection 属性上。

5. 常见报错与排查记录

5.1 一张问题速查表

下面这些是我在处理 Collection 参数问题时遇到的典型场景,整理成表格方便对照:

报错或现象原因解决方案
Parameter 'ids' not found. Available parameters are [collection, list]接口方法没加 @Param,XML 直接写了 ids加 @Param("ids"),或者把 collection 写成 list
There is no getter for property named 'id' in 'class java.lang.String'foreach 的 item 被当成对象,却用了#{item.id}item 是基本类型时直接用#{item}
SQLSyntaxErrorException: ... IN ()传入了空集合,foreach 生成非法 IN调用前判空,或 SQL 里加 if/choose 判断
Invalid bound statement (not found)Mapper 接口方法与 XML 的 namespace/id 对不上检查 XML namespace 和 select 的 id
两个 List 参数都未加 @Param 导致取错集合默认 collection/list key 被后一个参数覆盖每个参数显式加 @Param
大集合批量查询 DB 响应很慢IN 列表过长,执行计划或 DB 参数上限出问题每批 500 到 1000 个拆分查询

这张表不是万能药,但基本覆盖了日常开发里最常遇到的几个类型。自己排查时,先看报错信息里列出 available parameters,再对照接口方法定义,基本就能定位到是缺了 @Param,还是 XML 里的 collection 属性写错了。

5.2 通过 SQL 日志定位参数解析过程

排查这种问题最直接的手段,就是打开 MyBatis 的 SQL 日志。在 Spring Boot 项目里,我一般会在配置里临时打开:

mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

跑一次单元测试或调试请求,重点看两行日志:

==> Preparing: SELECT id, order_no, status FROM order WHERE id IN ( ? , ? , ? ) ==> Parameters: 1001(Long), 1002(Long), 1003(Long)

Preparing 那行会告诉我们 foreach 有没有把 IN 条件正确展开,Parameters 那行会告诉我们参数有没有按正确顺序和类型绑定。如果 Preparing 里只出现一个问号,说明参数没有被展开;如果 Parameters 里出现了异常对象,说明 item 类型传错了。这个方法比对着源码猜快得多,尤其是项目里引入的封装较多时。

我处理过一个很典型的场景:批量状态更新方法一直报 status 找不到,看准备语句那行发现 UPDATE 语句压根没带 status 的占位符,回头查 XML 才发现 foreach 的 collection 写成了业务对象的属性路径,MyBatis 在解析时就把它当对象属性取,结果当然找不到。把 collection 改成 @Param 指定的 ids 后,问题立即消失。

5.3 三种容易误判的情况

有一些情况比较容易误判,单独列一下。

第一种是“看起来是参数问题,实际是 SQL 映射问题”。比如报错显示Invalid bound statement (not found),不少人会去改参数注解,其实这是 Mapper 接口和 XML 的映射没对上。排查时先确认 XML 文件的 namespace 和接口全限定名一致,select 或 update 的 id 与接口方法名一致。

第二种是“分页插件对 Collection 参数的影响”。如果项目里引入了分页插件,要留意它是直接改写 SQL 的,不是它改变了参数解析,而是它会在你原本的 SQL 外面包一层。如果基础 SQL 里参数本身解析不对,分页插件帮不了忙,日志里会先看到原始 SQL 出错。遇到这种情况,先关掉分页插件跑一次,往往能快速区分是参数问题还是分页 SQL 改写问题。

第三种是“Controller 层用 @RequestBody 接收 List 后,以为传给 Mapper 就完事了”。其实 Controller 只是把 HTTP 请求体里的 JSON 转成了List<Long>,后续走到 Mapper 之前没有任何 MyBatis 干预。到了 Mapper 方法上,参数解析仍然按 @Param、list、collection 那套规则走。所以不要把前端传参成功误当成 Mapper 层也成功。

6. 实际项目中的工程化建议

除了上面的技术细节,我在项目里还沉淀了几个固定习惯。写 Mapper 接口时,所有参数都加 @Param,哪怕只有一个参数。这样做的好处是 XML 里的引用规则完全自解释,不再依赖 MyBatis 默认的 list、collection 命名,后续别人接手代码时也不用去猜。再者,所有批量方法在 Service 层入口做空集合判断,这几乎是零成本的事,但能避免大部分 SQL 拼接问题。

SQL 里我尽量不写裸的IN,而是配合<if>或<choose>做保护。批量更新和批量查询如果涉及大集合,就按 500 到 1000 一批拆分,拆分的边界值根据数据库性能和业务量来定。实测下来,单个 IN 控制在 1000 以内,MySQL 走主键索引的表现都比较稳定。

还有一个值得养成的习惯:新写或改造批量查询方法时,先写一个小型单元测试,直接调用 Mapper 方法,分别传 null、空集合、三个元素、一万个元素四组数据。这四种情况基本能把传值问题都暴露出来。很多故障不是逻辑复杂导致的,而是边界条件没在测试阶段覆盖住。

我在实际踩过几次坑之后,现在写 Mapper 接口不管单参数还是多参数,凡是入参里有 Collection,一律加 @Param,并且 XML 里的 collection 只写注解指定的名字,不依赖 MyBatis 默认的 list、collection。还有一个自己用得顺手的排查方法:不确定参数被解析成什么 key 时,先把几个可能的名字都试一遍,然后把 MyBatis SQL 日志打开跑一次,看 Preparing 和 Parameters 两行日志,比对着源码猜快得多。希望这篇总结能帮你少走两次弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询