Mybatis之动态SQL

一、概述

MyBatis的强大特性之一便是它的动态SQL。如果有使用JDBC或其他类似框架的经验,就能体会到根据不同条件拼接SQL语句有多么痛苦。拼接的时候要确保不能忘了必要的空格,还要注意省掉列名列表最后的逗号。利用动态SQL这一特性可以彻底摆脱这种痛苦。

通常使用动态SQL不可能是独立的一部分,MyBatis当然使用一种强大的动态SQL语言来改进这种情形,这种语言可以被用在任意的SQL映射语句中。

动态SQL元素和使用JSTL或其他类似基于XML的文本处理器相似。在MyBatis之前的版本中,有很多的元素需要来了解。MyBatis3大大提升了它们,现在用不到原先一半的元素就可以了。MyBatis采用功能强大的基于OGNL的表达式来消除其他元素。

二、原理

MyBatis动态SQL的底层核心是OGNL表达式解析+SQL动态拼接,流程如下:

  1. 执行SQL时,MyBatis通过OGNL表达式引擎解析动态标签中的条件(如test="title != null");
  2. SQL参数对象(POJOMap等)中提取对应属性值,计算表达式结果(true/false);
  3. 根据表达式结果,决定是否包含对应SQL片段,自动处理关键字(如WHERESET)和分隔符(逗号、AND/OR);
  4. 最终拼接出完整、合法的SQL语句,提交给数据库执行。

三、九大标签

3.1 条件判断:if

动态SQL通常要做的事情是有条件地包含where子句的一部分。比如:

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

3.2 分支选择:choose + when + otherwise

有些时候,我们不想用到所有的条件语句,而只想从中择其一二。针对这种情况,MyBatis提供了choose元素,它有点像Java中的switch语句。

还是上面的例子,但是这次变为提供了"title"就按"title"查找,提供了"author"就按"author"查找,若两者都没有提供,就返回所有符合条件的BLOG(实际情况可能是由管理员按一定策略选出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.3 SQL结构优化:where + set + trim

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>

where元素知道只有在一个以上的if条件有值的情况下才去插入"WHERE"子句。而且,若最后的内容是"AND"或"OR"开头的,where元素也知道如何将他们去除。

如果where元素没有按正常套路出牌,我们还是可以通过自定义trim元素来定制我们想要的功能。比如,和where元素等价的自定义trim元素为:

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

prefixOverrides属性会忽略通过管道分隔的文本序列(注意此例中的空格也是必要的)。它带来的结果就是所有在prefixOverrides属性中指定的内容将被移除,并且插入prefix属性中指定的内容。

类似的用于动态更新语句的解决方案叫做setset元素可以被用于动态包含需要更新的列,而舍去其他的。比如:

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

这里,set元素会动态前置SET关键字,同时也会消除无关的逗号,因为用了条件语句之后很可能就会在生成的赋值语句的后面留下这些逗号。

若你对等价的自定义trim元素的样子感兴趣,那这就应该是它的真面目:

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

注意这里我们忽略的是后缀中的值,而又一次附加了前缀中的值。

3.4 集合遍历:foreach

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

foreach元素的功能是非常强大的,它允许你指定一个集合,声明可以用在元素体内的集合项和索引变量。它也允许你指定开闭匹配的字符串以及在迭代中间放置分隔符。这个元素是很智能的,因此它不会偶然地附加多余的分隔符。

注意:你可以将一个List实例或者数组作为参数对象传给MyBatis,当你这么做的时候,MyBatis会自动将它包装在一个Map中并以名称为键。List实例将会以"list"作为键,而数组实例的键将是"array"。

3.5 表达式变量绑定:bind

bind元素可以从OGNL表达式中创建一个变量并将其绑定到上下文。比如:

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

四、多数据库支持

一个配置了"databaseId"变量的databaseIdProvider对于动态代码来说是可用的,这样就可以根据不同的数据库厂商构建特定的语句。比如下面的例子:

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

五、可插拔的脚本语言

MyBatis从3.2开始支持可插拔的脚本语言,因此你可以在插入一种语言的驱动(language driver)之后来写基于这种语言的动态SQL查询。

可以通过实现下面接口的方式来插入一种语言:

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

一旦有了自定义的语言驱动,你就可以在mybatis-config.xml文件中将它设置为默认语言:

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

除了设置默认语言,你也可以针对特殊的语句指定特定语言,这可以通过如下的lang属性来完成:

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

或者在你正在使用的映射中加上注解@Lang来完成:

public interface Mapper {
    @Lang(MyLanguageDriver.class)
    @Select("SELECT * FROM BLOG")
    List selectBlog();
}

注意:可以将Apache Velocity作为动态语言来使用,更多细节请参考MyBatis-Velocity项目。

你前面看到的所有xml标签都是默认MyBatis语言提供的,它是由别名为xml语言驱动器org.apache.ibatis.scripting.xmltags.XmlLanguageDriver驱动的。

六、总结

  1. 条件判断必做非空校验:避免null或空字符串导致的无效SQL(如test="title != null and title != ''");
  2. 优先使用:替代手动拼接WHERE和SET,减少语法错误;
  3. 批量操作用:注意collection参数名(List/Array需对应正确别名);
  4. 多数据库兼容用databaseId:避免写死数据库特有语法;
  5. 复杂表达式用:兼容多数据库,复用逻辑。

通过以上标签和特性,MyBatis动态SQL可轻松应对各类复杂场景,既保证了SQL的灵活性,又兼顾了开发效率和可维护性。

posted @ 2025-06-29 16:07  夏尔_717  阅读(74)  评论(0)    收藏  举报