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

1,327 阅读10分钟

探究动态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>

名为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元素:

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元素的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属性用于配置需要被移除的后缀字符串.

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

trim元素

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

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>

总结:

set

用于遍历处理集合参数的元素--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的关系(性质相同的数据使用了相同的颜色进行标注):

foreach元素

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

foreach元素

根据运行上下文,动态选择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元素:

choose

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

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

在介绍trim元素时,我们已经使用过了if元素,鉴于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元素:

bind

总结

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

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

加油!

附上完整的思维导图:

动态SQL元素