Mybatis 动态 SQL
动态 SQL
动态条件查询
以下是我们数据库表 tb_user 的记录:
假设现在有一个需求,就是根据输入的用户年龄和性别,查询用户的记录信息。你可能会说,这太简单了,脑袋里立马蹦出如下的 SQL 语句:
SELECT * FROM `tb_user` where age = 21 and sex = 1
你可能会觉得这条 SQL 语句还不够完美,因为用户名和年龄是输入的参数,不是写死,应该用占位符替换一下,所以修改如下:
SELECT * FROM `tb_user` where age = ? and sex = ?
你可能认为一切到此结束了,但是你想过没有还有以下这些情况?
- 输入的年龄和性别都是 null
- 输入的性别有值,但年龄为 null
- 输入的年龄有值,但性别 null
- 输入的年龄和性别都有值
现在明白了吧,你其实只处理了以上四种情况中的一种,具体而言是最后一种,还有三种情况并没有处理。
你可能觉得,我看不出剩下的三种情况与已经处理的最后那种情况有什么区别。
好吧,我们一起看看吧,我把 SQL 语句改为第一种情况,也就是年龄和性别都是 null,如下:
SELECT * FROM `tb_user` where age = null and sex = null
执行结果如下:
这个结果并不是我们想要的,因为当输入的年龄和性别参数为空,正确结果应该是查到所有 user 记录才对。
所以,正确的 SQL 语句应该如下:
SELECT * FROM `tb_user`
这四种情况的处理,一条 SQL 语句是搞不定的,应该要用四条 SQL 语句。
-
SELECT * FROM `tb_user` SELECT * FROM `tb_user` where sex = ? SELECT * FROM `tb_user` where age = ? SELECT * FROM `tb_user` where age = ? and sex = ?
以上情况就是所谓动态条件查询,也就是当查询条件动态改变时,不同的查询条件对应不同的 SQL 语句。
之前我们动态查询条件是年龄和性别,那么如果我再增加一个查询条件,比如姓名,这又会有多少种情况呀?
相信你很快就有答案了,是八种,没错吧。
怎么得来的呀,这很简单,就是数学的排列组合。当两个动态查询条件,是四种处理情况,也就是2的平方;当三个动态查询条件,就是2的三次方,有八种;以此类推,当有四个动态查询条件,那么就是2的四次方,有十六种。
你可能会想,如果有四种动态查询条件,我得写十六条 SQL 语句,这也太夸张了吧。
那有没有什么办法,无论有多少个动态查询条件,都只需要写一条 SQL 语句。
答案是,有,当然有啦,那就是 MyBatis 动态 SQL。
MyBatis 动态 SQL
动态 SQL 是 MyBatis 的一个强大的特性之一,它提供了 OGNL 表达式动态生成 SQL 的功能。
-
if
if 语句用来解决动态条件查询问题,它可以实现根据条件拼接 SQL 语句,也就是一条 SQL 语句搞定动态条件查询哈。
我们还是用之前年龄和性别两个动态条件查询,来说一说 if 语句的用法。
Mapper 接口方法:
public List<UserEntity> selectUserByAgeAndSex(@Param("userOne") UserEntity userOne,@Param("userTwo") UserEntity userTwo);
SQL 语句映射:
<select id="selectUserByAgeAndSex" resultMap="userResultMap"> select * from tb_user where age > #{userOne.age} and sex = #{userTwo.sex}; </select>
SQL 语句映射(增加动态条件查询):
<select id="selectUserByAgeAndSex" resultMap="userResultMap"> select * from tb_user where 1=1 <if test="userOne != null"> and age > #{userOne.age} </if> <if test="userTwo != null"> and sex = #{userTwo.sex} </if> </select>
接下来,我们分别测试动态条件查询的四种情况,测试前记得要打开 log4j 的 debug 日志开关,这样才能看到 MyBatis 在调试日志中生成的 SQL 语句。
log4j.rootLogger=DEBUG
现在执行之前写的 JUnit 测试方法,如下:
@Test public void selectUserByAgeAndSexTest() { UserEntity userEntityOne = new UserEntity(); userEntityOne.setAge(20); UserEntity userEntityTwo = new UserEntity(); userEntityTwo.setSex(1); List<UserEntity> userEntitie = userMapper.selectUserByAgeAndSex(userEntityOne,userEntityTwo); System.out.println(userEntities); Assert.assertNotNull(userEntities); }
以上执行结果和以前相同,是四种情况中最后一种,生成的 SQL 语句如下:
2020-07-06 21:25:54,791 [main] [mapper.UserMapper.selectUserByAgeAndSex]-[DEBUG] ==> Preparing: select * from tb_user where 1=1 and age > ? and sex = ? 2020-07-06 21:25:54,836 [main] [mapper.UserMapper.selectUserByAgeAndSex]-[DEBUG] ==> Parameters: 20(Integer), 1(Integer)
我把查询条件修改一下,如下:
userMapper.selectUserByAgeAndSex(null,null);
再执行测试,生成的 SQL 语句如下:
2020-07-06 21:28:06,789 [main] [mapper.UserMapper.selectUserByAgeAndSex]-[DEBUG] ==> Preparing: select * from tb_user where 1=1 2020-07-06 21:28:06,898 [main] [mapper.UserMapper.selectUserByAgeAndSex]-[DEBUG] ==> Parameters:
相信看到这里,你应该大致明白 if 语句的作用了吧。还有两种情况,我们继续修改查询条件,如下:
userMapper.selectUserByAgeAndSex(userEntityOne,null);
再执行测试,生成的 SQL 语句如下:
2020-07-06 21:30:46,695 [main] [mapper.UserMapper.selectUserByAgeAndSex]-[DEBUG] ==> Preparing: select * from tb_user where 1=1 and age > ? 2020-07-06 21:30:46,758 [main] [mapper.UserMapper.selectUserByAgeAndSex]-[DEBUG] ==> Parameters: 20(Integer)
最后一种情况了,修改查询条件,如下:
userMapper.selectUserByAgeAndSex(null,userEntityTwo);
执行测试,生成的 SQL 语句如下:
2020-07-06 21:31:59,133 [main] [mapper.UserMapper.selectUserByAgeAndSex]-[DEBUG] ==> Preparing: select * from tb_user where 1=1 and sex = ? 2020-07-06 21:31:59,195 [main] [mapper.UserMapper.selectUserByAgeAndSex]-[DEBUG] ==> Parameters: 1(Integer)
if 语句的作用好比 Java 中的 if 语句,它根据 test 判断条件如果为 true 则拼接里面包含的 SQL 语句片段,如果为 false 则不拼接 SQL 语句片段,语法如下:
<if test="判断条件"> 拼接的SQL语句片段 </if>
这里要注意一下,test 的判断条件直接是参数名,而不需要加 #{} 或者 ${}。
以后遇到动态条件查询,有多少个查询条件就写多少个 if 语句即可,是不是很方便呀。
-
where
可能有同学还会有一个疑惑,就是为什么要在 where 后加上 1=1 ,感觉怪怪的。其实,很容易想明白,目的是为了拼接 SQL 语句时语法正确。如果不加的话,可能会出现如下 SQL 语句:
select * from tb_user where and sex = ? #语法错误
这条 SQL 语句明显语法错误,现在应该明白加上 1=1 的用处了吧。
可能你还不死心,觉得这种写法看起来有点别扭,有没有办法不加 1=1 呀?
呵呵,还真有办法,MyBatis 开发者也考虑到这一点,所以专门提供了一个 where 语句,就是拿来搞定这个的。
用法如下:
<select id="selectUserByAgeAndSex" resultMap="userResultMap"> select * from tb_user <where> <if test="userOne != null"> and age > #{userOne.age} </if> <if test="userTwo != null"> and sex = #{userTwo.sex} </if> </where> </select>
可以看到,where 语句用于格式化输出,并没有什么实质的作用,只是让 SQL 语句看起来舒服一点罢了。
-
choose / when / otherwise
choose / when / otherwise 语句其实和 if 语句作用差不多,但是也有一些区别。
我们还是用之前年龄和性别两个动态条件查询,来说一说它的用法。
<select id="selectUserByAgeAndSex" resultMap="userResultMap"> select * from tb_user <where> <choose> <when test="userOne != null"> age > #{userOne.age} </when> <when test="userTwo != null"> sex = #{userTwo.sex} </when> <otherwise> age > 20 and sex = 1 </otherwise> </choose> </where> </select>
这里的 choose / when / otherwise 语句的作用其实和 Java 里的 switch / case / default 语句或者 if / elseif / else 语句差不多。
它和之前的 if 语句的区别,在于一个 choose 语句可以有多个条件判断分支,每一个 when 语句代表一个条件判断分支。当有一个 when 语句满足条件,其他的 when 语句不再执行条件判断,当所有的 when 都不满足条件,那么就选择默认分支 otherwise。
说了半天,我们还是测试一下,看看效果如何,测试代码如下:
List<UserEntity> userEntities = userMapper.selectUserByAgeAndSex(null,null);
我现在把两个条件查询参数都设置为 null,那么两个 when 语句都不满足条件,最终流程应该选择 otherwise 分支。
执行结果如下:
2020-07-07 16:44:18,077 [main] [mapper.UserMapper.selectUserByAgeAndSex]-[DEBUG] ==> Preparing: select * from tb_user WHERE age > 20 and sex = 1
结果果然不出所料。
现在 if 语句和 choose 语句的使用和区别都明白了, 那么在实际项目开发中该用哪个呢?
如果在 Java 里你知道何时用 if 语句或 switch / case / default 语句,我相信何时使用 if 语句或 choose / when / otherwise 语句,对你来说绝不是问题。
-
foreach
看到 foreach 语句很容易联想到 Java 里 for 语句的增强版 foreach 语句,用法如下:
//创建List并添加元素 List<String> list = new ArrayList<String>(); list.add("1"); list.add("3"); list.add("4"); //利用froeach语句输出集合元素 for (String x : list) { System.out.println(x); }
MyBatis 提供的 foreach 语句主要用来对一个集合进行遍历,通常是用来构建 IN 条件语句,也可用于其他情况下动态拼接 SQL 语句。
我们还是通过一个例子讲解 foreach 语句如何使用的,不过我要事先申明有一定难度哈。
假设我有一个需求,就是我的输入参数是姓名集合(里面有一堆姓名,如张三、李四、王五),需要从数据库中依次查询出对应的用户信息。
看到这个需求,如果你的 SQL 还算扎实,应该立马想到 IN 条件语句,而且脑袋里立刻浮现出如下 SQL 语句:
select * from tb_user where name in ('张三','李四','王五')
执行结果如下:
如果你脑袋里一片空白,建议抽空复习一下 SQL。
以上是直接在数据库里写 SQL 语句,要记住需求里的姓名集合是通过输入参数传递进来的,而不是这里直接写死的哈。
首先,我们需要在 UserMapper.java 里增加一个接口方法,而且方法的参数是姓名集合,如下:
/** * 根据姓名集合查询用户信息 * @param names 姓名集合 * @return 用户实体集合 */ public List<UserEntity> selectUserByNameList(List<String> names);
接着,在 UserMapper.xml 里增加这个接口方法的 SQL 语句映射,如下:
<select id="selectUserByNameList" resultMap="userResultMap"> select * from tb_user where <foreach item="name" collection="list" index="index" open="name in (" separator="," close=")"> #{name} </foreach> </select>
以上可知,foreach 语句有几个属性,如下:
-
collection:表示需要遍历的集合,它的属性值有三种情况,如下:
-
如果传入的是单参数且参数类型是一个 List 的时候,collection 属性值为 list
-
如果传入的是单参数且参数类型是一个 Array 数组的时候,collection 的属性值为 array
-
如果传入的参数是多个的时候,一般需要使用 @param 取别名,collection 属性值为别名
-
-
item:表示每次遍历时生成的对象名
-
index:表示在迭代过程中,每次迭代到的位置
-
open:表示开始遍历时要拼接的字符串
-
separator:表示在每次遍历时两个对象直接的连接字符串
-
close:表示结束遍历时要拼接的字符串
看到 foreach 语句居然有这么多属性,是不是觉得掌握有点难度,这只是纸老虎而已,别被它吓住了。
你只需要明白一点,以上 foreach 语句意思无非就是从 List 集合中迭代获取每个元素,然后再进行 SQL语句拼接,工作原理类似于 Java 的 for 循环动态拼接字符串,如下:
String sql = ""; sql += "name in ("; // 类似 open 属性(前缀) for(int i = 0;i < list.size;i++){//list 类似 collection 集合属性 ,i 类似 index 属性(集合下标) sql += list[i]; // 相当于 #{name},而 name 和 item 属性值相同 sql += ","; // 类似 separator 属性(分隔符) } sql += ")" // 类似 close 属性作用(后缀) System.out.println(sql); //打印结果为 name in ("张三","李四","王五")
这几个属性中,其他属性值照着填写即可,collection 属性值不能乱填,它有三种情况,规则上面已经写得很清楚了,由于我们只有一个参数而且集合是 List 类型,所以适合情况一,collection 属性值应该填写为 list。
最后,在 MyBatisTest.java 中添加单元测试方法,如下:
@Test public void selectUserByNameListTest() { ArrayList<String> names = new ArrayList<String>(); names.add("张三"); names.add("李四"); names.add("王五"); List<UserEntity> userEntities = userMapper.selectUserByNameList(names); System.out.println(userEntities); Assert.assertNotNull(userEntities); }
执行测试,结果如下:
2020-07-08 11:39:28,659 [main] [mapper.UserMapper.selectUserByNameList]-[DEBUG] ==> Preparing: select * from tb_user where name in ( ? , ? , ? ) 2020-07-08 11:39:28,705 [main] [mapper.UserMapper.selectUserByNameList]-[DEBUG] ==> Parameters: 张三(String), 李四(String), 王五(String) 2020-07-08 11:39:28,752 [main] [mapper.UserMapper.selectUserByNameList]-[DEBUG] <== Total: 3 [UserEntity{id=1, userName='zs', password='123456', name='张三', age=22, sex=1, birthday=Sun Sep 02 00:00:00 IRKST 1990, created='2020-06-17 09:30:58.0', updated='2020-06-17 09:30:58.0', interests=null}, UserEntity{id=2, userName='ls', password='123456', name='李四', age=24, sex=1, birthday=Sun Sep 05 00:00:00 IRKST 1993, created='2020-06-17 09:30:58.0', updated='2020-06-17 09:30:58.0', interests=null}, UserEntity{id=6, userName='ww', password='123456', name='王五', age=21, sex=1, birthday=Fri Jan 10 00:00:00 IRKT 1992, created='2020-06-24 18:53:48.0', updated='2020-06-24 18:53:48.0', interests=null}]
foreach 语句最终迭代拼接 SQL 语句构成了一条 IN 条件语句,输入参数 List 集合的元素分别对应三个参数占位符。
我们再仔细想一想,有没有发现这里 foreach 语句的作用其实就是批量查询,等价于三条 SQL 语句如下:
select * from tb_user where name in ( ? , ? , ? ) # 上面一条 SQL 语句等价于下面三条 SQL 语句 select * from tb_user where name = ? #占位符是张三 select * from tb_user where name = ? #占位符是李四 select * from tb_user where name = ? #占位符是王五
那么 foreach 可以完成批量查询,那么也可以完成批量删除等。
-
-
set
set 语句用于更新操作,功能和 where 语句差不多。
假设我们有一个需求,需要更新某个指定用户的姓名和用户名,但是只有姓名不为 null 才更新
映射接口方法:
/** * 更新用户姓名 * @param user 用户姓名 * @return 影响行数 */ public int updateUser(@Param("id") int id,@Param("name") String name);
SQL 语句映射:
<update id="updateUser"> update tb_user set <if test="name != null"> name=#{name}, </if> <if test="name != null"> user_name=#{name}, </if> where id=#{id}; </update>
我们使用 if 语句就可以轻松搞定,而且用一个 if 就够了,但是我用了两个 if 语句,目的是为了讲解 set 语句的作用。
单元测试代码如下:
@Test public void updateUserTest() { int result = userMapper.updateUser(1,"张三三"); sqlSession.commit(); Assert.assertEquals(1,result); }
执行测试,结果如下:
SQL: update tb_user set name=?, user_name=?, where id=?; ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'where id=1' at line 8
报语法错误,很明显,执行的 SQL 语句语法不对,user_name 后面多了一个逗号。
怎么改?你会觉得这还不简单,把多余的逗号去掉就可以啦。
但是,你想一想如果只有第一个 if 语句满足条件,那么一样也会出问题,SQL 语句变为如下:
update tb_user set name=?, where id=?; #SQL 语法错误
或者 两个 if 语句都不满足条件,那么 SQL语句变为如下:
update tb_user set where id=?; #SQL 语法错误
结果都会报 SQL 语法错误问题。
看来这个问题还有点棘手,这时候就该我们的 set 语句出场啦,如下:
<update id="updateUser"> update tb_user <set> <if test="name != null"> name=#{name}, </if> <if test="name != null"> user_name=#{name}, </if> </set> where id=#{id}; </update>
set 语句的作用就是更新操作时自动删除多余的逗号
当然,还有一个问题 set 语句仍然无法解决,就是如果两个 if 语句都不满足条件,即 set 语句后面为空,如下:
update tb_user set where id=?; #SQL 语法错误
-
trim
trim 元素的主要有两个功能:
-
可以在自己包含的内容前加上某些前缀,也可以在其后加上某些后缀
- prefix 属性(添加前缀)
- suffix 属性(添加后缀)
-
可以把包含内容的首部某些内容覆盖,即忽略,也可以把尾部的某些内容覆盖,
-
prefixOverrides 属性(覆盖首部)
-
suffixOverrides 属性(覆盖尾部)
-
正因为 trim 语句有这样的功能,trim 语句可以用来实现 where 语句和 set 语句的效果。
- trim 语句实现 where 语句效果如下:
<!-- where 语句 --> <select id="selectUserByAgeAndSex" resultMap="userResultMap"> select * from tb_user <where> <if test="userOne != null"> age > #{userOne.age} </if> <if test="userTwo != null"> sex = #{userTwo.sex} </if> </where> </select> <!-- trim 语句实现 where 语句--> <select id="selectUserByAgeAndSex" resultMap="userResultMap"> select * from tb_user <trim prefix="where" prefixOverrides="and | or"> <if test="userOne != null"> and age > #{userOne.age} </if> <if test="userTwo != null"> and sex = #{userTwo.sex} </if> </trim> </select>
prefix="where" 表示在 trim 语句包含的语句前添加前缀 where
prefixOverrides="and | or" 表示在 trim 语句包含的语句前出现 and 或 or 则自动忽略
想一想,这不就是之前 where 语句的功能,trim 语句确实可以代替 where 语句。
-
trim 语句实现 set 语句效果如下:
<!-- set 语句 --> <update id="updateUser"> update tb_user <set> <if test="name != null"> name=#{name}, </if> <if test="name != null"> user_name=#{name}, </if> </set> where id=#{id}; </update> <!-- trim 语句实现 set语句 --> <update id="updateUser"> update tb_user <trim prefix="set" suffixOverrides=","> <if test="name != null"> name=#{name}, </if> <if test="name != null"> user_name=#{name}, </if> </trim> where id=#{id}; </update>
prefix="set" 表示在 trim 语句包含的语句前添加前缀 setsuffixOverrides="," 表示在 trim 语句包含的语句后出现逗号则自动忽略
想一想,这不就是之前 set 语句的功能,trim 语句确实可以代替 set 语句。
以上可知,trim 语句比 set 语句和 where 语句更加灵活,但是使用也更复杂。 它不仅可以实现 set 语句和 where 语句的功能,还可以实现更多内容处理。
实际项目开发中,能用 set 语句或 where 语句尽量不用 trim 语句,可以理解 trim 语句是一个更加底层的内容处理语句。
-
总结
所有动态 SQL 语句本质都是简单的对 SQL 语句进行拼接、处理和优化
注意事项:
-
尽管动态 SQL 可以灵活的拼接 SQL 语句,但是也不要滥用 动态 SQL,尽可能业务逻辑比较相似的,通过条件进行控制。试想一下如果一整张表的所有逻辑全都是一条 SQL 语句,通过各种 if 或者 choose 拼接起来,这并不能代表你很牛逼,只能代表你很傻逼,因为可读性和可维护性非常差,出了问题排查起来就会要你的命
-
对于动态 SQL 根本仍旧是 SQL 的编写,所以需要具有良好的 SQL 语句编写能力,动态 SQL 只是可以让 SQL 语句更加灵活,并不能解决你 SQL 语句中的任何问题或者性能问题