MyBatis 动态 SQL 中的 <foreach> 标签完全解析
一、概述
<foreach> 是 MyBatis 动态 SQL 中用于遍历集合/数组 的核心标签。它允许你将一个 List、Set、Array 或 Map 中的元素逐个取出,动态构建 SQL 语句中的 IN 子句、批量插入、批量更新等操作。
核心价值:将 Java 层的循环逻辑转移到 SQL 层,避免了手动拼接字符串的繁琐和 SQL 注入风险。
二、基本语法
xml
<foreach collection="集合名称"
item="元素别名"
index="索引别名"
open="起始符号"
close="结束符号"
separator="分隔符">
#{元素别名}
</foreach>
2.1 属性详解
| 属性 | 作用 | 是否必须 | 说明 |
|---|---|---|---|
collection |
指定要遍历的集合/数组 | ✅ 必须 | 对应 @Param 注解名或默认的 list/array |
item |
当前遍历元素的别名 | ✅ 必须 | 在循环体内通过 #{item} 引用 |
index |
当前元素的索引/位置 | ❌ 可选 | List 为下标,Map 为 Key |
open |
循环开始时拼接的字符 | ❌ 可选 | 如 (、( |
close |
循环结束时拼接的字符 | ❌ 可选 | 如 )、) |
separator |
元素之间的分隔符 | ❌ 可选 | 如 ,、AND |
三、核心属性 collection 的取值规则
collection 是 <foreach> 中最容易写错的属性,它的取值规则如下:
| 场景 | collection 写法 |
示例 |
|---|---|---|
参数有 @Param("ids") |
注解值(推荐) | collection="ids" |
直接传入 List(无 @Param) |
list |
collection="list" |
直接传入 Array(无 @Param) |
array |
collection="array" |
直接传入 Set(无 @Param) |
collection |
collection="collection" |
传入 Map |
Map 的 Key | collection="keyName" |
| 对象中的集合属性 | 属性路径 | collection="user.roles" |
⚠️ 强烈建议 :无论参数是什么类型,都使用 @Param 注解明确命名,避免混淆。
java
// ✅ 推荐写法
List<Car> selectByIds(@Param("ids") List<Long> ids);
// XML 中直接使用 collection="ids"
四、常见使用场景
场景一:IN 查询
根据传入的 ID 列表批量查询数据。
xml
<select id="selectByIds" resultType="car">
SELECT * FROM t_car
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
调用示例:
java
List<Long> ids = Arrays.asList(1L, 3L, 5L, 7L);
List<Car> cars = carMapper.selectByIds(ids);
生成的 SQL:
sql
SELECT * FROM t_car WHERE id IN (1, 3, 5, 7)
场景二:批量插入(常用)
一次性插入多条记录,减少数据库连接开销。
xml
<insert id="insertBatch">
INSERT INTO t_car (car_num, brand, guide_price, produce_time, car_type)
VALUES
<foreach collection="list" item="car" separator=",">
(#{car.carNum}, #{car.brand}, #{car.guidePrice},
#{car.produceTime}, #{car.carType})
</foreach>
</insert>
调用示例:
java
List<Car> carList = Arrays.asList(
new Car("京A11111", "宝马", 30.0, "2024-01-01", "燃油车"),
new Car("京A22222", "奔驰", 45.0, "2024-01-02", "燃油车")
);
carMapper.insertBatch(carList);
生成的 SQL:
sql
INSERT INTO t_car (car_num, brand, guide_price, produce_time, car_type) VALUES
(?, ?, ?, ?, ?),
(?, ?, ?, ?, ?)
场景三:批量更新
一次性更新多条记录(注意:MySQL 支持 CASE WHEN 语法)。
xml
<update id="updateBatch">
UPDATE t_car
<trim prefix="SET" suffixOverrides=",">
<trim prefix="brand = CASE" suffix="END,">
<foreach collection="list" item="car">
WHEN id = #{car.id} THEN #{car.brand}
</foreach>
</trim>
<trim prefix="guide_price = CASE" suffix="END,">
<foreach collection="list" item="car">
WHEN id = #{car.id} THEN #{car.guidePrice}
</foreach>
</trim>
</trim>
WHERE id IN
<foreach collection="list" item="car" open="(" close=")" separator=",">
#{car.id}
</foreach>
</update>
场景四:动态拼接条件(多个 OR)
构建 (A = ? OR B = ? OR C = ?) 形式的查询条件。
xml
<select id="selectByMultiCondition" resultType="car">
SELECT * FROM t_car
<where>
<foreach collection="brands" item="brand" separator="OR" open="(" close=")">
brand = #{brand}
</foreach>
</where>
</select>
生成的 SQL (传入 ["宝马", "奔驰", "奥迪"]):
sql
SELECT * FROM t_car WHERE (brand = ? OR brand = ? OR brand = ?)
场景五:批量删除
xml
<delete id="deleteBatch">
DELETE FROM t_car
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
场景六:遍历 Map(使用 index)
当传入参数是 Map 时,item 代表值,index 代表键。
xml
<select id="selectByMap" resultType="car">
SELECT * FROM t_car
<where>
<foreach collection="params" item="value" index="key" separator="AND">
${key} = #{value}
</foreach>
</where>
</select>
调用示例:
java
Map<String, Object> params = new HashMap<>();
params.put("brand", "宝马");
params.put("car_type", "燃油车");
List<Car> cars = carMapper.selectByMap(params);
生成的 SQL:
sql
SELECT * FROM t_car WHERE brand = ? AND car_type = ?
五、与 <where> / <set> 的配合
5.1 配合 <where> 动态拼接条件
xml
<select id="searchCars" resultType="car">
SELECT * FROM t_car
<where>
<if test="brands != null and brands.size() > 0">
brand IN
<foreach collection="brands" item="brand" open="(" close=")" separator=",">
#{brand}
</foreach>
</if>
<if test="types != null and types.size() > 0">
AND car_type IN
<foreach collection="types" item="type" open="(" close=")" separator=",">
#{type}
</foreach>
</if>
</where>
</select>
5.2 配合 <set> 批量更新(使用 CASE WHEN)
xml
<update id="updateBatchPrice">
UPDATE t_car
<set>
<foreach collection="list" item="car" separator=",">
guide_price = CASE id
WHEN #{car.id} THEN #{car.guidePrice}
END
</foreach>
</set>
WHERE id IN
<foreach collection="list" item="car" open="(" close=")" separator=",">
#{car.id}
</foreach>
</update>
六、性能优化与最佳实践
6.1 批量插入优化
场景 :插入大量数据时,建议控制单次批量插入的记录数,避免 SQL 过长(超过 max_allowed_packet)。
优化方案:分批次插入,每批 500-1000 条。
java
public void batchInsert(List<Car> carList) {
int batchSize = 500;
for (int i = 0; i < carList.size(); i += batchSize) {
int end = Math.min(i + batchSize, carList.size());
List<Car> subList = carList.subList(i, end);
carMapper.insertBatch(subList);
}
}
6.2 避免 IN 查询数据量过大
当 IN 子句中的元素数量过多时,建议改用 JOIN 或临时表方案,否则会导致索引失效或 SQL 过长。
6.3 使用 @Param 注解
不推荐:
java
List<Car> selectByIds(List<Long> ids); // collection 只能用 list
推荐:
java
List<Car> selectByIds(@Param("ids") List<Long> ids); // collection 用 ids
6.4 空集合处理
在使用 <foreach> 之前,建议先用 <if> 判断集合是否为空,避免生成 IN () 这种语法错误的 SQL。
xml
<if test="ids != null and ids.size() > 0">
AND id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
七、常见错误与避坑指南
1. ❌ collection 属性名写错
xml
<!-- 接口:List<Car> selectByIds(@Param("ids") List<Long> ids); -->
<!-- ❌ 错误 -->
<foreach collection="idList" item="id">
<!-- ✅ 正确 -->
<foreach collection="ids" item="id">
2. ❌ 忘记处理空集合导致 SQL 语法错误
xml
<!-- ❌ 错误:当 ids 为空时,生成 AND id IN () -->
<foreach collection="ids" item="id" open="(" close=")" separator=",">
<!-- ✅ 正确:先判断非空 -->
<if test="ids != null and ids.size() > 0">
AND id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
3. ❌ 使用 ${} 而不是 #{}
xml
<!-- ❌ 错误:存在 SQL 注入风险 -->
<foreach collection="ids" item="id" separator=",">
${id}
</foreach>
<!-- ✅ 正确:使用 #{} 预编译 -->
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
4. ❌ 批量插入时忘记设置分隔符
xml
<!-- ❌ 错误:缺少 separator,导致多个 VALUES 粘连 -->
<foreach collection="list" item="car">
(#{car.carNum}, #{car.brand})
</foreach>
<!-- ✅ 正确 -->
<foreach collection="list" item="car" separator=",">
(#{car.carNum}, #{car.brand})
</foreach>
5. ❌ 参数名与对象属性混淆
xml
<!-- 传入的是 Car 对象的 List -->
<foreach collection="list" item="car">
<!-- ✅ 正确:通过 car 访问属性 -->
#{car.brand}
<!-- ❌ 错误:直接写属性名 -->
#{brand}
</foreach>
八、<foreach> vs 其他批量方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
<foreach> 批量插入 |
一次连接,效率高 | SQL 可能过长 | 中小批量数据(500-1000 条/次) |
JDBC addBatch() |
更底层的批量控制 | 需手写 JDBC 代码 | 超大批量数据 |
| 循环单条插入 | 实现简单 | 每条 SQL 一次连接,性能差 | 少量数据 |
Spring JdbcTemplate 批量 |
框架封装好 | 不适用于 MyBatis 场景 | Spring 项目 |
九、完整实战示例
Mapper 接口
java
public interface CarMapper {
// 批量插入
int insertBatch(@Param("list") List<Car> carList);
// IN 查询
List<Car> selectByIds(@Param("ids") List<Long> ids);
// 动态条件查询
List<Car> selectByBrands(@Param("brands") List<String> brands);
// 批量更新价格
int updateBatchPrice(@Param("list") List<Car> carList);
// 批量删除
int deleteBatch(@Param("ids") List<Long> ids);
}
XML 映射
xml
<mapper namespace="com.xie.mapper.CarMapper">
<!-- 批量插入 -->
<insert id="insertBatch">
INSERT INTO t_car (car_num, brand, guide_price, produce_time, car_type)
VALUES
<foreach collection="list" item="car" separator=",">
(#{car.carNum}, #{car.brand}, #{car.guidePrice},
#{car.produceTime}, #{car.carType})
</foreach>
</insert>
<!-- IN 查询 -->
<select id="selectByIds" resultType="car">
SELECT * FROM t_car
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
<!-- 动态条件 -->
<select id="selectByBrands" resultType="car">
SELECT * FROM t_car
<where>
<if test="brands != null and brands.size() > 0">
brand IN
<foreach collection="brands" item="brand" open="(" close=")" separator=",">
#{brand}
</foreach>
</if>
</where>
</select>
<!-- 批量更新 -->
<update id="updateBatchPrice">
UPDATE t_car
<set>
<foreach collection="list" item="car" separator=",">
guide_price = CASE id
WHEN #{car.id} THEN #{car.guidePrice}
END
</foreach>
</set>
WHERE id IN
<foreach collection="list" item="car" open="(" close=")" separator=",">
#{car.id}
</foreach>
</update>
<!-- 批量删除 -->
<delete id="deleteBatch">
DELETE FROM t_car
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
</mapper>
十、总结速查表
| 场景 | collection 写法 |
推荐写法 |
|---|---|---|
传入 List 且有 @Param("ids") |
ids |
✅ 推荐 |
传入 List 且无 @Param |
list |
⚠️ 不推荐 |
传入 Array 且有 @Param("ids") |
ids |
✅ 推荐 |
传入 Array 且无 @Param |
array |
⚠️ 不推荐 |
传入 Set 且有 @Param("ids") |
ids |
✅ 推荐 |
传入 Map |
Map 的 Key | ✅ 推荐 |
| 对象中的集合属性 | 属性路径 | ✅ 推荐 |
结语
<foreach> 标签是 MyBatis 中处理集合操作的核心利器。掌握它的核心要点:
collection属性优先使用@Param注解命名 ,避免使用默认的list/array。- 批量操作前判断集合是否为空,防止生成非法 SQL。
- 批量插入时控制批次大小,避免 SQL 过长。
- 始终使用
#{}而非${},防止 SQL 注入。
正确使用 <foreach>,可以极大提升批量数据操作的效率和代码的优雅性。