在前面的文章中,我们了解了 select
, insert
, update
以及 delete
元素的属性定义,但是刻意回避了这四个元素中关于动态 sql
的子元素定义.
<!ELEMENT select (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*> <!ELEMENT insert (#PCDATA | selectKey | include | trim | where | set | foreach | choose | if | bind)*> <!ELEMENT update (#PCDATA | selectKey | include | trim | where | set | foreach | choose | if | bind)*> <!ELEMENT delete (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*> 复制代码
动态 sql
元素是 mybatis
中一个非常方便且强大的功能,通过这些简单的元素我们可以很简单的根据运行上下文的不同来执行不同的 sql
语句.
目前,在 mybatis
中有 8
个动态sql元素:
按照在 select
元素中的定义顺序,他们分别是: include
, trim
, where
, set
, foreach
, choose
, if
以及 bind
.
这些元素的用法和作用各不相同,下面我们就按照顺序对上面的这些元素依次进行了解.
在 Mybatis源码之美:3.6.解析sql代码块 一文中,我们简单对 include
元素的用法做了了解,并指出: 我们可以通过include标签来引用已配置的sql元素,从而实现代码复用的效果.
include
元素的定义并不复杂,他只有一个属性和一个子元素定义:
<!ELEMENT include (property+)?> <!ATTLIST include refid CDATA #REQUIRED > 复制代码
其中属性 refid
用于引用 sql
代码块,他的取值可以是一个 sql
代码块的 全局唯一标志 ,也可以是当前 mapper
文件中的 sql
元素的简单引用标志.
比如,针对下面配置:
<mapper namespace="cn.jpanda.example.Mapper"> <sql id="sqlId"> ... </sql> <select id="selectId"> ... </select> </mapper> 复制代码
名为 selectId
的 select
元素在引用名为 sqlId
的 sql
元素时,既可以通过 cn.jpanda.example.Mapper.sqlId
来引用,也可以通过 sqlId
来引用.
被引用的 sql
元素可以包含动态参数或者动态 sql
定义,
<!ELEMENT sql (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*> 复制代码
在解析时,被引用 sql
元素所需的属性定义将会从运行上下文获取,因为在引用环境中所需的属性可能 不存 在或者 名称不匹配 ,因此 include
元素提供了 property
子元素来进行额外的上下文属性配置.
比如,在下面的这个示例中,被引用的 sql
元素需要一个名为 name
的属性配置:
<sql id="nameFilter"> AND name= ${name} </sql> 复制代码
但是在我们的引用方法 selectUserByName
中的入参名称为 uname
:
List<User> selectUserByName(@Param("uname") String name); 复制代码
这时候,除了调整 @Param
注解的设置之外,我们还可以通过 property
子属性将 uname
属性映射为 name
属性:
<select id="selectUserByName" resultType="org.apache.learning.dynamic_sql.User"> SELECT * FROM USER <where> <include refid="nameFilter"> <property name="name" value="'${uname}'"/> </include> </where> </select> 复制代码
这样,当我们调用 selectUserByName
时:
userMapper.selectUserByName("Panda"); 复制代码
真正执行的sql语句是:
SELECT * FROM USER WHERE name= 'Panda' 复制代码
需要注意上面定义的 nameFilter
中,关于参数的定义使用的 ${}
而不是 #{}
,有关 ${}
和 #{}
的区别,我们会在后文给出.
最后给一张图总结一下 include
元素:
在 java
中, String
对象有一个 trim
方法,该方法的作用是清理字符串两端的空白符.
这里的 trim
元素和其相似,却又有很大的不同之处,在 mybatis
中, trim
元素用于处理 SQL
配置中的字符串内容.
trim
元素有四个属性定义,这四个属性用于控制字符串处理的行为:
<!ELEMENT trim (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*> <!ATTLIST trim prefix CDATA #IMPLIED prefixOverrides CDATA #IMPLIED suffix CDATA #IMPLIED suffixOverrides CDATA #IMPLIED > 复制代码
同时 trim
元素也是一个 PCDATA
类型的节点,他可以混排 SQL
语句以及动态 SQL
元素定义.
trim
元素四个属性的用法比较简单,其中 prefix
属性用于配置被拼接的字符串前缀,他的效果相当于 String
对象的 concat()
方法,比如针对配置:
<select id="selectUserByName" resultType="org.apache.learning.dynamic_sql.User"> SELECT * FROM USER <trim prefix="WHERE"> name=#{name} </trim> </select> 复制代码
其 trim
配置 大致 与等效于 java
代码:
"WHERE".concat(" name=#{name}"); 复制代码
当然,二者的效果肯定有所不同,其中最大的差距在于:如果 trim
元素中无有效字符串,那么 trim
元素的配置会被忽略.
比如,针对下列单元测试:
UserMapper.xml
配置:
<sql id="nameFilter"> <if test="name != null"> name=#{name} </if> </sql> <select id="selectUserByName" resultType="org.apache.learning.dynamic_sql.User"> SELECT * FROM USER <trim prefix="WHERE"> <include refid="nameFilter"/> </trim> </select> 复制代码
UserMapper
接口定义:
public interface UserMapper { List<User> selectUserByName(@Param("name") String name); } 复制代码
单元测试(部分代码):
@Test public void selectUserByNameTest() { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); userMapper.selectUserByName("Panda"); userMapper.selectUserByName(null); } 复制代码
在运行时,两次方法调用分别执行了不同的 SQL
语句,执行日志(部分):
DEBUG [main] - ==> Preparing: SELECT * FROM USER WHERE name=? DEBUG [main] - ==> Parameters: Panda(String) DEBUG [main] - ==> Preparing: SELECT * FROM USER 复制代码
仔细看,第二次方法调用中执行的 SQL
语句没有 where
部分.
因此 trim
元素的 prefix
属性的实际效果应该类似于:
String rawSQL = "..."; String prefix = ""; String newSql=rawSQL; if (null != rawSQL && (!rawSQL.trim().isEmpty()) ) { newSql= prefix.concat(rawSQL); } 复制代码
trim
元素的 suffix
和 prefix
相似, suffix
属性用于配置需要添加的后缀.
prefixOverrides
属性用来配置字符串前需要被覆盖的内容,或者说需要被移除的内容,比如,针对配置:
<sql id="nameFilter2"> <if test="name != null"> AND name=#{name} </if> </sql> <select id="selectUserByName2" resultType="org.apache.learning.dynamic_sql.User"> SELECT * FROM USER <trim prefix="WHERE" prefixOverrides="AND"> <include refid="nameFilter"/> </trim> </select> 复制代码
根据 prefixOverrides
属性定义,在运行时 nameFilter2
中的 AND
字符串将会被移除.
单元测试:
@Test public void selectUserByNameTest2() { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); userMapper.selectUserByName2("Panda"); userMapper.selectUserByName2(null); } 复制代码
执行日志(部分):
DEBUG [main] - ==> Preparing: SELECT * FROM USER WHERE name=? DEBUG [main] - ==> Parameters: Panda(String) DEBUG [main] - ==> Preparing: SELECT * FROM USER 复制代码
同理, suffixOverrides
属性用于配置需要被移除的后缀字符串.
还是一样,用一张总结一下:
where
元素的作用是在映射声明语句中配置 where
关键字的位置,他是 trim
元素的一种直观体现,在实现上 where
元素也是作为 trim
元素的子类工作的.
<!ELEMENT where (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*> 复制代码
一个 where
元素的配置基本等同于下面的 trim
元素配置:
<trim prefix="WHERE" prefixOverrides="AND |OR |AND/n|OR/n|AND/r|or|r|AND/t|OR/t"> </trim> 复制代码
总结一下 where
元素:
set
元素和 where
元素十分相似,它用于在更新语句中配置设值列:
<!ELEMENT set (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*> 复制代码
他也是 trim
元素的一种直观体现,在实现上 set
元素也是作为 trim
元素的子类工作的.
一个 set
元素的配置基本等同于下面的 trim
元素配置:
<trim prefix="SET" prefixOverrides="," suffixOverrides=","> </trim> 复制代码
总结:
foreach
元素可以处理所有可迭代的对象( List
, Set
, Map
,数组).
<!ELEMENT foreach (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*> <!ATTLIST foreach collection CDATA #REQUIRED item CDATA #IMPLIED index CDATA #IMPLIED open CDATA #IMPLIED close CDATA #IMPLIED separator CDATA #IMPLIED > 复制代码
foreach
元素有 6
个属性定义,其中 collection
属性表示需要被处理的 可迭代的参数属性名称
, item
表示当前被处理的集合元素, index
表示当前被处理的集合元素的索引( 集合:索引下标
, Map:Key值
).
除此之外, open
属性用于配置整个集合的前缀, close
用于配置整个集合的后缀, separator
则用于配置每个集合参数之间的连接符.
foreach
元素最常见的应用场景是处理 in
语句,假设,我们需要获取名称在指定集合中的所有用户信息,这时候我们就可以使用 foreach
元素来简化我的代码:
UserMapper.java
:
public interface UserMapper { List<User> selectUser(@Param("names") List<String> names); } 复制代码
UserMapper.xml
:
<select id="selectUser" resultType="org.apache.learning.dynamic_sql.User"> SELECT * FROM USER <where> <if test="names !=null and names.size >0"> NAME IN <foreach collection="names" item="name" separator="," open="(" close=")" index=""> #{name} </foreach> </if> </where> </select> 复制代码
在 UserMapper.xml
文件中,我们通过 where
和 if
元素优化了我们的配置.
单元测试类:
public class CollectionDynamicSqlTest extends BaseDynamicSqlTest { @Override protected void addMappers(Configuration configuration) { configuration.addMapper(UserMapper.class); } @Test public void selectUserTest() { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); userMapper.selectUser(Arrays.asList("Panda", "panda")); userMapper.selectUser(Collections.emptyList()); } } 复制代码
在单元测试 selectUserTest()
方法中,两次调用 selectUser()
方法执行的 SQL
语句分别是:
SELECT * FROM USER WHERE NAME IN ( ? , ? ) SELECT * FROM USER
我们回头看一下 foreach
配置和生成sql的关系(性质相同的数据使用了相同的颜色进行标注):
最后,一张图总结一下 foreach
元素:
choose
元素有点像 java
语法中的 switch
语句,他可以在运行时根据上下文动态的选择需要使用的 SQL
语句.
<!ELEMENT choose (when* , otherwise?)> 复制代码
choose
元素有两个子元素定义: when
和 otherwise
;
<!ELEMENT when (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*> <!ATTLIST when test CDATA #REQUIRED > <!ELEMENT otherwise (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*> 复制代码
其中 when
元素有些类似于 switch
语句中的 case
关键字,他有一个 test
属性,该属性的取值是一个 OGNL
表达式,用于指定一个匹配条件,一个 choose
元素下可以存在多个 when
元素.
otherwise
元素有些类似于 switch
语句中的 default
关键字,当所有的 when
条件都不满足时,将会使用 otherwise
配置的 SQL
,一个 choose
元素最多可以拥有一个 otherwise
元素.
在运行时, choose
元素的子元素配置最多只有一个会生效.
现在我们有一个不太合理的需求,当我们获取用户数据时,如果传入了用户 id
就根据用户 id
进行查找,传入了用户 name
就按 name
查找,如果二者都没传,那就按照 gender
来查找,对应的映射配置是:
<select id="selectUser" resultType="org.apache.learning.dynamic_sql.User"> SELECT * FROM USER <where> <choose> <when test="id != null"> id = #{id} </when> <when test="name != null"> name=#{name} </when> <otherwise> gender=#{gender} </otherwise> </choose> </where> </select> 复制代码
提供一个单元测试;
@Test public void selectUserTest() { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = new User(); user.setId("38400000-8cf0-11bd-b23e-10b96e4ef00d"); user.setName("Panda"); user.setGender("男"); userMapper.selectUser(user); user.setId(null); userMapper.selectUser(user); user.setName(null); userMapper.selectUser(user); } 复制代码
上述单元测试,将会依次执行下列 SQL
:
SELECT * FROM USER WHERE id = ? SELECT * FROM USER WHERE name=? SELECT * FROM USER WHERE gender=?
最后,一张图总结一下 choose
元素:
if
元素和 when
元素有些类似, if
元素也有一个必填的 test
属性用来指定需要满足的条件,在运行时, mybatis
将会根据 if
元素 test
属性的配置,来决定 if
元素下的 SQL
是否生效.
在介绍 trim
元素时,我们已经使用过了 if
元素,鉴于 if
元素比较简单,因此这里就不再重复提供示例了.
最后一张图总结一下 if
元素:
bind
元素可以动态的创建一个参数配置,并绑定到 OGNL
运行上下文中,利用这一特性,我们可以做很多事情.
比如:
<select id="selectUser" resultType="org.apache.learning.dynamic_sql.User"> <bind name="namePattern" value="'%'+ name + '%'"/> SELECT * FROM USER WHERE name LIKE #{namePattern}; </select> 复制代码
在名为 selectUser
的映射声明配置中,我们为 name
参数前后包装上了 %
符号,并动态赋值给 namePattern
属性,之后在查询语句中,我们使用了 namePattern
属性.
编写一个单元测试使用该配置:
@Test public void selectUserTest() { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); log.debug("获取到的用户数据为:{}", userMapper.selectUser("an")); } 复制代码
成功得到运行日志(关键):
...省略... DEBUG [main] - ==> Preparing: SELECT * FROM USER WHERE name LIKE ?; DEBUG [main] - ==> Parameters: %an%(String) DEBUG [main] - <== Total: 1 DEBUG [main] - 获取到的用户数据为:[User(id=38400000-8cf0-11bd-b23e-10b96e4ef00d, name=Panda, gender=男)] ...省略... 复制代码
分析日志,我们可以发现,执行 SELECT * FROM USER WHERE name LIKE ?
语句时,使用的参数是 %an%
,这证明我们的 bind
元素按照预期执行了.
最后,一张图总结一下 bind
元素:
经过上面的学习,我们就了解了 mybais
的 8
个动态参数,后面的文章我们将继续回到配置文件的解析过程中去,在后续,我们将会逐渐了解到这 8
个动态参数更详细的用法以及实现.
学习很枯燥,但是总是有收获!
加油!
附上完整的思维导图: