foreach用法
SQL 语句中有时会使用 IN 关键字,例如 id in(1,2,3)。可以使用 ${ids}
方式直接获取值,但这种写法不能防止 SQL 注入,想避免 SQL 注入就需要用 #{}
的方式,这时就要配合使用 foreach 标签来满足需求。
foreach 可以对数组、Map 或实现了 Iterable 接口(如 List、Set)的对象进行遍历。数组在处理时会转换为 List 对象,因此 foreach 遍历的对象可以分为两大类:Iterable 类型和 Map 类型。这两种类型在遍历循环时情况不一样,这一节会通过 3 个例子来讲解 foreach 的用法。
foreach实现in集合
foreach 实现 in 集合(或数组)是最简单和常用的一种情况,下面介绍如何根据传入的用户 id 集合查询出所有符合条件的用户。首先在 UserMapper 接口中增加如下方法。
/**
* 根据用户ID集合查询
* @param idList 用户ID集合
* @return 用户信息集合
*/
List<SysUser> selectByIdList(List<Long> idList);
在 UserMapper.xml 中增加如下代码。
<select id="selectByIdList" resultMap="baseResultMap">
select id,
user_name userName,
user_password userPassword,
user_email userEmail,
user_info userInfo,
head_img headImg,
create_time createTime
from sys_user
where id in
<foreach collection="idList" open="(" close=")" separator="," item="id" index="i">
#{id}
</foreach>
</select>
foreach 包含以下属性。
-
collection:必填,值为要迭代循环的属性名。这个属性值的情况有很多。
-
item:变量名,值为从迭代对象中取出的每一个值。
-
index:索引的属性名,在集合数组情况下值为当前索引值,当迭代循环的对象是 Map 类型时,这个值为 Map 的 key(键值)。
-
open:整个循环内容开头的字符串。
-
close:整个循环内容结尾的字符串。
-
separator:每次循环的分隔符。
collection 的属性要如何设置呢?来看一下 MyBatis 是如何处理这种类型的参数的。
-
只有一个数组参数或集合参数以下代码是 DefaultSqlSession 中的方法,也是默认情况下的处理逻辑。
private Object wrapCollection(final Object object) { return ParamNameResolver.wrapToMapIfCollection(object, (String)null); } public static Object wrapToMapIfCollection(Object object, String actualParamName) { MapperMethod.ParamMap map; if (object instanceof Collection) { map = new MapperMethod.ParamMap(); map.put("collection", object); if (object instanceof List) { map.put("list", object); } Optional.ofNullable(actualParamName).ifPresent((name) -> { map.put(name, object); }); return map; } else if (object != null && object.getClass().isArray()) { map = new MapperMethod.ParamMap(); map.put("array", object); Optional.ofNullable(actualParamName).ifPresent((name) -> { map.put(name, object); }); return map; } else { return object; } }
当参数类型为集合的时候,默认会转换为 Map 类型,并添加一个 key 为 collection 的值(MyBatis 3.3.0 版本中增加),如果参数类型是 List 集合,那么就继续添加一个 key 为 list 的值(MyBatis 3.2.8 及低版本中只有这一个 key),这样,当 collection="list" 时,就能得到这个集合,并对它进行循环操作。
当参数类型为数组的时候,也会转换成 Map 类型,默认的 key 为 array。当采用如下方法使用数组参数时,就需要把 foreach 标签中的 collection 属性值设置为 array。
/** * 根据用户ID集合查询 * @param idArray 用户ID集合 * @return 用户信息集合 */ List<SysUser> selectByIdList(Long[] idArray);
上面提到的是数组或集合类型的参数默认的名字。推荐使用 @Param 来指定参数的名字,这时 collection 就设置为通过 @Param 注解指定的名字。
-
有多个参数
第 2 章中讲过,当有多个参数的时候,要使用 @Param 注解给每个参数指定一个名字,否则在 SQL 中使用参数时就会不方便,因此将 collection 设置为 @Param 注解指定的名字即可。
-
参数是 Map 类型
使用 Map 和使用 @Param 注解方式类似,将 collection 指定为对应 Map 中的 key 即可。如果要循环所传入的 Map,推荐使用 @Param 注解指定名字,此时可将 collection 设置为指定的名字,如果不想指定名字,就使用默认值 _parameter。
-
参数是一个对象
这种情况下指定为对象的属性名即可。当使用对象内多层嵌套的对象时,使用属性.属性(集合和数组可以使用下标取值)的方式可以指定深层的属性值。
先来看一个简单的测试代码,验证以上说法。
@Test
public void testSelectByIdList(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<Long> idList = new ArrayList<>();
idList.add(1L);
idList.add(1001L);
// 业务逻辑中必须校验idList.size() > 0
List<SysUser> userList = userMapper.selectByIdList(idList);
Assert.assertEquals(2,userList.size());
} finally {
// 不要忘记关闭 sqlSession
sqlSession.close();
}
}
该测试输出的日志如下。
可以观察日志打印的 SQL 语句,foreach 元素中的内容最终成为了 in(?,?),根据这部分内容很容易就能理解 open、item、separator 和 close 这些属性的作用。
关于不同集合类型参数的相关内容,建议大家利用上面的基础方法多去尝试,帮助更好地理解。
foreach实现批量插入
如果数据库支持批量插入,就可以通过 foreach 来实现。批量插入是 SQL-92 新增的特性,目前支持的数据库有 DB2、SQL Server 2008 及以上版本、PostgreSQL 8.2 及以上版本、MySQL、SQLite 3.7.11 及以上版本、H2。批量插入的语法如下。
INSERT INTO tablename (column-a, [column-b,...])
VALUES ('value-1a', ['value-1b', ...]),
('value-2a', ['value-2b', ...]),
...
从待处理部分可以看出,后面是一个值的循环,因此可以通过 foreach 实现循环插入。
在 UserMapper 接口中增加如下方法。
/**
* 批量插入用户信息
* @param userList 用户集合
*/
void insertList(List<SysUser> userList);
在 UserMapper.xml 中添加如下 SQL。
<insert id="insertList" useGeneratedKeys="true" keyProperty="id">
insert into sys_user(
user_name, user_password, user_email,
user_info, head_img, create_time
)
values
<foreach collection="userList" item="user" separator=",">
(#{user.userName},#{user.userPassword},#{user.userEmail},
#{user.userInfo},#{user.headImg,jdbcType=BLOB},#{user.createTime,jdbcType=TIMESTAMP})
</foreach>
</insert>
通过 item 指定了循环变量名后,在引用值的时候使用的是 “属性.属性” 的方式,如 user.userName。 |
针对该方法编写如下测试。
@Test
public void testInsertUserList(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 创建一个 user 对象
List<SysUser> userList = new ArrayList<>();
SysUser user;
for (int i = 0; i < 2; i++) {
user = new SysUser();
user.setUserName("test" + i);
user.setUserPassword("123456");
user.setUserEmail("test@zccoder.com");
userList.add(user);
}
// 将新建的对象批量插入数据库中
// 特别注意,这里的返回值 result 是执行 SQL 影响的行数
int result = userMapper.insertList(userList);
Assert.assertEquals(2, result);
for (SysUser userTemp : userList) {
System.out.println(userTemp.getId());
}
} finally {
// 为了不影响其它测试,这里选择回滚
sqlSession.rollback();
// 不要忘记关闭 sqlSession
sqlSession.close();
}
}
为了使输出的日志不那么冗长,这里只测试插入两条数据的情况,输出的日志如下。
从日志中可以看到通过批量 SQL 语句插入了两条数据。
从 MyBatis 3.3.1 版本开始,MyBatis 开始支持批量新增回写主键值的功能(该功能由本书作者提交),这个功能首先要求数据库主键值为自增类型,同时还要求该数据库提供的 JDBC 驱动可以支持返回批量插入的主键值(JDBC 提供了接口,但并不是所有数据库都完美实现了该接口),因此到目前为止,可以完美支持该功能的仅有 MySQL 数据库。由于 SQL Server 数据库官方提供的 JDBC 只能返回最后一个插入数据的主键值,所以不能支持该功能。
如果要在 MySQL 中实现批量插入返回自增主键值,只需要在原来代码基础上进行如下修改即可。
<insert id="insertList" useGeneratedKeys="true" keyProperty="id">
和单表一样,此处增加了 useGeneratedKeys 和 keyProperty 两个属性,增加这两个属性后,简单修改测试类,输出 id 值。
// 在调用 insertList 之后
for(SysUser user: userList) {
System.out.println(user.getId());
}
执行测试后,可以看到 id 部分的日志如下。
1023
1024
关于批量插入的内容就介绍这么多,对于不支持该功能的数据库,许多人会通过 select…union all select… 的方式去实现,这种方式在不同数据库中实现也不同,并且这种实现也不安全,因此本书中不再提供示例。
foreach实现动态UPDATE
这一节主要介绍当参数类型是 Map 时,foreach 如何实现动态 UPDATE。
当参数是 Map 类型的时候,foreach 标签的 index 属性值对应的不是索引值,而是 Map 中的 key,利用这个 key 可以实现动态 UPDATE。
现在需要通过指定的列名和对应的值去更新数据,实现代码如下。
<update id="updateByMap">
update sys_user
set
<foreach collection="_parameter" item="val" index="key" separator=",">
${key} = #{val}
</foreach>
where id = #{map.id}
</update>
这里的 key 作为列名,对应的值作为该列的值,通过 foreach 将需要更新的字段拼接在 SQL 语句中。
该 SQL 对应在 UserMapper 接口中的方法如下。
/**
* 通过 Map 更新列
* @param map Map集合
*/
void updateByMap(@Param("map") Map<String,Object> map);
这里没有通过 @Param 注解指定参数名,因而 MyBatis 在内部的上下文中使用了默认值 _parameter 作为该参数的 key,所以在 XML 中也使用了 _parameter 。编写测试代码如下。
@Test
public void testUpdateByMap(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
Map<String,Object> map = new HashMap<>();
// 查询条件,同样也是更新字段,必须保证该值存在
map.put("id",1L);
// 更新其它字段
map.put("user_email","test@zccoder.com");
map.put("user_password","12345678");
map.put("id",1L);
userMapper.updateByMap(map);
// 根据当前 id 查询修改后的数据
SysUser user = userMapper.selectById(1L);
Assert.assertEquals("test@zccoder.com",user.getUserEmail());
} finally {
// 为了不影响其它测试,这里选择回滚
sqlSession.rollback();
// 不要忘记关闭 sqlSession
sqlSession.close();
}
}
测试代码输出日志如下。
到这里,foreach 的全部内容就介绍完了,下一节将介绍 bind 的用法。