Mybatis 动态SQL

本文最后更新于:2024年9月8日 晚上

Mybatis 动态SQL

介绍

  • 动态SQL:动态SQL指的是根据不同的查询条件,生成不同的Sql语句。
  • MyBatis 的强大特性之一便是它的动态 SQL,如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号,利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
  • 虽然在以前使用动态 SQL 并非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射语句中的强大的动态 SQL 语言得以改进这种情形。
  • 动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似,在 MyBatis 之前的版本中,有很多元素需要花时间了解,MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可,MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。

if 语句

  • test:判断条件。

实例

  • 这条语句提供了可选的查找文本功能,如果不传入 "title”,那么所有处于 "ACTIVE” 状态的 BLOG 都会返回,如果传入了 "title” 参数,那么就会对 "title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现, "title” 的参数值需要包含查找掩码或通配符字符)
1
2
3
4
5
6
7
8
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
  • 如果希望通过 "title” 和 "author” 两个参数进行可选搜索该怎么办呢?首先,我想先将语句名称修改成更名副其实的名称,接下来,只需要加入另一个条件即可。
1
2
3
4
5
6
7
8
9
10
<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 语句。
  • 还是上面的例子,但是策略变为:传入了 "title” 就按 "title” 查找,传入了 "author” 就按 "author” 查找的情形,若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员精选的 Blog)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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’” 设置成动态条件,看看会发生什么。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 会变成这样:
1
2
SELECT * FROM BLOG
WHERE
  • 这会导致查询失败,如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:
1
2
3
SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’
  • 这个查询也会失败,这个问题不能简单地用条件元素来解决,这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。
  • MyBatis 有一个简单且适合大多数场景的解决办法,而在其他场景中,可以对其进行自定义以符合需求,而这,只需要一处简单的改动:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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 元素只会在子元素返回任何内容的情况下才插入 "WHERE” 子句,而且,若子句的开头为 "AND” 或 "OR”, where元素也会将它们去除。
  • 如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能,比如,和 where元素等价的自定义 trim 元素为:
1
2
3
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
  • prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的),上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。
  • 用于动态更新语句的类似解决方案叫做 set,set 元素可以用于动态包含需要更新的列,忽略其它不更新的列,比如:
1
2
3
4
5
6
7
8
9
10
<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 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)
  • 来看看与 set 元素等价的自定义 trim 元素吧:
1
2
3
<trim prefix="SET" suffixOverrides=",">
...
</trim>
  • 注意,我们覆盖了后缀值设置,并且自定义了前缀值。

foreach

  • 动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候),比如:
1
2
3
4
5
6
7
8
9
<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 是值。
  • 至此,我们已经完成了与 XML 配置及映射文件相关的讨论,下一章将详细探讨 Java API,以便你能充分利用已经创建的映射配置。

script

  • 要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素,比如:
1
2
3
4
5
6
7
8
9
10
11
@Update({"<script>",
"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}",
"</script>"})
void updateAuthorValues(Author author);

bind

bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文,比如:

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

多数据库支持

  • 如果配置了 databaseIdProvider,你就可以在动态代码中使用名为 "_databaseId” 的变量来为不同的数据库构建特定的语句,比如下面的例子:
1
2
3
4
5
6
7
8
9
10
11
<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>

动态 SQL 中的插入脚本语言

  • MyBatis 从 3.2 版本开始支持插入脚本语言,这允许你插入一种语言驱动,并基于这种语言来编写动态 SQL 查询语句。
  • 可以通过实现以下接口来插入一种语言:
1
2
3
4
5
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 文件中将它设置为默认语言:
1
2
3
4
5
6
<typeAliases>
<typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
<setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>
  • 或者,你也可以使用 lang 属性为特定的语句指定语言:
1
2
3
<select id="selectBlog" lang="myLanguage">
SELECT * FROM BLOG
</select>
  • 或者,在你的 mapper 接口上添加 @Lang 注解:
1
2
3
4
5
public interface Mapper {
@Lang(MyLanguageDriver.class)
@Select("SELECT * FROM BLOG")
List<Blog> selectBlog();
}
  • 提示 可以使用 Apache Velocity 作为动态语言,更多细节请参考 MyBatis-Velocity 项目。
  • 你前面看到的所有 xml 标签都由默认 MyBatis 语言提供,而它由语言驱动 org.apache.ibatis.scripting.xmltags.XmlLanguageDriver(别名为 xml)所提供。

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!