资讯详情

MyBatis批量查询:Collection参数、@Param与foreach全解析

📅 2026/10/21 9:19:46 | 华诺云谱 👁 阅读
MyBatis批量查询:Collection参数、@Param与foreach全解析
上个月我在一个老项目里改批量查询逻辑用 MyBatis-Plus 自带的 selectBatchIds 用得挺顺传入一个ListLong就能把一批订单数据捞回来。后来另一个模块要按一批订单号做状态查询我照着 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 里预置了一个批量主键查询方法方法签名大致是这样的ListT selectBatchIds(Param(Constants.COLL) Collection? extends Serializable idList);这里有一个很容易被忽略的细节框架在参数上主动加了Param(Constants.COLL)注解。Constants.COLL的值就是字符串coll所以框架内置 SQL 里用的才是collectioncoll。这说明它并不是靠什么魔法而是提前给 Collection 参数固定了一个别名。日常使用很简单ListLong orderIds orderService.getTodayOrderIds(); ListOrder orders orderMapper.selectBatchIds(orderIds);底层生成的 SQL 大致是SELECT id, order_no, status FROM order WHERE id IN (?,?,?)注意这个 IN 条件不是写死的而是 MyBatis 在运行时根据传入的 List 大小动态拼出来的。selectBatchIds 的语义很清晰你给我一堆主键我一次性把这批数据查出来。但问题也藏在这里——框架把复杂参数解析都封装好了很多人在自己动手写类似方法时才会第一次直面 Collection 类型参数传值的坑。1.2 自定义批量查询踩坑现场我在项目里写的自定义方法一开始是这样定义的ListOrder electBatchIds(ListLong orderIds);对应的 XML 里我这样写select idelectBatchIds resultTypecom.example.order.Order SELECT id, order_no, status FROM order WHERE id IN foreach collectionorderIds itemid open( separator, close) #{id} /foreach /select看着没什么问题是吧实际跑起来直接报Parameter orderIds not found。原因很简单MyBatis 解析参数时不会因为你 Java 方法里把变量名叫 orderIds就以 orderIds 作为 XML 的引用 key。我后来做了两个改动都能解决第一种在接口方法参数上加注解ListOrder electBatchIds(Param(ids) ListLong ids);XML 里把collectionorderIds改成collectionids。第二种不改接口方法把 XML 里的collectionorderIds改成collectionlist因为 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 有一个默认规则。以ListLong ids这个单参数为例实际生成的参数 Map 大致是{ collection - ids 对象, list - ids 对象, param1 - ids 对象 }所以你说collectionlist没问题说collectioncollection也可以。数组参数则是另一个规则Long[] ids生成的 key 是array和param1XML 里该写collectionarray。常被人忽略的一点如果你只有一个 Collection 参数且不加 Param方法是能跑的问题只在于 key 名不是你以为的那个。很多人因为平时习惯了ListUser list mapper.selectByIds(ids)这种写法就默认 XML 里也可以直接用方法参数名 ids结果被 MyBatis 的默认规则狠狠教育了一次。2.2 Param 注解如何改变规则在方法参数上加了 Param 之后参数 Map 的 key 就会变成注解里指定的值。比如ListOrder electBatchIds(Param(ids) ListLong ids);生成的 Map 里就会多出一个ids - ids 对象同时默认的list、collection也依然存在。这解释了为什么加 Param 是最稳妥的做法你不需要去猜 MyBatis 的默认规则直接用自己定义的名字就行。那是不是说只要接口方法上有 ParamXML 里写collectionlist也能用严格说单 List 参数时确实还能取到值因为 list 这个默认 key 还在。但我不推荐这样混着用原因有两个。第一可读性差。别人看接口方法时看到 Param(ids)再看 XML 里写的却是 list会一头雾水。第二多参数场景下默认 key 很可能互相覆盖。比如方法里有两个 List 参数都叫 collection 和 list后一个会把前一个覆盖掉结果完全不可预期。所以凡是 Collection 类型参数建议一律用 Param 固定别名别把希望寄托在默认规则上。2.3 方法参数名都去哪了还有一个常见疑问Java 方法里明明叫 idsMyBatis 为什么就是不认这要从参数名的获取方式说起。Java 编译成字节码时如果不开启-parameters编译参数方法参数名在源码里是一个回事在运行时拿到的可能是arg0、arg1这种占位名。而且不同版本、不同构建配置下名字也不一样。MyBatis 为了稳定干脆不依赖编译期参数名主要用位置索引加注解来定位参数。那意思就是如果我不加 ParamXML 里只能用param1、param2这种位置名或者list、collection这种类型名。写业务代码时谁会愿意用 param1 这种鬼名字所以最佳实践很明确只要 XML 里要用到参数名就给对应的 Java 参数加 Param。3. XML 里 foreach 的写法和参数引用3.1 foreach 标签属性逐个说批量查询的核心就是把 Collection 遍历成 IN 条件这一操作在 MyBatis 里由 foreach 标签完成。下面是一个典型的写法select idelectBatchIds resultTypecom.example.order.Order SELECT id, order_no, status FROM order WHERE status 1 if testids ! null and ids.size() 0 AND id IN foreach collectionids itemid 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 testids ! null and ids.size() 0做保护或者干脆在 Service 层先判断集合为空直接返回空列表连 SQL 都不用发。想要更严谨一点可以用 choose 标签把查询分支包起来choose when testids ! null and ids.size() 0 SELECT id, order_no, status FROM order WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /when otherwise SELECT id, order_no, status FROM order WHERE 1 0 /otherwise /choose除了空集合还有一批数据量过大带来的隐患。比如一次传入上万条 IDIN 条件会变得非常长这会带来几个问题SQL 文本可能超过数据库或驱动允许的最大长度某些数据库对 IN 列表项数有硬性上限长 IN 列表可能导致执行计划不理想索引没走成。我的做法是大集合按每批 500 到 1000 个拆分分批查询最后在 Java 层合并结果。虽然多发了几个 SQL但整体可控、可排查比一条超大 SQL 挂在线上半天强。4. 多参数场景下的 Collection 传值4.1 多个参数混用时必须用 Param实际业务里批量查询往往不只是传一个 ID 集合还要带筛选条件。比如批量更新订单状态int batchUpdateStatus(Param(ids) ListLong ids, Param(status) Integer status);对应 XMLupdate idbatchUpdateStatus UPDATE order SET status #{status} WHERE id IN foreach collectionids itemid 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 里用 collectionlist 代替 ids然后再把 id 元素取出。两种办法都很丑而且很容易写错。为什么多参数场景下默认规则更不靠谱因为一旦有两个 List 参数它们的 collection、list 默认 key 会互相覆盖取到的可能是第二个 List而不是你期望的第一个。加 Param 是唯一清晰可控的方案。4.2 Service 层传值时容易忽视的细节Collection 参数传值的问题不只在 Mapper 接口和 XML 之间Service 层到底传了什么也很关键。先看空集合和 null 的区别。foreach 遍历 null 会直接抛异常空集合则可能拼出非法 SQL。我现在的习惯是凡是调用 electBatchIds 这类方法Service 层先做一次防御public ListOrder queryOrders(ListLong ids) { if (ids null || ids.isEmpty()) { return Collections.emptyList(); } return orderMapper.electBatchIds(ids); }再看元素类型。foreach 里的 item 类型就是集合元素类型如果我误传了一个ListOrder那#{id}实际绑定的会是整个 Order 对象查询条件就变成WHERE id IN (com.example.order.Orderxxx)运行时数据直接出错。所以传参时一定要保证集合泛型跟 SQL 语义一致。如果用 Set 代替 List 也可以MyBatis-Plus 的 selectBatchIds 参数定义就是Collection? extends Serializable说明 Collection 的各种子类基本都能用。不过要注意 Set 不保证有序如果业务上要求查询结果按传入顺序展示应该在 Java 层用 LinkedHashMap 或者手动排序不要指望 SQL 的 IN 顺序能保持。还有个容易被忽略的点当前端通过 Controller 传ListString ids时接口层接收没问题真正到了 MyBatis 这一层规则没有任何变化依然是上面的 Collection 解析逻辑。换句话说传值问题贯穿整个调用链但最终的关键点始终在 Mapper 方法参数和 XML 的 collection 属性上。5. 常见报错与排查记录5.1 一张问题速查表下面这些是我在处理 Collection 参数问题时遇到的典型场景整理成表格方便对照报错或现象原因解决方案Parameter ids not found. Available parameters are [collection, list]接口方法没加 ParamXML 直接写了 ids加 Param(ids)或者把 collection 写成 listThere is no getter for property named id in class java.lang.Stringforeach 的 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 转成了ListLong后续走到 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 两行日志比对着源码猜快得多。希望这篇总结能帮你少走两次弯路。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑