foreach
导学
本节你将掌握 MyBatis 动态 SQL 中用于集合遍历的核心元素 foreach。学习目标是:
- 理解
foreach在动态 SQL 中的核心作用(将 Java 集合展开为 SQL 片段) - 掌握
collection、item、index、open、separator、close六个核心属性 - 理解
collection的三种取值来源:list、array、Map.key和@Param指定名 - 能够编写基于
foreach的 IN 条件查询、批量插入、批量更新
定义
foreach 是 MyBatis 动态 SQL 中用于遍历集合元素的核心元素。它将 Java 中的集合(List、Array、Map)迭代展开为 SQL 语句中的多个值或记录,广泛应用于以下场景:
- IN 条件查询:
WHERE id IN (1, 2, 3) - 批量插入:
INSERT INTO student (name, major) VALUES ('大翔', '计算机科学'), ('白歌', '软件工程') - 批量更新:
UPDATE student SET major = '人工智能' WHERE id IN (1, 2, 3)
在 JDBC 编程中,处理集合参数需要手动拼接占位符:
List<Integer> ids = Arrays.asList(1, 2, 3);
String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
String sql = "SELECT * FROM student WHERE id IN (" + placeholders + ")";
// 然后逐个设置参数...
这种写法不仅繁琐,而且拼接过程容易出错。MyBatis 的 foreach 元素将集合遍历逻辑声明式地表达在 XML 中,自动处理占位符生成和参数绑定,既简洁又安全。
适用位置与核心属性
foreach 可以嵌套在 select、insert、update、delete 语句内部,或嵌套在 where、set、trim 等容器元素内部。
| 属性 | 是否必填 | 说明 |
|---|---|---|
collection | 是 | 指定要遍历的集合。取值可以是 list(List 类型参数)、array(数组参数)、Map 的 key,或 @Param 注解指定的名称 |
item | 是 | 集合中当前元素的别名,在 foreach 内部通过该别名引用当前元素 |
index | 否 | 当前元素的索引(List/Array 时为下标,Map 时为 key)。可选,默认不绑定索引 |
open | 否 | 在遍历结果前添加的字符串,如 ( |
separator | 否 | 每个元素之间的分隔符,如 , |
close | 否 | 在遍历结果后添加的字符串,如 ) |
核心原理
foreach 集合遍历展开流程图
完整示例
场景说明
乐途学院的学生管理系统需要实现以下三个基于集合遍历的功能:
- 根据 ID 列表查询学生:传入一个 ID 列表,查询对应的学生记录
- 批量插入学生:传入一个学生列表,一次性插入多条记录
- 根据 ID 数组批量更新专业:传入一个 ID 数组,将对应学生的专业统一更新
要求分别展示 collection 为 list、array、Map 三种情况的示例。
操作前的数据库表结构及初始数据
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT,
major VARCHAR(20),
score DECIMAL(5,2)
);
INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);
当前表数据:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
完整的映射文件片段
StudentMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.flying.mapper.StudentMapper">
<!-- 示例1:collection 为 list,IN 条件查询 -->
<select id="findByIdList" resultType="Student">
SELECT id, name, age, major, score
FROM student
WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- 示例2:collection 为 array,IN 条件查询 -->
<select id="findByIdArray" resultType="Student">
SELECT id, name, age, major, score
FROM student
WHERE id IN
<foreach collection="array" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- 示例3:collection 为 @Param 指定名,IN 条件查询 -->
<select id="findByIdParam" resultType="Student">
SELECT id, name, age, major, score
FROM student
WHERE id IN
<foreach collection="idList" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- 示例4:collection 为 Map 的 key,批量更新 -->
<update id="updateMajorByIdMap" parameterType="java.util.HashMap">
UPDATE student
SET major = #{newMajor}
WHERE id IN
<foreach collection="idMap.keys" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<!-- 示例5:批量插入学生(使用 foreach 遍历对象列表) -->
<insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO student (name, age, major, score)
VALUES
<foreach collection="list" item="student" separator=",">
(#{student.name}, #{student.age}, #{student.major}, #{student.score})
</foreach>
</insert>
<!-- 示例6:使用 index 属性,遍历带索引的场景 -->
<update id="batchUpdateScore" parameterType="java.util.HashMap">
<foreach collection="scoreMap" item="score" index="stuId" separator=";">
UPDATE student
SET score = #{score}
WHERE id = #{stuId}
</foreach>
</update>
</mapper>
StudentMapper.java(接口)
package com.flying.mapper;
import com.flying.entity.Student;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
public interface StudentMapper {
// collection = "list"
List<Student> findByIdList(List<Integer> idList);
// collection = "array"
List<Student> findByIdArray(int[] idArray);
// collection = "idList"(@Param 指定)
List<Student> findByIdParam(@Param("idList") List<Integer> ids);
// collection = "idMap.keys"(Map 的 key 集合)
int updateMajorByIdMap(Map<String, Object> paramMap);
// 批量插入
int batchInsert(List<Student> students);
// 使用 index 遍历 Map
int batchUpdateScore(Map<Integer, Double> scoreMap);
}
实际执行结果
示例1:collection 为 list(IN 查询)
测试代码:
List<Integer> idList = Arrays.asList(1, 3, 5);
List<Student> list = mapper.findByIdList(idList);
最终生成的 SQL 语句:
SELECT id, name, age, major, score FROM student WHERE id IN (?, ?, ?)
参数值: 1, 3, 5
查询结果集:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
分析: collection="list" 表示参数是一个 List 对象。foreach 遍历该列表,每个元素用 #{id} 引用,open="(" 和 close=")" 生成括号,separator="," 在元素间添加逗号。
示例2:collection 为 array(IN 查询)
测试代码:
int[] idArray = {2, 4};
List<Student> list = mapper.findByIdArray(idArray);
最终生成的 SQL 语句:
SELECT id, name, age, major, score FROM student WHERE id IN (?, ?)
参数值: 2, 4
查询结果集:
| id | name | age | major | score |
|---|---|---|---|---|
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
分析: collection="array" 表示参数是一个数组对象。foreach 对数组的遍历与 List 完全一致,只是 collection 的取值不同。
示例3:collection 为 @Param 指定名(IN 查询)
测试代码:
List<Integer> ids = Arrays.asList(1, 2, 3, 4);
List<Student> list = mapper.findByIdParam(ids);
最终生成的 SQL 语句:
SELECT id, name, age, major, score FROM student WHERE id IN (?, ?, ?, ?)
参数值: 1, 2, 3, 4
查询结果集:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
分析: 当方法参数有多个时,或者为了代码可读性,可以使用 @Param("idList") 注解为参数命名。此时 collection 的值就是注解中指定的名称 "idList",而不是默认的 "list"。
示例4:collection 为 Map 的 key 集合(批量更新)
测试代码:
Map<Integer, String> idMap = new HashMap<>();
idMap.put(1, "dummy"); // value 无所谓,只用 key
idMap.put(2, "dummy");
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("idMap", idMap);
paramMap.put("newMajor", "人工智能");
mapper.updateMajorByIdMap(paramMap);
最终生成的 SQL 语句:
UPDATE student SET major = ? WHERE id IN (?, ?)
参数值: 人工智能, 1, 2
执行后表数据:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 人工智能 | 95.50 |
| 2 | 白歌 | 21 | 人工智能 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
分析: collection="idMap.keys" 表示遍历 Map 的 key 集合。在 foreach 内部,#{id} 引用的是 Map 的每一个 key。这是 foreach 处理 Map 的典型方式。
示例5:批量插入(遍历对象列表)
测试代码:
List<Student> students = new ArrayList<>();
Student s1 = new Student();
s1.setName("乐途");
s1.setAge(23);
s1.setMajor("数据科学");
s1.setScore(93.0);
Student s2 = new Student();
s2.setName("蓝天");
s2.setAge(22);
s2.setMajor("网络安全");
s2.setScore(89.5);
students.add(s1);
students.add(s2);
mapper.batchInsert(students);
最终生成的 SQL 语句:
INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?), (?, ?, ?, ?)
参数值: 乐途, 23, 数据科学, 93.0, 蓝天, 22, 网络安全, 89.5
执行后表数据(新增两条):
| id | name | age | major | score |
|---|---|---|---|---|
| ... | ... | ... | ... | ... |
| 6 | 乐途 | 23 | 数据科学 | 93.00 |
| 7 | 蓝天 | 22 | 网络安全 | 89.50 |
分析: item="student" 表示当前元素是一个 Student 对象,在 foreach 内部通过 #{student.name}、#{student.age} 等方式引用对象的属性。separator="," 在每个 VALUES 元组之间添加逗号,实现一条 SQL 插入多条记录。
示例6:使用 index 遍历 Map(批量更新分数)
测试代码:
Map<Integer, Double> scoreMap = new HashMap<>();
scoreMap.put(3, 95.0); // id=3 的学生分数改为 95.0
scoreMap.put(5, 91.5); // id=5 的学生分数改为 91.5
mapper.batchUpdateScore(scoreMap);
最终生成的 SQL 语句(MySQL 多语句模式):
UPDATE student SET score = ? WHERE id = ?;
UPDATE student SET score = ? WHERE id = ?
参数值: 95.0, 3, 91.5, 5
执行后表数据:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 95.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 91.50 |
分析: collection="scoreMap" 遍历 Map 时,item="score" 是 Map 的 value,index="stuId" 是 Map 的 key。separator=";" 在每个 UPDATE 语句之间添加分号。注意:这种多语句执行需要在 JDBC URL 中开启 allowMultiQueries=true。
易错场景 / 常见误区
| 误区 | 错误示例 | 后果 | 正解 |
|---|---|---|---|
| 集合为空时不做校验 | 传入空 List | foreach 不输出内容,SQL 变成 WHERE id IN (),语法错误 | Java 层前置校验集合非空,或在 XML 中用 if 包裹 foreach |
collection 值写错 | collection="ids" 但参数未用 @Param("ids") | 报错 "There is no getter for property named 'ids'" | 单参数 List 用 collection="list",单参数 Array 用 collection="array",多参数用 @Param 指定名 |
批量插入时忘记 separator | separator 为空 | 多个 VALUES 元组之间没有逗号,SQL 语法错误 | separator="," |
在 foreach 内部用错 item 别名 | #{name} 但 item 是 student | 报错找不到属性 name | 使用 #{student.name} 或确保 item 别名与引用一致 |
| 批量更新用分号但 JDBC 未开启多语句 | separator=";" | 驱动拒绝执行多条语句 | 在 JDBC URL 添加 allowMultiQueries=true,或改用单条 UPDATE + CASE WHEN |
混淆 item 和 index | 以为 index 是元素值 | index 是索引/key,item 才是元素值 | 需要元素值时用 item,需要索引/key 时用 index |
面试考点
Q1:foreach 的 collection 属性有哪些常见取值?
A:
collection的常见取值包括:①"list":当传入参数是单个List时;②"array":当传入参数是单个数组时;③"Map的key"或"map.keys":当遍历 Map 的 key 集合时;④@Param注解指定的名称:当方法参数有多个,或为了可读性显式命名时。如果参数是单个对象且对象内部有集合属性,则collection可以写该属性的路径,如"student.hobbies"。
Q2:以下代码有什么问题?
<foreach collection="ids" item="id" open="(" close=")"> #{id} </foreach>
A:缺少
separator=","。当集合有多个元素时,元素之间没有分隔符,生成的 SQL 会变成(1 2 3),而不是(1, 2, 3),导致语法错误。正确写法是添加separator=","。
Q3:foreach 能否用于批量插入?有什么注意事项?
A:可以。批量插入时,
foreach遍历对象列表,item是当前对象,separator=","在每个VALUES元组之间添加逗号。注意事项:① 确保数据库和驱动支持批量插入语法;② 大量数据时建议分批处理(如每批 500~1000 条),避免 SQL 过长导致内存或网络问题;③ 使用useGeneratedKeys获取自增 ID 时,部分数据库驱动对批量插入的主键返回支持有限。
Q4:foreach 遍历 Map 时,item 和 index 分别代表什么?
A:遍历 Map 时,
index代表当前 Map 条目的key,item代表当前 Map 条目的value。例如<foreach collection="scoreMap" item="score" index="stuId">中,stuId是学生的 ID(key),score是分数值(value)。
小结
foreach 是 MyBatis 动态 SQL 中处理集合遍历的核心元素,它将 Java 中的 List、Array、Map 展开为 SQL 中的多个值或记录。通过 collection、item、index、open、separator、close 六个属性,foreach 可以灵活应对 IN 查询、批量插入、批量更新等多种场景。
使用 foreach 时需要注意:① 确保集合非空,或在 XML 中用 if 做前置判断;② 单参数 List 用 collection="list",单参数 Array 用 collection="array";③ 多参数或需要可读性时用 @Param 指定名称;④ 批量操作注意数据库驱动限制和 SQL 长度限制。
下一节我们将学习 bind 元素,它是 OGNL 上下文变量绑定工具,常用于模糊查询中拼接 % 通配符。
下一章引子
当你需要在 XML 映射文件中创建一个临时变量(如在模糊查询前拼接 % 通配符),或者需要复用一个复杂的 OGNL 表达式时,bind 元素是你的得力助手。它能在 OGNL 上下文中创建一个新变量,供后续的 SQL 片段引用。继续阅读,掌握动态 SQL 中的变量绑定技巧。