MyBatis
的强大特性之一便是它的动态 SQL
。如果你有使用 JDBC
或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL
语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL
这一特性可以彻底摆脱这种痛苦。
虽然在以前使用动态 SQL
并非一件易事,但正是 MyBatis
提供了可以被用在任意 SQL
映射语句中的强大的动态 SQL
语言得以改进这种情形。
动态 SQL
元素和 JSTL
或基于类似 XML
的文本处理器相似。在 MyBatis
之前的版本中,有很多元素需要花时间了解。MyBatis 3
大大精简了元素种类,现在只需学习原来一半的元素便可。MyBatis
采用功能强大的基于 OGNL
的表达式来淘汰其它大部分元素。
动态 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>
有时我们不想应用到所有的条件语句,而只想从中择其一项。针对这种情况,MyBatis
提供了 choose
元素,它有点像 Java
中的 switch
语句。
还是上面的例子,但是这次变为提供了“title
”就按“title
”查找,提供了“author
”就按“author
”查找的情形,若两者都没有提供,就返回所有符合条件的 BLOG
(实际情况可能是由管理员按一定策略选出 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>
现在回到“if
”示例,这次我们将“ACTIVE = 1
”也设置成动态的条件,看看会发生什么。
<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
属性会忽略通过管道分隔的文本序列(注意此例中的空格也是必要的)。它的作用是移除SQL语句中第一个在 prefixOverrides
属性中指定的内容(即AND
和OR
),并且插入 prefix
属性中指定的内容(即WHERE
)。
类似的用于动态更新语句的解决方案叫做 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>
注意这里我们删去的是后缀值,同时添加了前缀值。
动态 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
是值。
bind
元素可以从 OGNL
表达式中创建一个变量并将其绑定到上下文。比如:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
select>
一个配置了“_databaseId
”变量的 databaseIdProvider
可用于动态代码中,这样就可以根据不同的数据库厂商构建特定的语句。比如下面的例子:
<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>
MyBatis
从 3.2
开始支持可插拔脚本语言,这允许你插入一种脚本语言驱动,并基于这种语言来编写动态 SQL
查询语句。
可以通过实现以下接口来插入一种语言:
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);
}
一旦设定了自定义语言驱动,你就可以在 mybatis-config.xml
文件中将它设置为默认语言:
<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();
}