探究动态sql参数
在前面的文章中,我们了解了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
.
这些元素的用法和作用各不相同,下面我们就按照顺序对上面的这些元素依次进行了解.
用于引入SQL代码块的元素--include
在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
元素:
动态处理SQL字符串内容的元素--trim
在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
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
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
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
元素:
根据运行上下文,动态选择SQL代码块的元素--choose
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
元素:
根据运行上下文决定指定SQL配置是否生效的元素--if
if
元素和when
元素有些类似,if
元素也有一个必填的test
属性用来指定需要满足的条件,在运行时,mybatis
将会根据if
元素test
属性的配置,来决定if
元素下的SQL
是否生效.
在介绍trim
元素时,我们已经使用过了if
元素,鉴于if
元素比较简单,因此这里就不再重复提供示例了.
最后一张图总结一下if
元素:
动态为运行上下文添加参数配置的元素--bind
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
个动态参数更详细的用法以及实现.
学习很枯燥,但是总是有收获!
加油!
附上完整的思维导图: