动态SQL MyBatis

659 阅读7分钟

title: 动态SQL---MyBatis date: 2017.06.10 19:18 categories:

  • 技术博客 tags:
  • 框架了解
  • MyBatis

能够体现MyBatis这个框架的强大与灵活特性的一点,就是Dynamic SQL,正常且合理来讲,学习持久层框架前,Java程序员应该自己去尝试实现JDBC,从耦合到解耦,从不可复用到通用,这个过程不仅仅可以理解JPA规范,更能让自己的基础更扎实,对简单的设计模式也会加入实践。

经过自己对JDBC的折腾,我的一个感触就是:综合来看就一点最关键,即拼接SQL语句,这其实也是代码所要解决最麻烦的问题。

MyBatis的XML解析方案一方面写起来代码不耦合,另一方面用起来功能灵活,并且与Hibernate很不同的一点就是:适合开发者直接对SQL调优。从自己尝试的过程中也有所体现,其实大家都在朝着这个方向编码。

1. 大概原理

无非就是XML解析。

用过JSP的人一定用过JSTL,同理,MyBatis也是定义了自己的标签库,通过标签建立ORM这种思想上来源于OGNL,不过这不是今天的重点,大概明白他是怎么做的就行了。

MyBatis3后的标签精简到了四种:

  • if
  • choose(when,otherwise)
  • trim(where,set)
  • foreach

2. 标签用法

- if

where语句是最常见的:

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

这个场景里面,用户可输入想要的博客的名称,不输入的话到了拼接SQL这层上会自动去掉后面的AND title like #{title},这归功于if,里面的test写的就是条件。当然,现实情况中我们可能会有多个条件,例如:

<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>

相应的在用户界面上就多了个作者的输入口,if标签简单实用。

- choose, when, otherwise

有些时候我们会有类似于Java中switch的需求,这时候单单if可能就不合适了,,因为if是多选,而switch是单选,而choose, when, otherwise的组合可以实现Java中switch的效果:

<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>

上面的otherwise也类似于Javaswitch中的default,即前面两个条件都不满足的情况下才返回featured=1的数据。

- trim, where, set

前面的几个标签已足以应付大多数的场景,但前面有个一定成立的条件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

这样当然不行,接下来还有第二个条件被满足的情况:

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

这样当然也是不行的,所以多条件的拼接处理起来有些麻烦,但MyBatis的用法真叫一个优雅,因为这些问题已经得到了完美解决,只需要在前面的基础上添加一个where闭合标签:

<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,并且后面的and也是根据条件来拼接的,官方宣称可以应付90%的场景,当然我平时写下来用这个标签是最多的,一直完美运行。

除了whereMyBatis还给了开发者自定义的选项,即trim,你可以理解为这是where的详细定义版,我们可以配置具体的元素,下面的代码与上面是一样的功能:

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

prefix代表前缀,内部的条件只要有满足的就拼上,反之就不拼,当然suffix后缀功能也有,而prefixOverrides则负责检测是否需要删掉前缀(后缀同理),如果需要配置多个项需要遵循两个规则:

同样地,在update中我们使用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>

而使用trim后我们就可以自定义前缀set,以及前后缀复写(智能删除最后一项的标记):

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

我同事写过稍微详细点的:

<insert id="insertSelective"  >
    INSERT INTO tableName
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="column_first != null" >
        COLUMN_FIRST,
      </if>
      <if test="columnSecond!= null" >
        COLUMN_SECOND,
      </if>
    </trim>
  </insert>

一方面必须承认他这个代码有一些冗余(高级黑),但这里我们只是拿来看看如何配置详细的前后缀,明白意思即可。

- foreach

在迭代集合的情况下,foreach就成为了首选,直接放代码:

<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>

collection代表你将要迭代的集合,index代表元素索引(根据你的对象类型有所区分),item代表每项元素,同时我们还可以自定义开闭以及分隔符号。

**注意:**可以迭代多种类型(ListSet等),包括Map以及数组对象都是可以的,特别需要留意的一点,迭代Map时,index代表键对象,而item代表值对象。

- bind

bind标签我还真没在项目里面用过,不过也很简单,这个标签支持我们在XML中定义自己的变量,比如模糊查询中定义模糊字段:

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

- Multi-db vendor support

可配置的多数据库支持,我们可以通过预先定义每个数据库的id来在SQL XML中动态选择语句:

<insert id="insert">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    <if test="_databaseId == 'oracle'">
      select seq_users.nextval from dual
    </if>
    <if test="_databaseId == 'db2'">
      select nextval for seq_users from sysibm.sysdummy1"
    </if>
  </selectKey>
  insert into users values (#{id}, #{name})
</insert>

- Pluggable Scripting Languages For Dynamic SQL

MyBatis3.2版本中提供了插件式脚本的功能,也就是说我们可以自定义语句拼接的规则:

首先实现以下接口:

public interface LanguageDriver {
  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

其次在配置中修改默认的脚本驱动:

<typeAliases>
  <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
  <setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>

在不改变默认的情况下,我们可以针对特定的语句应用自己的脚本,只需要在语句上添加lang标签:

<select id="selectBlog" lang="myLanguage">
  SELECT * FROM BLOG
</select>

如果使用注解的情况,框架也提供了@Lang,非常方便:

public interface Mapper {
  @Lang(MyLanguageDriver.class)
  @Select("SELECT * FROM BLOG")
  List<Blog> selectBlog();
}

官方推荐使用Apache Velocity来做动态页面,我这里用的是FreeMarker,道理上是一样的,总之要结合实践嘛。

**注意:**官方的脚本驱动写在了org.apache.ibatis.scripting.xmltags.XmlLanguageDriver中,默认的别名是xml

3. 小结

简单介绍了下MyBatis中的动态SQL,从项目的角度来看,可能具体的标签用法更加重要,但从我个人学习的角度来看,框架的设计理念更值得琢磨,实践确实是检验框架的真理,多用多学吧。

参考: MyBatis中Dynamic SQL官方文档