MyBatis动态SQL:OGNL驱动的SQL组装艺术
如果你完整写过几个 MyBatis 的 XML 映射文件,你大概会遇到这样一个场景:一个查询接口,前端可能传姓名、可能传年龄范围、可能什么都不传——你要根据这些条件动态决定 SQL 的 WHERE 子句长什么样。
用 Java 代码拼接 SQL 字符串,是个非常痛苦的事情。空格多了少了、AND 和 OR 的位置对不对、最后一项后面有没有多余的逗号——这些细节足以让一个下午变得无比漫长。如果你有 JPA 或 Hibernate 的使用经验,你可能知道它们用 Criteria API 来解决这个问题。而 MyBatis 的方式是:在 XML 里写标签,让 SQL 自己“长”出来。
这一篇,我们把 MyBatis 动态 SQL 的核心标签和底层原理讲清楚。
学习目标
- 理解动态 SQL 的诞生背景——拼接 SQL 是“程序员最痛苦的经历之一”
- 掌握核心动态标签的用法(
if、where、set、trim、foreach、choose) - 理解 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 的核心标签有四个:if、choose (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>如果 title 为 null,第一个 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>如果 state 和 title 都是 null,生成的 SQL 是 SELECT * FROM BLOG WHERE ——语法错误。
更隐蔽的问题:如果 state 是 null、title 有值,生成的 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 以
AND或OR开头,会自动去掉
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:更灵活的定制
where 和 set 实际上是 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 <= #{maxAge}中的<=是 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 值 | 说明 |
|---|---|---|
直接传入 List | list | 默认名称 |
| 直接传入数组 | array | 默认名称 |
使用 @Param("ids") | ids | 注解指定的名称,优先使用 |
| 单个对象中的集合属性 | 属性名 | 如 user.orderIds |
新手错误 vs 正确姿势
| 错误表象 | 根本原因 | 正确姿势 |
|---|---|---|
<where> 生成的 SQL 开头多了一个 AND,查询结果不对 | 在 <where> 内部的第一个 <if> 中写了 AND,但 <where> 只会去掉开头的 AND。如果第一个条件不成立、第二个条件成立,第二个条件的 AND 在开头,会被正确去掉 | 所有 <if> 内部的连接词都写 AND 或 OR,让 <where> 自动处理第一个 |
foreach 的 collection 写错导致报错 There is no getter for property | 不理解参数类型对应的 collection 默认名称 | 单参 List 用 list,数组用 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 可以替代 where 和 set,但反过来不行——trim 更灵活,而 where 和 set 更语义化,代码可读性更好。
思考与延伸
- 动手验证:在 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事务@Transactional失效原因及解决办法小结
今天就跟大家聊聊有关spring中@Transactional失效原因及解决办法小结,主要从三个方面考虑,具有一定的参考价值,感兴趣的可以了解一下2023-08-08
Spring @ExceptionHandler注解统一异常处理和获取方法名
这篇文章主要介绍了Spring注解之@ExceptionHandler 统一异常处理和获取方法名,在实际项目中,合理使用@ExceptionHandler能够提高代码的可维护性和用户体验,通过本文的解析和实践,读者可以更好地理解和掌握@ExceptionHandler的用法和原理2023-09-09


最新评论