Mybatis 文档篇 4:Dynamic SQL

One of the most powerful features of MyBatis has always been its Dynamic SQL capabilities. If you have any experience with JDBC or any similar framework, you understand how painful it is to conditionally concatenate strings of SQL together, making sure not to forget spaces or to omit a comma at the end of a list of columns. Dynamic SQL can be downright painful to deal with.
MyBatis 最强大的特性之一是它的动态 SQL。如果你用过 JDBC 或其他类似的框架,你就知道根据不同条件拼接 SQL 语句有多痛苦了,比如不能忘记空格或要去掉最后一个列后面的逗号。动态 SQL 可以完全摆脱这种痛苦。

While working with Dynamic SQL will never be a party, MyBatis certainly improves the situation with a powerful Dynamic SQL language that can be used within any mapped SQL statement.
虽然在以前使用动态 SQL 并非易事,MyBatis 改进了这种情况,提供了可供任何映射 SQL 语句使用的强大的动态 SQL 语言。

The Dynamic SQL elements should be familiar to anyone who has used JSTL or any similar XML based text processors.In previous versions of MyBatis, there were a lot of elements to know and understand. MyBatis 3 greatly improves upon this, and now there are less than half of those elements to work with.
如果你使用过 JSTL 或任何类似的基于 XML 的文本处理器,那么动态 SQL 元素对你来说一点也不陌生。在 MyBatis 之前的版本中,有很多的元素需要去学习和理解。MyBatis 3 很大程度上优化了这一点,现在只需要使用比之前少于一半的元素。

MyBatis employs powerful OGNL based expressions to eliminate most of the other elements:
MyBatis 使用强大的基于 OGNL 的表达式来淘汰其他大部分元素:

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

1 if

The most common thing to do in dynamic SQL is conditionally include a part of a where clause. For example:
在动态 SQL 中最常做的一件事是根据条件拼接 where 子句。

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

This statement would provide an optional text search type of functionality. If you passed in no title, then all active Blogs would be returned. But if you do pass in a title, it will look for a title like that (for the keen eyed, yes in this case your parameter value would need to include any masking or wildcard characters).
这个语句提供了一种可选的查找文本功能。如果你没有传进来一个 title,那么所有 ‘ACTIVE’ 状态的 Blog 都会被返回。如果你传入了一个 title,那么它会模糊查找匹配 title 的(细心的读者可能会发现,你的参数值需要包含一些掩码或通配符)。

What if we wanted to optionally search by title and author? First, I’d change the name of the statement to make more sense. Then simply add another condition.
如果我们想要选择性地按照 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>

2 choose, when, otherwise

Sometimes we don’t want all of the conditionals to apply, instead we want to choose only one case among many options. Similar to a switch statement in Java, MyBatis offers a choose element.
有时候我们不想应用到所有的条件,而是从中选一个。MyBatis 提供了和 Java 中的 switch 语句很像的 choose 元素。

Let’s use the example above, but now let’s search only on title if one is provided, then only by author if one is provided. If neither is provided, let’s only return featured blogs.
我们继续使用上面的例子,但是现在改为如果提供了 title 就只按 title 查,如果提供了 author 就只按 author 查。如果都没有提供,就只返回 featured = 1 的 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>

3 trim, where, set

The previous examples have been conveniently dancing around a notorious dynamic SQL challenge. Consider what would happen if we return to our "if" example, but this time we make "ACTIVE = 1" a dynamic condition as well.
前面的例子已经很好地解决了一个臭名昭著的动态 SQL 的问题。现在让我们回到 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>

What happens if none of the conditions are met? You would end up with SQL that looked like this:
如果上面没有条件匹配怎么办?最终你的 SQL 会变成这个样子:

SELECT * FROM BLOG
WHERE

This would fail. What if only the second condition was met? You would end up with SQL that looked like this:
这就会导致失败。如果只有第二个条件匹配怎么办?最终你的 SQL 又会变成这个样子:

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

This would also fail. This problem is not easily solved with conditionals, and if you’ve ever had to write it, then you likely never want to do so again.
这同样也会导致失败。这个问题不能简单地使用条件句式来解决,如果你曾经不得不这么写过,那么你可能再也不想这么用。

3.1 where 元素

MyBatis has a simple answer that will likely work in 90% of the cases. And in cases where it doesn’t, you can customize it so that it does. With one simple change, everything works fine:
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>

The where element knows to only insert "WHERE" if there is any content returned by the containing tags. Furthermore, if that content begins with "AND" or "OR", it knows to strip it off.
where 元素只会在其包含的标签内有返回内容时才插入 "WHERE" 。而且,如果返回的内容是以 "AND" 或 "OR" 开头,它也会将其去除。

3.2 trim 元素

If the where element does not behave exactly as you like, you can customize it by defining your own trim element. For example, the trim equivalent to the where element is:
如果 where 元素没有以你想要的方式工作,你可以自定义 trim 元素来自定义它。例如,和 where 元素等价的 trim 如下:

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

The prefixOverrides attribute takes a pipe delimited list of text to override, where whitespace is relevant. The result is the removal of anything specified in the prefixOverrides attribute, and the insertion of anything in the prefix attribute.
prefixOverrides 属性采用管道分隔的文本列表进行覆盖,其中的空格也是必要的。它的作用是移除所有在 prefixOverrides 属性中指定的内容,并且插入在 prefix 中指定的内容。

3.3 set 元素

There is a similar solution for dynamic update statements called set. The set element can be used to dynamically include columns to update, and leave out others. For example:
还有一个动态 update 语句的类似解决方案 :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>

Here, the set element will dynamically prepend the SET keyword, and also eliminate any extraneous commas that might trail the value assignments after the conditions are applied.
这里,set 元素将动态地在前面拼接 set 关键字,同时也会删掉在条件应用之后可能出现在值后面的无关逗号。

用 trim 实现 set:

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

4 foreach

Another common necessity for dynamic SQL is the need to iterate over a collection, often to build an IN condition. For example:
动态 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>

The foreach element is very powerful, and allows you to specify a collection, declare item and index variables that can be used inside the body of the element. It also allows you to specify opening and closing strings, and add a separator to place in between iterations. The element is smart in that it won’t accidentally append extra separators.
foreach 元素是很强大的,它允许你指定一个 collection(集合),声明在元素体中可以使用的 item(集合项) 和 index(索引) 变量。它也允许你指定开头和结尾的字符串,并在迭代项之间添加一个分隔符。这个元素是很聪明的,它不会偶然地添加多余的分隔符。

NOTE You can pass any Iterable object (for example List, Set, etc.), as well as any Map or Array object to foreach as collection parameter. When using an Iterable or Array, index will be the number of current iteration and value item will be the element retrieved in this iteration. When using a Map (or Collection of Map.Entry objects), index will be the key object and item will be the value object.
注意:你可以传递任何一个可迭代对象(比如 List、Set 等),以及任何 Map 或数组对象到 foreach,作为 collection 的参数。使用可迭代对象或数组时,index 是当前迭代的次数,item 的值是本次迭代获取的元素。当使用一个 Map(或 Map.Entry 对象的集合)时,index 是 key 对象,item 则是 value 对象。

5 bind

The bind element lets you create a variable out of an OGNL expression and bind it to the context. For example:
bind 元素可以让你从 OGNL 表达式中创建一个变量并将它绑定到上下文。

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

6 多数据库厂商支持

If a databaseIdProvider was configured a "_databaseId" variable is available for dynamic code, so you can build different statements depending on database vendor.
如果一个 databaseIdProvider 配置了一个 "_databaseId" 变量,那么它可用于动态代码,因此你可以根据不同的数据库厂商构建不同的语句。

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

7 可插拔脚本语言

Starting from version 3.2 MyBatis supports pluggable scripting languages, so you can plug a language driver and use that language to write your dynamic SQL queries.
从3.2 版本开始,MyBatis 支持可插拔脚本语言,所以你可以插入一个语言驱动并使用该语言来编写你的动态 SQL 查询语句。

You can plug a language by implementing the following interface:
你可以通过实现下面这个接口来插入语言:

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);
}

Once you have your custom language driver you can set it to be the default by configuring it in the mybatis-config.xml file:
一旦你有了自己的自定义语言驱动你就可以在 mybatis-config.xml 中将配置它设置为默认语言。

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

Instead of changing the default, you can specify the language for an specific statement by adding the lang attribute as follows:
除了改变默认语言,你也可以通过添加 lang 属性来为特定的语句指定语言:

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

Or, in the case you are using mappers, using the @Lang annotation:
或者,如果你使用的是 Mapper 接口,使用 @Lang 注解:

public interface Mapper {
  @Lang(MyLanguageDriver.class)
You can use Apache Velocity as your dynamic l  @Select("SELECT * FROM BLOG")
  List<Blog> selectBlog();
}

NOTE anguage. Have a look at the MyBatis-Velocity project for the details.
注意:你可以使用 Apache Velocity 来作为动态语言。 更多细节请参考 MyBatis-Velocity 项目。

All the xml tags you have seen in the previous sections are provided by the default MyBatis language that is provided by the driver org.apache.ibatis.scripting.xmltags.XmlLanguageDriver which is aliased as xml.
你前面看到的所有的 xml 标签都是由默认 MyBatis 语言提供的,它是由别名为 xml 的语言驱动 org.apache.ibatis.scripting.xmltags.XmlLanguageDriver 提供。

最后

说明:MyBatis 官网提供了简体中文的翻译,但个人觉得较为生硬,甚至有些地方逻辑不通,于是自己一个个重新敲着翻译的(都不知道哪里来的自信...),有些地方同官网翻译有出入,有些倔强地保留了自己的,有的实在别扭则保留了官网的,这些都会在实践中一一更正。鉴于个人英文能力有限,文章中保留了官方文档原英文介绍(个别地方加以调整修剪),希望有缘看到这里的朋友们能够有自己的理解,不会被我可能错误或不合理的翻译带跑偏(〃'▽'〃),欢迎指正!

当前版本:mybatis-3.5.0
官网文档:MyBatis
官网翻译:MyBatis 简体中文
项目实践:MyBatis Learn

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容