(第4章):MyBatis动态SQL【foreach、bind、OGNL用法】


4.4 foreach 用法

SQL 语句中有时会使用 IN 关键字,例如 id in (1,2,3)。可以使用 ${ids}方式直接获取值,但这种写法不能防止 SQL 注入,想避免 SQL 注入就需要用#{}的方式,这时就要配合使用 foreach 标签来满足需求。

foreach 可以对数组、Map 或实现了 Iterable 接口(如 List、Set)的对象进行遍历。数组在处理时会转换为 List 对象,因此 foreach 遍历的对象可以分为两大类:Iterable 类型和 Map 类型。这两种类型在遍历循环时情况不一样,这一节会通过 3 个例子来讲解 foreach 的用法。

4.4.1 foreach 实现 in 集合

foreach 实现 in 集合(或数组)是最简单和常用的一种情况,下面介绍如何根据传入的用户 id 集合查询出所有符合条件的用户。首先在 UserMapper 接口中增加如下方法。

    /**
* 根据用户 id 集合查询
*
* @param idList
* @return
*/
List<SysUser> selectByIdList(List<Long> idList);

在 UserMapper.xml 中增加如下代码。

    <select id="selectByIdList" resultType="SysUser">
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="list" open="(" close=")" separator="," item="id" index="i">
#{id}
</foreach>
</select>

foreach 包含以下属性。

· collection :必填,值为要迭代循环的属性名。这个属性值的情况有很多。

· item:变量名,值为从迭代对象中取出的每一个值。

· index:索引的属性名,在集合数组情况下值为当前索引值,当迭代循环的对象是 Map 类型时,这个值为 Map 的 key(键值)。

· open:整个循环内容开头的字符串。

· close:整个循环内容结尾的字符串。

· separator :每次循环的分隔符。

collection 的属性要如何设置呢?来看一下 MyBatis 是如何处理这种类型的参数的。

   1.只有一个数组参数或集合参数

以下代码是 DefaultSqlSession 中的方法,也是默认情况下的处理逻辑。

当参数类型为集合的时候,默认会转换为 Map 类型,并添加一个 key 为 collection 的值(MyBatis 3.3.0 版本中增加),如果参数类型是 List 集合,那么就继续添加一个 key 为 list 的值(MyBatis 3.2.8 及低版本中只有这一个 key),

这样,当 collection= "list"时,就能得到这个集合,并对它进行循环操作。

当参数类型为数组的时候,也会转换成 Map 类型,默认的 key 为 array。当采用如下方法使用数组参数时,就需要把 foreach 标签中的 collection 属性值设置为 array。

上面提到的是数组或集合类型的参数默认的名字。推荐使用@Param 来指定参数的名字,这时 collection 就设置为通过@Param 注解指定的名字。

  2.有多个参数

第 2 章中讲过,当有多个参数的时候,要使用@Param 注解给每个参数指定一个名字,否则在 SQL 中使用参数时就会不方便,因此将 collection 设置为@Param 注解指定的名字即可。

  3.参数是 Map 类型

使用 Map 和使用@Param 注解方式类似,将 collection 指定为对应 Map 中的 key 即可。如果要循环所传入的 Map,推荐使用@Param 注解指定名字,此时可将 collection 设置为指定的名字,如果不想指定名字,就使用默认值_parameter。

  4.参数是一个对象

这种情况下指定为对象的属性名即可。当使用对象内多层嵌套的对象时,使用 属性.属性(集合和数组可以使用下标取值)的方式可以指定深层的属性值。

先来看一个简单的测试代码,验证以上说法。

    @Test
public void testSelectByIdList(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<Long> idList = new ArrayList<Long>();
idList.add(1L);
idList.add(1001L);
//业务逻辑中必须校验 idList.size() > 0
List<SysUser> userList = userMapper.selectByIdList(idList);
Assert.assertEquals(2, userList.size());
} finally {
//不要忘记关闭 sqlSession
sqlSession.close();
}
}

public void testSelectByIdList()

可以观察日志打印的 SQL 语句,foreach 元素中的内容最终成为了 in (?,?),根据这部分内容很容易就能理解 open、item、 separator 和 close 这些属性的作用。

关于不同集合类型参数的相关内容,建议大家利用上面的基础方法多去尝试,帮助更好地理解。


4.4.2 foreach 实现批量插入

如果数据库支持批量插入,就可以通过 foreach 来实现。批量插入是 SQL-92 新增的特性,目前支持的数据库有 DB2、 SQL Server 2008 及以上版本、 PostgreSQL 8.2 及以上版本、MySQL、SQLite 3.7.11 及以上版本、H2。批量插入的语法如下。

从待处理部分可以看出,后面是一个值的循环,因此可以通过 foreach 实现循环插入。

在 UserMapper 接口中增加如下方法。

    /**
* 批量插入用户信息
*
* @param userList
* @return
*/
int insertList(@Param("userList)") 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 testInsertList(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//创建一个 user 对象
List<SysUser> userList = new ArrayList<SysUser>();
for(int i = 0; i < 2; i++){
SysUser user = new SysUser();
user.setUserName("test" + i);
user.setUserPassword("123456");
user.setUserEmail("test@mybatis.tk");
userList.add(user);
}
//将新建的对象批量插入数据库中,特别注意,这里的返回值 result 是执行的 SQL 影响的行数
int result = userMapper.insertList(userList);
Assert.assertEquals(2, result);
for(SysUser user : userList){
System.out.println(user.getId());
}
} finally {
//为了不影响数据库中的数据导致其他测试失败,这里选择回滚
sqlSession.rollback();
//不要忘记关闭 sqlSession
sqlSession.close();
}
}

从日志中可以看到通过批量 SQL 语句插入了两条数据。

从 MyBatis 3.3.1 版本开始,MyBatis 开始支持批量新增回写主键值的功能(该功能由本书作者提交),这个功能首先要求数据库主键值为自增类型,同时还要求该数据库提供的 JDBC 驱动可以支持返回批量插入的主键值(JDBC 提供了接口,但并不是所有数据库都完美实现了该接口),因此到目前为止,可以完美支持该功能的仅有 MySQL 数据库。由于 SQL Server 数据库官方提供的 JDBC 只能返回最后一个插入数据的主键值,所以不能支持该功能。

关于批量插入的内容就介绍这么多,对于不支持该功能的数据库,许多人会通过 select...union all select... 的方式去实现,这种方式在不同数据库中实现也不同,并且这种实现也不安全,因此本书中不再提供示例。

4.4.3 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 = #{id}
</update>

这里的 key 作为列名,对应的值作为该列的值,通过 foreach 将需要更新的字段拼接在 SQL 语句中。

该 SQL 对应在 UserMapper 接口中的方法如下。

    /**
* 通过 Map 更新列
*
* @param map
* @return
*/
int updateByMap(Map<String, Object> map);

这里没有通过@Param 注解指定参数名,因而 MyBatis 在内部的上下文中使用了默认值_ parameter 作为该参数的 key,所以在 XML 中也使用了 _parameter

编写测试代码如下。

    @Test
public void testUpdateByMap(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//从数据库查询 1 个 user 对象
Map<String, Object> map = new HashMap<String, Object>();
map.put("id", 1L);
map.put("user_email", "test@mybatis.tk");
map.put("user_password", "12345678");
//更新数据
userMapper.updateByMap(map);
//根据当前 id 查询修改后的数据
SysUser user = userMapper.selectById(1L);
Assert.assertEquals("test@mybatis.tk", user.getUserEmail());
} finally {
//为了不影响数据库中的数据导致其他测试失败,这里选择回滚
sqlSession.rollback();
//不要忘记关闭 sqlSession
sqlSession.close();
}
}

测试代码输出日志如下。


4.5 bind 用法

bind 标签可以使用 OGNL 表达式创建一个变量并将其绑定到上下文中。在前面的例子中, UserMapper.xml 有一个 selectByUser 方法,这个方法用到了 like 查询条件,部分代码如下。

使用 concat 函数连接字符串,在 MySQL 中,这个函数支持多个参数,但在Oracle 中只支持两个参数。由于不同数据库之间的语法差异,如果更换数据库,有些 SQL 语句可能就需要重写。针对这种情况,可以使用 bind 标签来避免由于更换数据库带来的一些麻烦。将上面的方法改为 bind 方式后,代码如下。

bind 标签的两个属性都是必选项,name 为绑定到上下文的变量名,value 为 OGNL 表达式。创建一个 bind 标签的变量后,就可以在下面直接使用,使用 bind 拼接字符串不仅可以避免因更换数据库而修改 SQL,也能预防 SQL 注入。

大家可以根据需求,灵活使用 OGNL 表达式来实现功能,关于更多常用的 OGNL 表达式,我们会在 4.7 节中详细介绍。


4.6 多数据库支持

bind 标签并不能解决更换数据库带来的所有问题,那么还可以通过什么方式支持不同的数据库呢?这需要用到 if 标签以及由 MyBatis 提供的 databaseIdProvider 数据库厂商标识配置。

MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性的。MyBatis 会加载不带 databaseId 属性和带有匹配当前数据库 databaseId 属性的所有语句。如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。为支持多厂商特性,只要像下面这样在 mybatis-config.xml 文件中加入 databaseIdProvider 配置即可。

< databaseIdProvider type= "DB_VENDOR"/>

这里的 DB_VENDOR 会通过 DatabaseMetaData # getDatabaseProductName ()返回的字符串进行设置。由于通常情况下这个字符串都非常长而且相同产品的不同版本会返回不同的值,所以通常会通过设置属性别名来使其变短,代码如下。

上面列举了常见的数据库产品名称,在有 property 配置时, databaseId 将被设置为第一个能匹配数据库产品名称的属性键对应的值,如果没有匹配的属性则会被设置为 null。在这个例子中,如果 getDatabaseProductName ()返回 Microsoft SQL Server , databaseId 将被设置为 sqlserver 。

   提示!

DB_VENDOR 的匹配策略为, DatabaseMetaData # getDatabaseProductName ()返回的字符串包含 property 中 name 部分的值即可匹配,所以虽然 SQL Server 的产品全名一般为 Microsoft SQL Server ,但这里只要设置为 SQL Server 就可以匹配。

数据库产品名一般由所选择的当前数据库的 JDBC 驱动所决定,只要找到对应数据库 DatabaseMetaData 接口的实现类,一般在 getDatabaseProductName ()方法中就可以直接找到该值。任何情况下都可以通过调用 DatabaseMetaData # getDatabaseProductName ()方法获取具体的值。

  

数据库的更换可能只会引起某个 SQL 语句的部分不同,所以也没有必要使用上面的写法,而可以使用 if 标签配合默认的上下文中的_ databaseId 参数这种写法去实现。这样可以避免大量重复的 SQL 出现,方便修改。

selectByUser 方法可以修改如下。这样就可以针对局部来适配不同的数据库了。


4.7 OGNL 用法

在 MyBatis 的动态 SQL 和 ${}形式的参数中都用到了 OGNL 表达式,所以我们有必要了解一下 OGNL 的简单用法。MyBatis 常用的 OGNL 表达式如下。

============================================================================

end

MyBatis从入门到精通(第4章):MyBatis动态SQL【foreach、bind、OGNL用法】的更多相关文章

  1. MyBatis从入门到精通(第5章):5.4 Example 介绍

    jdk1.8.MyBatis3.4.6.MySQL数据库5.6.45.Eclipse Version: 2019-12 M2 (4.14.0) MyBatis从入门到精通(第5章):MyBatis代码 ...

  2. MyBatis从入门到精通(第9章):Spring集成MyBatis(下)

    MyBatis从入门到精通(第9章):Spring集成MyBatis(下) springmvc执行流程原理 mybatis-spring  可以帮助我们将MyBatis代码无缝整合到Spring中.使 ...

  3. MyBatis从入门到精通(第9章):Spring集成MyBatis(中)

    MyBatis从入门到精通(第9章):Spring集成MyBatis(中) 框架(Framework)是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法.应该将应用自身的设计和具体 ...

  4. MyBatis从入门到精通(第9章):Spring集成MyBatis(上)

    MyBatis从入门到精通(第9章):Spring集成MyBatis(上) Spring是一个为了解决企业级Web应用开发过程中面临的复杂性,而被创建的一个非常流行的轻量级框架. mybatis-sp ...

  5. MyBatis从入门到精通(第5章):MyBatis代码生成器

    jdk1.8.MyBatis3.4.6.MySQL数据库5.6.45.Eclipse Version: 2019-12 M2 (4.14.0) MyBatis从入门到精通(第5章):MyBatis代码 ...

  6. MyBatis从入门到精通(第4章):MyBatis动态SQL【if、choose 和 where、set、trim】

    (第4章):MyBatis动态SQL[if.choose 和 where.set.trim] MyBatis 的强大特性之一便是它的动态 SQL.MyBatis 3.4.6版本采用了功能强大的OGNL ...

  7. MyBatis从入门到精通:第一章实体类与Mapper.xml文件

    实体类: package tk.mybatis.simple.model; public class Country { public Long getId() { return id; } publ ...

  8. MyBatis从入门到精通:第一章配置MyBatis

    <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC ...

  9. MyBatis从入门到精通(第2章):MyBatis XML方式的基本用法【insert用法、update用法、delete用法】

    2.4  insert 用法 2.4.1  简单的 insert方法 在接口 UserMapper.java 中添加如下方法. /** * 新增用户 * @param sysUser * @retur ...

随机推荐

  1. PHP数据库基础(简单的)

    经常用到的函数 $link=mysql_connect("数据库地址","用户名","密码");//链接数据库mysql_query(“se ...

  2. css布局 -双飞翼布局&圣杯布局

    一,双飞翼布局 左右两边固定,中间可以随着浏览器放大和缩小 <!DOCTYPE html> <html lang="en"> <head> &l ...

  3. spring boot 实战教程

    二八法则 - get more with less Java.spring经过多年的发展,各种技术纷繁芜杂,初学者往往不知道该从何下手.其实开发技术的世界也符合二八法则,80%的场景中只有20%的技术 ...

  4. CMD手动打jar包

    代码: jar -cvfM "jarpage.jar" @fileslist.txt 解析: 将文档(fileslist.txt)中所有路径对应文件打成jar包,取名为:jarpa ...

  5. Mac安装vue产生错误

    npm WARN checkPermissions Missing write access to /usr/local/lib/node_modules/webpack/node_modules/_ ...

  6. Codeforces 446C 线段树 递推Fibonacci公式

    聪哥推荐的题目 区间修改和区间查询,但是此题新颖之处就在于他的区间修改不是个定值,而是从L 到 R 分别加 F1.F2....Fr-l+1 (F为斐波那契数列) 想了一下之后,觉得用fib的前缀和来解 ...

  7. 10 Json(unity3D)

    //写入json文档注意事项: 1.在Asset下要有一个StreamingAssets文件夹 2.在文件夹内,有一个已创建好的json空文档 3.引入命名空间 using Litjson; usin ...

  8. 2、用优化器使loss最小

    2.tf.train.AdamOptimizer()函数是Adam优化算法:是一个寻找全局最优点的优化算法,引入了二次方梯度校正. tf.train.AdamOptimizer.__init__( l ...

  9. vue组件化应用构建

    组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型.独立和通常可复用的组件构建大型应用.仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树: 在 Vue 里,一个组件本质上是 ...

  10. C++ 一个exe的两个运行实例之间共享数据

    #pragma data_seg("Shared") volatile int iNum = 0; #pragma data_seg() #pragma comment(linke ...