4.3 foreach:集合展开与批量手术 本节摘要:foreach 把集合拆开缝进 SQL:展开成 IN 括号里的值序列、拼成多值 INSERT、或构造 CASE WHEN 批量更新。本节讲透六个属性、空集合防护与分批上限,收在"foreach 不是批量写入唯一解"的判断上。 动态拼装的最后一节,处理"一个参数是一堆值"的场景。 六个属性一次讲清 属性 | 作用 | 备注 collection | 要遍历的集合在参数里的名字 | 单参数 List 默认名 list;数组是 array;
本节摘要:foreach 把集合拆开缝进 SQL:展开成 IN 括号里的值序列、拼成多值 INSERT、或构造 CASE WHEN 批量更新。本节讲透六个属性、空集合防护与分批上限,收在"foreach 不是批量写入唯一解"的判断上。
动态拼装的最后一节,处理"一个参数是一堆值"的场景。
<select id="findByIds" resultType="User"> SELECT id, username, email FROM users WHERE id IN <foreach collection="ids" item="id" index="i" open="(" separator="," close=")"> #{id} </foreach> </select>
List<User> list = mapper.findByIds(Arrays.asList(1, 2, 3)); // ==> Preparing: SELECT ... WHERE id IN ( ? , ? , ? ) // ==> Parameters: 1(Integer), 2(Integer), 3(Integer)
| 属性 | 作用 | 备注 |
|---|---|---|
| collection | 要遍历的集合在参数里的名字 | 单参数 List 默认名 list;数组是 array;建议显式 Param 命名 |
| item | 每次迭代的元素变量名 | 片段内用井号引用 |
| index | 索引变量名 | List 是下标,Map 是键 |
| open / close | 整个循环体前后的包裹符 | IN 场景的括号 |
| separator | 元素之间的分隔符 | IN 用逗号,多值 INSERT 也用逗号 |
collection 的取名规则是新手高频卡点:方法只传一个 List 且不加注解时,默认名是 list(数组是 array);参数复杂或想语义化,就用注解命名:
// 注解命名后,collection 与注解值对齐 List<User> findByIds(@Param("ids") List<Integer> ids);
背景:购物车页展示勾选的商品,后端拿到商品 id 列表查详情。
事故版本——用户购物车为空时,列表是空的:
mapper.findByIds(Collections.emptyList());
foreach 循环体零次执行,open 与 close 仍会输出,SQL 变成 WHERE id IN ( )——直接语法报错。防护在两个层面:
<!-- 防护一:XML 侧判断列表非空才拼 IN 片段 --> <select id="findByIds" resultType="User"> SELECT id, username, email FROM users <where> <if test="ids != null and ids.size() > 0"> id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </if> </where> </select>
// 防护二:服务层短路,空列表根本不发 SQL public List<User> cartItems(List<Integer> ids) { if (ids == null || ids.isEmpty()) { return Collections.emptyList(); } return mapper.findByIds(ids); }
双层防护不是重复——XML 层防的是"其他调用方忘判空",服务层短路省的是一次注定失败的数据库往返。
<insert id="insertBatch"> INSERT INTO users (username, email) VALUES <foreach collection="list" item="u" separator=","> (#{u.username}, #{u.email}) </foreach> </insert>
List<User> batch = List.of( new User("u1", "u1@example.com"), new User("u2", "u2@example.com")); mapper.insertBatch(batch); session.commit(); // ==> Preparing: INSERT INTO users (username, email) // VALUES ( ? , ? ) , ( ? , ? )
一批数据一条 SQL、一次往返,这是它快的原因;也是它危险的根源——行数乘列数就是占位符个数,行数过大时会撞数据库的参数上限或语句长度上限。2.3 节给出的纪律在此完整落地:几百行内放心用,接近千行就分批,字段宽的表把分批阈值再往低调。
批量场景不止插入。一批订单各自改各自的状态,逐条 UPDATE 是 N 次往返,foreach 加 CASE WHEN 折成一条:
<update id="updateStatusBatch"> UPDATE orders SET status = <foreach collection="list" item="o" open="CASE id" separator=" "> WHEN #{o.id} THEN #{o.status} </foreach> ELSE status END WHERE id IN <foreach collection="list" item="o" open="(" separator="," close=")"> #{o.id} </foreach> </update>
// 输入:[{id=101, status=PAID}, {id=102, status=CLOSED}] // 生成:UPDATE orders SET status = // CASE id WHEN ? THEN ? WHEN ? THEN ? ELSE status END // WHERE id IN ( ? , ? )
一个 foreach 展开成 CASE 分支,另一个展开成 IN 条件,两个循环遍历同一个集合——open 与 separator 的组合让"CASE id"与"ELSE status END"精确包裹住循环体。这种写法可读性有门槛,超过几十行数据或分支逻辑复杂时,BATCH 执行器(2.3 节)是更朴素的选择。

遍历 Map 时 index 是键,item 是值——适合"列名到条件值"这类成对参数:
<!-- 遍历 Map:index 是键 item 是值(示意:批量校验键值对) --> <select id="findByPairs" resultType="User"> SELECT id, username FROM users <where> <foreach collection="pairs" index="col" item="val" separator=" AND "> ${col} = #{val} </foreach> </where> </select>
注意这里 ${col} 拼的是列名(结构位置,美元合法),#{val} 走值通道——2.2 节的分界线在 foreach 内同样生效,且列名入参必须先过白名单。
⚠️ 常见坑:把 foreach 当万能批量方案。它生成的是"一条更大的 SQL",超大 SQL 的解析、执行计划与网络传输都是新成本;另一头,嵌套 foreach(循环里再循环)生成的语句行数是笛卡尔积式的,上线前先用测试数据量演算一遍。
💡 关键直觉:foreach 的本质是"把集合结构翻译成 SQL 结构"——列表译成 IN 序列、行集合译成 VALUES 组、更新集译成 CASE 分支。翻译有损,规模一大就失真;此时换 BATCH 执行器,让 JDBC 层的批处理接管。
第 4 章收官。到这里,手术三段论的中段全部打通。下一章进入术后监护:缓存、类型缝合线、插件、代码生成与 Spring 集成。