MyBatis 动态 SQL 中的 <foreach> 标签完全解析

MyBatis 动态 SQL 中的 <foreach> 标签完全解析

一、概述

<foreach> 是 MyBatis 动态 SQL 中用于遍历集合/数组 的核心标签。它允许你将一个 ListSetArrayMap 中的元素逐个取出,动态构建 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 中处理集合操作的核心利器。掌握它的核心要点:

  1. collection 属性优先使用 @Param 注解命名 ,避免使用默认的 list/array
  2. 批量操作前判断集合是否为空,防止生成非法 SQL。
  3. 批量插入时控制批次大小,避免 SQL 过长。
  4. 始终使用 #{} 而非 ${},防止 SQL 注入。

正确使用 <foreach>,可以极大提升批量数据操作的效率和代码的优雅性。

相关推荐
SomeB1oody2 小时前
【RustyML入门】5.0. 模型评估
开发语言·后端·机器学习·rust·教程
唐青枫3 小时前
别只把 enum 当常量列表:Zig 枚举、状态机与 Tagged Union 实战
后端
2601_963870213 小时前
【计算机毕业设计】基于Spring Boot的非遗文创产品交易平台的设计与实现
java·spring boot·后端
lv__pf3 小时前
Spring之AOP底层源码解析(下)【TL spring14】
java·后端·spring
苍何3 小时前
保姆级教程,Codex 接入 DeepSeek V4 Pro 和 Grok 4.6,6 种方法全整理
后端
苍何3 小时前
全球首款原生全模态交互式世界模型来了,AI 视频终于能“玩”了
后端
一只叫煤球的猫3 小时前
Spring AI 2.0 源码解析(一):一次 ChatClient 调用到底经历了什么?
后端·面试·ai编程
andongni2033 小时前
后端参数校验
spring boot·后端·mybatis