zoukankan      html  css  js  c++  java
  • MyBatis 动态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 语句中的任何问题或者性能问题

    作者:Binge
    本文版权归作者和博客园共有,转载必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。
  • 相关阅读:
    [LeetCode] Bulb Switcher II 灯泡开关之二
    [LeetCode] Second Minimum Node In a Binary Tree 二叉树中第二小的结点
    [LeetCode] 670. Maximum Swap 最大置换
    [LeetCode] Trim a Binary Search Tree 修剪一棵二叉搜索树
    [LeetCode] Beautiful Arrangement II 优美排列之二
    [LeetCode] Path Sum IV 二叉树的路径和之四
    [LeetCode] Non-decreasing Array 非递减数列
    [LeetCode] 663. Equal Tree Partition 划分等价树
    [LeetCode] 662. Maximum Width of Binary Tree 二叉树的最大宽度
    [LeetCode] Image Smoother 图片平滑器
  • 原文地址:https://www.cnblogs.com/binbingg/p/13747322.html
Copyright © 2011-2022 走看看