MyBatis动态SQL:OGNL驱动的SQL组装艺术

 更新时间:2026年06月18日 12:00:37   作者:C137的本贾尼  
MyBatis动态SQL通过if、where、set等等标签简化SQL拼接,提升代码可读性和维护性,OGNL表达式解析让条件判断更灵活,这篇文章给大家介绍MyBatis动态SQL:OGNL驱动的SQL组装艺术,感兴趣的朋友一起看看吧

如果你完整写过几个 MyBatis 的 XML 映射文件,你大概会遇到这样一个场景:一个查询接口,前端可能传姓名、可能传年龄范围、可能什么都不传——你要根据这些条件动态决定 SQL 的 WHERE 子句长什么样。

用 Java 代码拼接 SQL 字符串,是个非常痛苦的事情。空格多了少了、AND 和 OR 的位置对不对、最后一项后面有没有多余的逗号——这些细节足以让一个下午变得无比漫长。如果你有 JPA 或 Hibernate 的使用经验,你可能知道它们用 Criteria API 来解决这个问题。而 MyBatis 的方式是:在 XML 里写标签,让 SQL 自己“长”出来

这一篇,我们把 MyBatis 动态 SQL 的核心标签和底层原理讲清楚。

学习目标

  • 理解动态 SQL 的诞生背景——拼接 SQL 是“程序员最痛苦的经历之一”
  • 掌握核心动态标签的用法(ifwheresettrimforeachchoose
  • 理解 MyBatis 动态 SQL 的底层原理——OGNL 表达式解析
  • 掌握 <sql><include> 实现 SQL 片段的复用

正文

一、为什么需要动态 SQL

先看一个典型的场景:用户列表查询,支持按用户名模糊搜索、按年龄范围筛选、按性别过滤。前端可能传任意组合的参数,也可能什么都不传。

如果用 JdbcTemplate 或原生 JDBC,你的代码大概是这样的:

StringBuilder sql = new StringBuilder("SELECT * FROM users WHERE 1=1");
List<Object> params = new ArrayList<>();
if (username != null && !username.isEmpty()) {
    sql.append(" AND username LIKE ?");
    params.add("%" + username + "%");
}
if (minAge != null) {
    sql.append(" AND age >= ?");
    params.add(minAge);
}
if (maxAge != null) {
    sql.append(" AND age <= ?");
    params.add(maxAge);
}
if (gender != null) {
    sql.append(" AND gender = ?");
    params.add(gender);
}
// 还要处理排序、分页...

这段代码的问题很明显:

  • SQL 结构和 Java 代码混在一起,可读性差
  • 每增加一个查询条件,就要改 Java 代码、重新编译
  • 字符串拼接容易出错——少个空格、多个 AND,运行时才发现
  • WHERE 1=1 这种写法虽然能解决问题,但总让人感觉不够优雅

MyBatis 的动态 SQL,就是把“根据条件拼接 SQL”这件事从 Java 代码挪到了 XML 里,用标签来表达条件逻辑。XML 里的 SQL 更接近真实的 SQL 语法,可读性更好,维护起来也更直观。

MyBatis 官方文档对动态 SQL 的定位说得很直接:“如果你用过 JDBC 或其他类似框架,你就会理解有条件地拼接 SQL 字符串有多么痛苦——要确保不忘掉空格,或不要省略列名列表末尾的逗号。”

二、核心标签详解

MyBatis 动态 SQL 的核心标签有四个:ifchoose (when, otherwise)trim (where, set)foreach。我们逐一来看。

1. if:最基础的条件判断

if 是最常用的标签,用法和编程语言里的 if 类似——条件成立时,标签内的 SQL 片段才会被加入最终语句。

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

如果 titlenull,第一个 if 不会生成任何内容;如果 title 有值,AND title like ? 就会被拼接到 SQL 中。

关键点test 属性的值是一个 OGNL 表达式,返回值必须是布尔类型。author != null and author.name != null 这种链式访问是 OGNL 的对象导航能力——你可以一路点下去访问嵌套对象的属性。

2. choose / when / otherwise:多路选择

if 是“每个条件独立判断,符合条件的都加上”。但有时候你需要的是“从多个候选中选一个”——类似 Java 的 switch 语句。

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

逻辑是:先判断 title,有值就用 title 条件;否则判断 author,有值就用 author 条件;如果都没有,就用 featured = 1 作为兜底。choose 只取第一个匹配的 when,匹配后不再继续判断后面的条件。

3. where / set:解决“多余的连接词”

if 有一个经典的问题:如果所有条件都不满足,WHERE 后面直接跟了个空,SQL 就语法错误了

<select id="findBlog" resultType="Blog">
    SELECT * FROM BLOG
    WHERE
    <if test="state != null">
        state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
</select>

如果 statetitle 都是 null,生成的 SQL 是 SELECT * FROM BLOG WHERE ——语法错误。

更隐蔽的问题:如果 statenulltitle 有值,生成的 SQL 是 SELECT * FROM BLOG WHERE AND title like ? ——多了一个 AND

where 标签解决的就是这两个问题:

<select id="findBlog" resultType="Blog">
    SELECT * FROM BLOG
    <where>
        <if test="state != null">
            state = #{state}
        </if>
        <if test="title != null">
            AND title like #{title}
        </if>
    </where>
</select>

<where> 的行为很智能:

  • 只有内部至少有一个条件成立时,才会输出 WHERE 关键字
  • 如果生成的 SQL 以 ANDOR 开头,会自动去掉

set 标签做类似的事情,但针对 UPDATE 语句:

<update id="updateUser">
    UPDATE users
    <set>
        <if test="username != null">username = #{username},</if>
        <if test="email != null">email = #{email},</if>
        <if test="age != null">age = #{age},</if>
    </set>
    WHERE id = #{id}
</update>

<set> 会动态添加 SET 关键字,并自动去掉最后一个字段后面多余的逗号。

4. trim:更灵活的定制

whereset 实际上是 trim 的特例。trim 提供了更精细的控制:

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

prefix 表示在内容前添加什么,prefixOverrides 表示移除内容开头的哪些字符串。where 等价于 prefix="WHERE" prefixOverrides="AND |OR "

同理,set 等价于 prefix="SET" suffixOverrides=","

当你需要更复杂的拼接逻辑时(比如在 INSERT 中动态处理列名和值),trim 就派上用场了。

5. foreach:处理集合遍历

这是动态 SQL 里另一个高频使用的标签,主要用在两个场景:IN 查询批量插入

IN 查询

<select id="findByIds" resultType="User">
    SELECT * FROM users
    WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

传入 ids = [1, 2, 3],生成的 SQL 是 SELECT * FROM users WHERE id IN (1, 2, 3)

foreach 的属性含义:

  • collection:要遍历的集合名称(后面会详细讲怎么传参)
  • item:每次迭代时当前元素的变量名
  • open:循环开始前添加的字符串
  • close:循环结束后添加的字符串
  • separator:元素之间的分隔符

批量插入

<insert id="batchInsert">
    INSERT INTO users (username, email, age) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.username}, #{user.email}, #{user.age})
    </foreach>
</insert>

6. bind:变量绑定

bind 用于在 OGNL 表达式中创建一个新变量,在 SQL 中引用。常见于模糊查询:

<select id="findByUsername" resultType="User">
    <bind name="pattern" value="'%' + username + '%'"/>
    SELECT * FROM users WHERE username LIKE #{pattern}
</select>

这样就不用每次在 Java 代码里拼接 % 了。

7. sql + include:SQL 片段复用

如果你发现多个查询语句中重复出现相同的字段列表或相同的 WHERE 条件,可以用 <sql> 定义片段,用 <include> 引用。

<!-- 定义字段列表 -->
<sql id="userColumns">
    id, username, email, age, create_time
</sql>
<!-- 定义通用的 WHERE 条件 -->
<sql id="userWhere">
    <where>
        <if test="username != null">
            AND username LIKE CONCAT('%', #{username}, '%')
        </if>
        <if test="email != null">
            AND email = #{email}
        </if>
    </where>
</sql>
<!-- 使用 -->
<select id="findAll" resultType="User">
    SELECT <include refid="userColumns"/>
    FROM users
</select>
<select id="findByCondition" resultType="User">
    SELECT <include refid="userColumns"/>
    FROM users
    <include refid="userWhere"/>
</select>

这样,字段列表和条件逻辑只需维护一份,修改时所有引用的地方自动生效。

三、OGNL 原理浅析

动态 SQL 的“动态”二字,靠的是 OGNL(Object-Graph Navigation Language) 表达式引擎。

OGNL 是一种表达式语言,它的核心能力是:通过字符串表达式访问 Java 对象图中的任意属性、调用方法、进行逻辑运算

在 MyBatis 的动态 SQL 标签中,test 属性的值就是一个 OGNL 表达式。MyBatis 在解析 XML 时,会把 test 表达式交给 OGNL 引擎求值——结果是 true,标签内的 SQL 片段就保留;结果是 false,就丢弃。

OGNL 能做什么?

能力表达式示例说明
访问属性user.name获取 user 对象的 name 属性
访问嵌套属性user.address.city链式导航
调用方法name.startsWith('A')调用字符串的 startsWith 方法
逻辑运算age != null and age > 18与或非、比较运算
集合访问list[0]map['key']访问集合中的元素

动态 SQL 的解析时机

MyBatis 在启动时会把 XML 中的动态 SQL 解析成一棵 SqlNode 树,存放在 MappedStatement 中。真正执行时,DynamicSqlSource.getBoundSql() 方法会遍历这棵树:遇到静态文本直接拼接,遇到动态标签就用 OGNL 对参数求值,决定是否包含对应的 SQL 片段。

// DynamicSqlSource 的核心逻辑(简化)
public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);  // 遍历 SqlNode 树,动态生成 SQL
    // 解析 SQL 中的 #{}
    return sqlSourceParser.parse(context.getSql()).getBoundSql(parameterObject);
}

OGNL 的引入,让 MyBatis 的动态 SQL 有了极大的灵活性——你可以在 test 里写复杂的条件判断,甚至可以调用参数对象的方法。但灵活性也意味着责任:OGNL 表达式中不要包含来自用户输入的不可信内容,否则存在安全隐患。

代码示例

示例一:多条件查询(if + where)

这是一个用户管理系统的典型场景:支持按用户名模糊搜索、按性别筛选、按年龄范围查询。

Mapper 接口

package com.example.demo.mapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface UserMapper {
    /**
     * 多条件查询用户列表
     * @param username 用户名(模糊匹配)
     * @param gender 性别
     * @param minAge 最小年龄
     * @param maxAge 最大年龄
     */
    List<User> findUsers(@Param("username") String username,
                         @Param("gender") String gender,
                         @Param("minAge") Integer minAge,
                         @Param("maxAge") Integer maxAge);
}

XML 映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
    <select id="findUsers" resultType="com.example.demo.entity.User">
        SELECT id, username, email, gender, age, create_time
        FROM users
        <where>
            <if test="username != null and username != ''">
                AND username LIKE CONCAT('%', #{username}, '%')
            </if>
            <if test="gender != null and gender != ''">
                AND gender = #{gender}
            </if>
            <if test="minAge != null">
                AND age >= #{minAge}
            </if>
            <if test="maxAge != null">
                AND age <= #{maxAge}
            </if>
        </where>
        ORDER BY create_time DESC
    </select>
</mapper>

关键观察

  • 当所有参数都为 null 时,<where> 不输出任何内容,SQL 变为 SELECT ... FROM users ORDER BY create_time DESC
  • 当只有 username 有值时,<where> 输出 WHERE username LIKE CONCAT('%', ?, '%'),开头的 AND 被自动去掉
  • age &lt;= #{maxAge} 中的 &lt;= 是 XML 转义,因为 < 在 XML 中会被解析为标签开始符

示例二:批量删除(foreach)

批量删除是 foreach 最典型的使用场景。

Mapper 接口

void deleteByIds(@Param("ids") List<Integer> ids);

XML 映射文件

<delete id="deleteByIds">
    DELETE FROM users
    WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>

传入 ids = Arrays.asList(1, 2, 3, 5, 8),生成的 SQL 为:

DELETE FROM users WHERE id IN (1, 2, 3, 5, 8)

collection 属性的取值规则(这是新手最容易写错的地方):

参数类型collection说明
直接传入 Listlist默认名称
直接传入数组array默认名称
使用 @Param("ids")ids注解指定的名称,优先使用
单个对象中的集合属性属性名user.orderIds

新手错误 vs 正确姿势

错误表象根本原因正确姿势
<where> 生成的 SQL 开头多了一个 AND,查询结果不对<where> 内部的第一个 <if> 中写了 AND,但 <where> 只会去掉开头AND。如果第一个条件不成立、第二个条件成立,第二个条件的 AND 在开头,会被正确去掉所有 <if> 内部的连接词都写 ANDOR,让 <where> 自动处理第一个
foreachcollection 写错导致报错 There is no getter for property不理解参数类型对应的 collection 默认名称单参 Listlist,数组用 array;建议统一使用 @Param 显式指定名称
<set> 中所有条件都不满足,生成的 SQL 是 UPDATE users SET WHERE id = ?,语法错误未考虑所有更新字段都为 null 的边界情况在业务层校验,确保至少有一个字段要更新;或使用 <trim> 配合条件判断做兜底
OGNL 表达式中用 = 而不是 == 做相等判断混淆了 XML 属性和 Java 代码OGNL 表达式遵循 Java 语法:相等用 ==,字符串比较用 equals()==(字面量)

疑难深度追问

Q1:MyBatis 解析动态 SQL 是在哪个阶段完成的?

分两个阶段。第一阶段是启动时:MyBatis 解析 XML 文件,将动态标签(<if><where> 等)解析成 SqlNode 对象树,存储在 MappedStatement 中。第二阶段是执行时DynamicSqlSource.getBoundSql() 被调用,遍历 SqlNode 树,用 OGNL 对参数求值,动态生成最终的 SQL 字符串。

Q2:为什么动态 SQL 的标签中可以使用对象的嵌套属性(如 user.address.city)?

因为 OGNL 支持对象导航(Object Navigation)。当你在 test 中写 user.address.city 时,OGNL 会从传入的参数对象开始,依次调用 getUser()getAddress()getCity() 来获取值。如果中间某个属性为 null,OGNL 会返回 null 而不会抛出 NullPointerException——这让条件判断更安全。

Q3:<trim> 的四个属性分别控制什么?能否用 <trim> 替代 <where><set>

  • prefix:在内容前面添加的字符串
  • suffix:在内容后面添加的字符串
  • prefixOverrides:从内容开头移除的字符串列表(用 | 分隔)
  • suffixOverrides:从内容结尾移除的字符串列表

<trim prefix="WHERE" prefixOverrides="AND |OR "> 完全等价于 <where><trim prefix="SET" suffixOverrides=","> 完全等价于 <set>。所以 trim 可以替代 whereset,但反过来不行——trim 更灵活,而 whereset 更语义化,代码可读性更好。

思考与延伸

  • 动手验证:在 Spring Boot 项目中开启 MyBatis 的 SQL 日志(logging.level.com.example.demo.mapper=DEBUG),分别调用 findUsers 方法传入不同的参数组合,观察最终生成的 SQL 语句,验证 <where>AND 的处理是否符合预期。
  • 思考题:如果需要支持“姓名包含 A 或 B”这样的 OR 条件,用动态 SQL 怎么实现?提示:<if> 中可以用 OGNL 表达式组合多个条件。
  • 延伸阅读:MyBatis 官方文档的“Dynamic SQL”章节对每个标签都有详细的说明和示例。另外,Apache Commons 的 OGNL 官方语言指南对 OGNL 的语法有完整介绍——如果你需要在 test 中写更复杂的表达式,这份文档会很有帮助。

参考与延伸阅读

  • MyBatis. MyBatis 3 | Dynamic SQL. mybatis.org, 2025-01-02
  • Apache Commons. OGNL Language Guide. commons.apache.org
  • 阿里云开发者社区. MyBatis动态SQL解析原理. 阿里云开发者社区, 2023-07-23
  • 腾讯云. MyBatis 动态 SQL 为什么这么灵活?背后靠的是 OGNL. 腾讯云, 2025-12-27
  • MySql教程网. OGNL 表达式是如何助力 MyBatis 实现动态 SQL 的?. mysql360.com, 2025-01-18

到此这篇关于MyBatis动态SQL:OGNL驱动的SQL组装艺术的文章就介绍到这了,更多相关MyBatis动态SQL内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • spring redis 如何实现模糊查找key

    spring redis 如何实现模糊查找key

    这篇文章主要介绍了spring redis 如何实现模糊查找key的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • Java依赖包漏洞检测命令详解

    Java依赖包漏洞检测命令详解

    这篇文章主要介绍了Java依赖包漏洞检测命令,你可以在修复漏洞后快速进行依赖项的安全检查,并生成新的报告,而不需要等待 CVE 数据库的下载和更新,这样可以显著提高扫描的效率,需要的朋友可以参考下
    2024-11-11
  • Java并发编程Semaphore计数信号量详解

    Java并发编程Semaphore计数信号量详解

    这篇文章主要介绍了Java并发编程Semaphore计数信号量详解,具有一定参考价值,需要的朋友可以了解下。
    2017-10-10
  • spring事务@Transactional失效原因及解决办法小结

    spring事务@Transactional失效原因及解决办法小结

    今天就跟大家聊聊有关spring中@Transactional失效原因及解决办法小结,主要从三个方面考虑,具有一定的参考价值,感兴趣的可以了解一下
    2023-08-08
  • Spring @ExceptionHandler注解统一异常处理和获取方法名

    Spring @ExceptionHandler注解统一异常处理和获取方法名

    这篇文章主要介绍了Spring注解之@ExceptionHandler 统一异常处理和获取方法名,在实际项目中,合理使用@ExceptionHandler能够提高代码的可维护性和用户体验,通过本文的解析和实践,读者可以更好地理解和掌握@ExceptionHandler的用法和原理
    2023-09-09
  • 利用Spring Validation实现输入验证功能

    利用Spring Validation实现输入验证功能

    这篇文章主要给大家介绍了如何利用Spring Validation完美的实现输入验证功能,文中有详细的代码示例,具有一定的参考价值,感兴趣的朋友可以借鉴一下
    2023-06-06
  • Java模拟栈和队列数据结构的基本示例讲解

    Java模拟栈和队列数据结构的基本示例讲解

    这篇文章主要介绍了Java模拟栈和队列数据结构的基本示例,栈的后进先出和队列的先进先出是数据结构中最基础的知识,本文则又对Java实现栈和队列结构的方法进行了细分,需要的朋友可以参考下
    2016-04-04
  • Java中反射的一个简单使用

    Java中反射的一个简单使用

    一直感觉Java的反射机制很强大,JAVA反射技术在平时我们的开发中虽然很少会用到,但在我们所使用的框架源码中是经常会用到的。这篇文中就给大家介绍了关于Java中反射的一个简单使用,有需要的朋友们下面来一起看看吧。
    2016-11-11
  • java  线程详解及线程与进程的区别

    java 线程详解及线程与进程的区别

    这篇文章主要介绍了java 线程详解及线程与进程的区别的相关资料,网上关于java 线程的资料很多,对于进程的资料很是,这里就整理下,需要的朋友可以参考下
    2017-01-01
  • MyBatis下SQL注入攻击的3种方式

    MyBatis下SQL注入攻击的3种方式

    SQL注入漏洞作为WEB安全的最常见的漏洞之一,本文希望通过Mybatis框架使用不当导致的SQL注入问题为例,能够抛砖引玉给新手一些思路。感兴趣的可以了解一下
    2021-07-07

最新评论