转载

Mybatis源码之美:3.9.探究动态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元素:

Mybatis源码之美:3.9.探究动态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>
复制代码

名为 selectIdselect 元素在引用名为 sqlIdsql 元素时,既可以通过 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 元素:

Mybatis源码之美:3.9.探究动态SQL参数

动态处理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 元素的 suffixprefix 相似, 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 属性用于配置需要被移除的后缀字符串.

还是一样,用一张总结一下:

Mybatis源码之美:3.9.探究动态SQL参数

用于配置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 元素:

Mybatis源码之美:3.9.探究动态SQL参数

配置更新语句中设值列的元素--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>
复制代码

总结:

Mybatis源码之美:3.9.探究动态SQL参数

用于遍历处理集合参数的元素--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 文件中,我们通过 whereif 元素优化了我们的配置.

单元测试类:

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的关系(性质相同的数据使用了相同的颜色进行标注):

Mybatis源码之美:3.9.探究动态SQL参数

最后,一张图总结一下 foreach 元素:

Mybatis源码之美:3.9.探究动态SQL参数

根据运行上下文,动态选择SQL代码块的元素--choose

choose 元素有点像 java 语法中的 switch 语句,他可以在运行时根据上下文动态的选择需要使用的 SQL 语句.

<!ELEMENT choose (when* , otherwise?)>
复制代码

choose 元素有两个子元素定义: whenotherwise ;

<!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 元素:

Mybatis源码之美:3.9.探究动态SQL参数

根据运行上下文决定指定SQL配置是否生效的元素--if

if 元素和 when 元素有些类似, if 元素也有一个必填的 test 属性用来指定需要满足的条件,在运行时, mybatis 将会根据 if 元素 test 属性的配置,来决定 if 元素下的 SQL 是否生效.

在介绍 trim 元素时,我们已经使用过了 if 元素,鉴于 if 元素比较简单,因此这里就不再重复提供示例了.

最后一张图总结一下 if 元素:

Mybatis源码之美:3.9.探究动态SQL参数

动态为运行上下文添加参数配置的元素--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 元素:

Mybatis源码之美:3.9.探究动态SQL参数

总结

经过上面的学习,我们就了解了 mybais8 个动态参数,后面的文章我们将继续回到配置文件的解析过程中去,在后续,我们将会逐渐了解到这 8 个动态参数更详细的用法以及实现.

学习很枯燥,但是总是有收获!

加油!

附上完整的思维导图:

Mybatis源码之美:3.9.探究动态SQL参数
原文  https://juejin.im/post/5eba6e456fb9a04359029697
正文到此结束
Loading...