MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如,拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。

在 MyBatis 之前的版本中,有很多动态 SQL 元素需要花时间了解。MyBatis 3 大大精简了动态 SQL 元素种类,现在只需学习原来一半的元素便可。

MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。主要的动态 SQL 元素有下面几个:if、choose (when, otherwise)、trim (where, set)、foreach。下文将详细的介绍一下这几个SQL元素的用法。

if标签:条件判断

动态 SQL 通常要做的事情是根据条件包含 where 子句的一部分。比如:

<select id="findActiveBlogWithTitleLike" resultType="Blog">
  SELECT * FROM BLOG WHERE state = "ACTIVE"
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

这条语句提供了一种可选的查找文本功能。如果没有传入"title",那么所有处于"ACTIVE"状态的BLOG都会返回;反之若传入了"title",那么就会对"title"一列进行模糊查找并返回 BLOG 结果(注意:"title"参数值是可以包含一些掩码或通配符的)。

如果希望通过"title"和"author"两个参数进行可选搜索该怎么办呢?首先,改变语句的名称让它更具实际意义,然后只要加入另一个条件即可,代码如下所示:

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG WHERE state = "ACTIVE"
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

choose, when, otherwise标签:复杂条件判断

有时我们不想应用到所有的条件语句,而只想从中择其一项。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。

mybatis并没有if...else,在mybatis的sql mapper文件中,条件判断要用choose...when...otherwise。

choose 标签是按顺序判断其内部 when 标签中的 test 条件出否成立,如果有一个成立,则 choose 结束。当 choose 中所有 when 的条件都不满足时,则执行 otherwise 中的sql。类似于Java 的 switch 语句,choose 为 switch,when 为 case,otherwise 则为 default。

还是上面的例子,但是这次变为提供了"title"就按"title"查找,提供了"author"就按"author"查找的情形,若两者都没有提供,就返回所有符合条件的 BLOG,代码如下所示:

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG WHERE state = "ACTIVE"
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

trim, where, set标签

前面几个例子已经方便地解决了一个臭名昭著的动态 SQL 问题。现在回到上面的 "if" 示例,这次我们将 "state = 'ACTIVE' "也设置成动态的条件,看看会发生什么。

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG
  WHERE
  <if test="state != null">
    state = #{state}
  </if>
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

如果这些条件没有一个能匹配上会发生什么?最终这条 SQL 会变成这样:

SELECT * FROM BLOG WHERE

这会导致查询失败。如果仅仅第二个条件匹配又会怎样?这条 SQL 最终会是这样:

SELECT * FROM BLOG WHERE AND title like "someTitle"

这个查询也会失败。

MyBatis 有一个简单的处理,这在 90% 的情况下都会有用。而在不能使用的地方,你可以自定义处理方式来令其正常工作。一处简单的修改就能达到目的:

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

where 元素只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入 "WHERE" 子句。而且,若语句的开头为 "AND" 或 "OR" ,where 元素也会将它们去除。

如果 where 元素没有按正常套路出牌,我们可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

prefixOverrides 属性会忽略通过管道分隔的文本序列(注意此例中的空格也是必要的)。它的作用是移除所有指定在 prefixOverrides 属性中的内容,并且插入 prefix 属性中指定的内容。

类似的用于动态更新语句的解决方案叫做 set。set 元素可以用于动态包含需要更新的列,而舍去其它的。比如:

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

这里,set 元素会动态前置 SET 关键字,同时也会删掉无关的逗号,因为用了条件语句之后很可能就会在生成的 SQL 语句的后面留下这些逗号。(注:因为用的是 "if" 元素,若最后一个 "if" 没有匹配上而前面的匹配上,SQL 语句的最后就会有一个逗号遗留)

若你对 set 元素等价的自定义 trim 元素的代码感兴趣,那这就是它的真面目:

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

注意:这里我们删去的是后缀值,同时添加了前缀值。

foreach标签:遍历集合元素

动态 SQL 的另外一个常用的操作需求是对一个集合进行遍历,通常是在构建 IN 条件语句的时候。比如:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及在迭代结果之间放置分隔符。这个元素是很智能的,因此它不会偶然地附加多余的分隔符。

注意:你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象传递给 foreach 作为集合参数。当使用可迭代对象或者数组时,index 是当前迭代的次数,item 的值是本次迭代获取的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。

mybatis之foreach用法详细介绍

foreach元素的属性主要有item,index,collection,open,separator,close。

(1)item:集合中元素迭代时的别名,该参数为必选。

(2)index:在list和数组中,index是元素的序号,在map中,index是元素的key,该参数可选。

(3)open:foreach代码的开始符号,一般是 "(" ,它和 close=")" 合用。常用在in(),values()时。该参数可选。

(4)separator:元素之间的分隔符,例如在in()的时候,separator=","会自动在元素中间用","隔开,避免手动输入逗号导致sql错误,如in(1,2,)这样。该参数可选。

(5)close: foreach代码的关闭符号,一般是 ")" ,它和 open="(" 合用。常用在in(),values()时。该参数可选。

(6)collection: 要做foreach的对象,作为入参时,List对象默认用"list"代替作为键,数组对象有"array"代替作为键,Map对象没有默认的键。当然在作为入参时可以使用@Param("keyName")来设置键,设置keyName后,list,array将会失效。除了入参这种情况外,还有一种作为参数对象的某个字段的时候。举个例子:如果User有属性List ids,入参是User对象,那么这个collection = "ids"。如果User有属性Ids ids,其中Ids是个对象,Ids有个属性List id,入参是User对象,那么collection = "ids.id"

在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有一下3种情况:

(1)如果传入的是单参数且参数类型是一个List的时候,collection属性值为list。

(2)如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array。

(3)如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的,map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key。

针对最后一条,我们来看一下官方说法:

注意:你可以将一个 List 实例或者数组作为参数对象传给 MyBatis,当你这么做的时候,MyBatis 会自动将它包装在一个 Map 中并以名称为键。List 实例将会以“list”作为键,而数组实例的键将是“array”。

所以,不管是多参数还是单参数的list,array类型,都可以封装为map进行传递。如果传递的是一个list,则mybatis会封装为一个list为key,list值为value的map,如果是array,则封装成一个array为key,array的值为value的map,如果自己封装,则colloection里放的是自己封装的map里的key值。

mybatis之foreach使用方法

(1)单参数List 类型

<select id="countByUserList" resultType="_int" parameterType="list">
select count(*) from users
  <where>
    id in
    <foreach item="item" collection="list" separator="," open="(" close=")" index="">
      #{item.id, jdbcType=NUMERIC}
    </foreach>
  </where>
</select>
SqlSession sqlSession = sqlSessionFactory.openSession();
try 
{
      Mapper mapper = sqlSession.getMapper(Mapper.class);
      User user = new User();
      user.setId(1);
      user.setName("root");
      List<User> users = new ArrayList<User>();
      users.add(user);
      int count = mapper.countByUserList(users);
      
} 
finally 
{
      sqlSession.close();
}

(2)单参数array数组的类型:

<select id="getBlog" resultType="Blog">
     select * from t_blog where id in
     <foreach collection="array" index="index" item="item" open="(" separator="," close=")">
          #{item}
     </foreach>
</select>
SqlSession sqlSession = sqlSessionFactory.openSession();
try 
{
    BlogMapper blogMapper = session.getMapper(BlogMapper.class);
    int[] ids = new int[] {1,3,6,9};
    List blogs = blogMapper.getBlog(ids);       
} 
finally 
{
    sqlSession.close();
}

(3)自定义将参数封装成Map的类型

<select id="getBlog" resultType="Blog">
         select * from t_blog where title like "%"#{title}"%" and id in
          <foreach collection="ids" index="index" item="item" open="(" separator="," close=")">
               #{item}
          </foreach>
 </select>
SqlSession sqlSession = sqlSessionFactory.openSession();
try 
{
   BlogMapper blogMapper = session.getMapper(BlogMapper.class);
   final List ids = new ArrayList();
   ids.add(1);
   ids.add(2);
   ids.add(3);
   Map params = new HashMap();
   params.put("ids", ids);
   params.put("title", "中国");
   List blogs = blogMapper.getBlog(params);       
} 
finally 
{
   sqlSession.close();
}

标签: none

已有 2 条评论

  1. 小张 小张

    这个还是采用xml形式来做的,那怎么在将这些xml动态sql在注解使用,注解不是一个字符串吗?

  2. 小张 小张

    交流群二维码过期了,大佬们

添加新评论