Skip to content

MyBatis 动态 SQL 深度解析

面试官:MyBatis 的动态 SQL 你用过哪些标签?

:常用的有 ifchoosewheresetforeachtrimbindsqlinclude

面试官where 标签和直接写 WHERE 1=1 有什么区别?

where 标签是智能标签,会自动处理多余的 AND/OR 前缀,并且只有在有子元素输出内容时才生成 WHERE 关键字。而 WHERE 1=1 是一种妥协写法,虽然性能损耗微小(数据库优化器会优化掉),但不够优雅,且无法处理所有边界情况。

面试官:能说说 MyBatis 动态 SQL 的底层原理吗?它是怎么解析这些标签的?

这个追问直击底层——能说出 SqlNode 组成的 AST 树、OGNL 表达式求值、DynamicSqlSource vs RawSqlSource 的候选人,才算真正掌握动态 SQL 的精髓。


Q1:if 标签和 choose 标签的区别?各自适用什么场景?必考

Section titled “Q1:if 标签和 choose 标签的区别?各自适用什么场景?”

核心区别if 是多条件独立判断,choose 是多条件单选(类似 switch-case)。

对比表

维度if 标签choose/when/otherwise 标签
逻辑关系多个条件独立判断,可多个同时成立多个条件互斥,只有一个分支生效
类比多个独立的 if 语句switch-case 语句
默认分支无默认分支<otherwise> 提供默认分支
适用场景多条件组合查询按优先级选择一种查询方式

代码示例

<!-- if 标签:多条件组合查询 -->
<select id="queryUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>
<!-- 生成的 SQL 示例 -->
<!-- 参数:{name: '张', age: 18} -->
SELECT * FROM user WHERE name LIKE '%张%' AND age = 18
<!-- 参数:{age: 18, status: 1} -->
SELECT * FROM user WHERE age = 18 AND status = 1
<!-- choose 标签:按优先级选择查询方式 -->
<select id="findUser" resultType="User">
SELECT * FROM user WHERE
<choose>
<when test="id != null">
id = #{id} <!-- 最高优先级:ID 查询 -->
</when>
<when test="phone != null">
phone = #{phone} <!-- 次优先级:手机号查询 -->
</when>
<when test="email != null">
email = #{email} <!-- 再次:邮箱查询 -->
</when>
<otherwise>
status = 1 <!-- 默认:查询所有启用用户 -->
</otherwise>
</choose>
</select>
<!-- 生成的 SQL 示例 -->
<!-- 参数:{id: 1, phone: '13800138000'} -->
SELECT * FROM user WHERE id = 1 <!-- 只用 id,忽略 phone -->
<!-- 参数:{phone: '13800138000'} -->
SELECT * FROM user WHERE phone = '13800138000'
<!-- 参数:{} -->
SELECT * FROM user WHERE status = 1 <!-- 默认分支 -->

实战场景对比

✅ if 适用场景:
- 多条件组合查询(商品筛选:品牌 + 价格区间 + 分类)
- 可选的过滤条件(用户列表:可选的状态、时间范围)
- 多个独立的条件可能同时生效
✅ choose 适用场景:
- 按优先级查询(先查 ID,再查手机号,再查邮箱)
- 互斥的查询策略(精确查询 vs 模糊查询)
- 需要默认兜底逻辑

Q2:where 标签的工作原理是什么?它是怎么智能处理 AND/OR 的?必考

Section titled “Q2:where 标签的工作原理是什么?它是怎么智能处理 AND/OR 的?”

where 标签解决的问题:条件拼接时多余的 AND/OR 前缀。

问题场景

<!-- ❌ 错误示例:第一个 if 不成立时,SQL 变成 WHERE AND age=18 -->
SELECT * FROM user WHERE
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
<!-- 参数:{age: 18} -->
<!-- 生成的 SQL:SELECT * FROM user WHERE AND age = 18(语法错误!) -->

解决方案对比

方案实现优点缺点
WHERE 1=1WHERE 1=1 <if>AND ...</if>简单粗暴不够优雅,多余条件
where 标签<where><if>AND ...</if></where>智能处理,优雅需要 MyBatis 支持
trim 标签<trim prefix="WHERE" prefixOverrides="AND |OR ">灵活可控配置稍复杂

where 标签的行为规则

<select id="queryUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
<if test="status != null">OR status = #{status}</if>
</where>
</select>
<!-- 规则1:子元素全部没有输出 → 不生成 WHERE 关键字 -->
<!-- 参数:{} -->
<!-- 生成:SELECT * FROM user -->
<!-- 规则2:子元素输出以 AND/OR 开头 → 自动去掉前缀 -->
<!-- 参数:{age: 18} -->
<!-- 原始:WHERE AND age = 18 -->
<!-- 处理:WHERE age = 18(自动去掉 AND) -->
<!-- 规则3:子元素输出不以 AND/OR 开头 → 保留原样 -->
<!-- 参数:{name: '张三', age: 18} -->
<!-- 原始:WHERE AND name = '张三' AND age = 18 -->
<!-- 处理:WHERE name = '张三' AND age = 18(只去掉第一个 AND) -->

where 标签等价的 trim 写法

<!-- where 标签(语法糖) -->
<where>
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</where>
<!-- 等价的 trim 标签 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</trim>

trim 标签参数详解

参数作用示例
prefix在内容前添加前缀prefix="WHERE" → 内容前加 WHERE
suffix在内容后添加后缀suffix=")" → 内容后加 )
prefixOverrides去掉内容前缀`prefixOverrides=“AND
suffixOverrides去掉内容后缀suffixOverrides="," → 去掉结尾的逗号

本质一句话where 标签是 trim prefix="WHERE" prefixOverrides="AND |OR " 的语法糖,自动处理 WHERE 关键字生成和多余的 AND/OR 前缀。


Q3:set 标签和 trim 标签的关系?UPDATE 语句如何动态更新?高频

Section titled “Q3:set 标签和 trim 标签的关系?UPDATE 语句如何动态更新?”

set 标签:UPDATE 语句专用,智能处理多余的逗号。

问题场景

<!-- ❌ 错误示例:最后一个 if 不成立时,SQL 变成 SET name='张三', WHERE ... -->
<update id="updateUser">
UPDATE user SET
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
WHERE id = #{id}
</update>
<!-- 参数:{id: 1, name: '张三'} -->
<!-- 生成的 SQL:UPDATE user SET name = '张三', WHERE id = 1(语法错误!) -->

解决方案

<!-- ✅ set 标签(推荐) -->
<update id="updateUser">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
<if test="email != null">email = #{email},</if>
</set>
WHERE id = #{id}
</update>
<!-- ✅ 等价的 trim 标签 -->
<update id="updateUser">
UPDATE user
<trim prefix="SET" suffixOverrides=",">
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
<if test="email != null">email = #{email},</if>
</trim>
WHERE id = #{id}
</update>
<!-- 生成的 SQL 示例 -->
<!-- 参数:{id: 1, name: '张三', age: 18} -->
UPDATE user SET name = '张三', age = 18 WHERE id = 1
<!-- 参数:{id: 1, name: '张三'} -->
UPDATE user SET name = '张三' WHERE id = 1 <!-- 自动去掉逗号 -->

set 标签的行为

  1. 如果子元素有输出 → 生成 SET 关键字
  2. 自动去掉结尾的逗号(,)
  3. 如果子元素没有输出 → 不生成 SET 关键字(但 SQL 会出错,因为没有 SET)

实战案例:动态更新部分字段

// Java 代码
public void updateUserSelective() {
User user = new User();
user.setId(1L);
user.setName("张三");
// age、email 不设置,保持数据库原值
userMapper.updateUser(user);
}
<!-- Mapper XML -->
<update id="updateUser">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
<if test="email != null">email = #{email},</if>
update_time = NOW(), <!-- 固定更新时间 -->
</set>
WHERE id = #{id}
</update>
<!-- 生成的 SQL -->
UPDATE user SET name = '张三', update_time = NOW() WHERE id = 1

Q4:foreach 标签如何实现批量操作?有哪些性能陷阱?必考

Section titled “Q4:foreach 标签如何实现批量操作?有哪些性能陷阱?”

foreach 标签核心属性

属性说明示例
collection集合参数名List 默认 list,数组默认 array,@Param 指定的名称
item每次迭代的变量名item="id"#{id}
index迭代索引List 是下标,Map 是 key
open整体包裹的开头open="(" → 以 ( 开头
close整体包裹的结尾close=")" → 以 ) 结尾
separator元素间分隔符separator="," → 元素间用逗号分隔

应用场景 1:批量查询(IN 查询)

<select id="selectByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- 参数:{ids: [1, 2, 3]} -->
<!-- 生成的 SQL:SELECT * FROM user WHERE id IN (1, 2, 3) -->

应用场景 2:批量插入

<insert id="batchInsert">
INSERT INTO user (name, age, email) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.email})
</foreach>
</insert>
<!-- 参数:{users: [{name:'张三',age:18}, {name:'李四',age:20}]} -->
<!-- 生成的 SQL -->
INSERT INTO user (name, age, email) VALUES
('张三', 18, 'zhangsan@example.com'),
('李四', 20, 'lisi@example.com')

应用场景 3:批量更新

<update id="batchUpdate">
<foreach collection="users" item="user" separator=";">
UPDATE user SET name = #{user.name}, age = #{user.age}
WHERE id = #{user.id}
</foreach>
</update>
<!-- 生成的 SQL(多条 SQL) -->
UPDATE user SET name = '张三', age = 18 WHERE id = 1;
UPDATE user SET name = '李四', age = 20 WHERE id = 2
<!-- 注意:需要在 JDBC URL 中开启多语句支持 -->
<!-- jdbc:mysql://localhost:3306/db?allowMultiQueries=true -->

性能陷阱与解决方案

陷阱问题解决方案
IN 查询参数过多MySQL 限制 SQL 长度(max_allowed_packet)分批查询,每批 500-1000 个 ID
批量插入数据量过大单条 SQL 参数过多,内存溢出分批插入,每批 500-1000 条
批量更新使用分号分隔需要开启 allowMultiQueries=true,有 SQL 注入风险使用 CASE WHEN 语法(单条 SQL)
foreach 嵌套循环性能极差,避免使用应用层预处理,传扁平化参数

性能优化示例:批量插入分批处理

// ❌ 错误:一次性插入 10000 条(可能内存溢出)
userMapper.batchInsert(users); // users.size() = 10000
// ✅ 正确:分批插入,每批 1000 条
public void batchInsertUsers(List<User> users) {
int batchSize = 1000;
for (int i = 0; i < users.size(); i += batchSize) {
int end = Math.min(i + batchSize, users.size());
List<User> batch = users.subList(i, end);
userMapper.batchInsert(batch); // 每次插入 1000 条
}
}

性能数据对比

批量插入方式10000 条耗时100000 条耗时说明
单条插入(循环)~8s~80s每次 SQL 预编译 + 执行
批量插入(1 次)~0.5s内存溢出单条 SQL,参数过多
批量插入(分批 1000)~0.6s~6s平衡性能与内存

Q5:MyBatis 中的 OGNL 表达式是什么?支持哪些写法?高频

Section titled “Q5:MyBatis 中的 OGNL 表达式是什么?支持哪些写法?”

OGNL(Object-Graph Navigation Language):MyBatis 在动态 SQL 中使用的表达式语言,用于条件判断和变量访问。

OGNL 常用语法

<!-- 1. 基本属性访问 -->
<if test="user != null">
AND name = #{user.name}
</if>
<!-- 2. 方法调用 -->
<if test="name != null and name.length() > 0">
AND name = #{name}
</if>
<if test="list != null and list.size() > 0">
AND id IN (...)
</if>
<!-- 3. 字符串比较(推荐用 ==) -->
<if test='status == "ACTIVE"'>
AND status = 'ACTIVE'
</if>
<if test="status == 'ACTIVE'">
AND status = 'ACTIVE'
</if>
<!-- ❌ 错误:使用 equals(OGNL 会把 'ACTIVE' 解析为变量名) -->
<if test="status.equals('ACTIVE')">
<!-- 可能报错:NoSuchPropertyException -->
</if>
<!-- 4. 数字比较 -->
<if test="age > 18">
AND is_adult = 1
</if>
<if test="age >= 18 and age <= 60">
AND age_group = 'adult'
</if>
<!-- 注意:XML 中需要转义 > 和 < -->
<if test="age &gt; 18"> <!-- > 转义为 &gt; -->
<if test="age &lt; 60"> <!-- < 转义为 &lt; -->
<!-- 或使用 CDATA -->
<if test="age > 18"><![CDATA[ AND is_adult = 1 ]]></if>
<!-- 5. 集合判断 -->
<if test="list != null and list.size() > 0">
AND id IN (...)
</if>
<!-- 6. 枚举比较 -->
<if test="status == @com.example.UserStatus@ACTIVE">
AND status = 'ACTIVE'
</if>
<!-- 7. instanceof 判断 -->
<if test="obj instanceof @java.lang.String@class">
AND name = #{obj}
</if>
<!-- 8. 调用静态方法 -->
<if test="@java.util.Arrays@asList(1,2,3).contains(id)">
AND id IN (1, 2, 3)
</if>
<!-- 9. 创建对象 -->
<if test="new java.util.Date().after(createTime)">
AND create_time < NOW()
</if>

常见陷阱与解决方案

陷阱错误写法正确写法原因
字符串比较用 equalstest="status.equals('ACTIVE')"test='status == "ACTIVE"'OGNL 会把 'ACTIVE' 解析为变量名
特殊字符未转义test="age > 18"test="age &gt; 18"XML 中 > 需要转义
空字符串判断test="name != ''"test="name != null and name != ''"先判空,再判空串
集合判空test="list != null"test="list != null and list.size() > 0"还需要判断 size
字符串包含单引号test="name == 'O\'Neil'"test='name == "O\'Neil"'单双引号嵌套

OGNL 内置对象

<!-- _parameter:方法参数(单个参数时) -->
<if test="_parameter != null">
AND id = #{_parameter}
</if>
<!-- _databaseId:数据库厂商标识(多数据库支持) -->
<if test="_databaseId == 'mysql'">
LIMIT 10
</if>
<if test="_databaseId == 'oracle'">
AND ROWNUM <= 10
</if>

Q6:MyBatis 是如何解析动态 SQL 的?底层是什么数据结构?高频

Section titled “Q6:MyBatis 是如何解析动态 SQL 的?底层是什么数据结构?”

核心原理:SqlNode 组成的 AST(抽象语法树)

解析流程(MyBatis 启动时):

┌──────────────────────────────────────────────────────────────┐
│ XML 解析阶段 │
│ XMLStatementBuilder.parseStatementNode() │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ LanguageDriver.createSqlSource() │
│ - 创建 SqlSource(SQL 源对象) │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ XMLScriptBuilder.parseScriptNode() │
│ - 解析 XML 节点,生成 SqlNode 树 │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 解析各 XML 节点,创建对应的 SqlNode │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ <if> → IfSqlNode │ │
│ │ <choose> → ChooseSqlNode │ │
│ │ <foreach> → ForEachSqlNode │ │
│ │ <where> → WhereSqlNode │ │
│ │ <set> → SetSqlNode │ │
│ │ <trim> → TrimSqlNode │ │
│ │ <bind> → VarDeclSqlNode │ │
│ │ 文本节点 → TextSqlNode / StaticTextSqlNode │ │
│ └────────────────────────────────────────────────────────┘ │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 组合成 MixedSqlNode(AST 树) │
│ - 所有 SqlNode 放入 List,形成混合节点 │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 包装为 DynamicSqlSource 或 RawSqlSource │
│ - 含动态标签 → DynamicSqlSource(运行时生成 SQL) │
│ - 纯静态 SQL → RawSqlSource(启动时生成 SQL) │
└──────────────────────────────────────────────────────────────┘

执行阶段(SQL 执行时):

┌──────────────────────────────────────────────────────────────┐
│ DynamicSqlSource.getBoundSql(parameterObject) │
│ - 获取 BoundSql(包含最终 SQL 和参数映射) │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 创建 DynamicContext │
│ - 包含参数对象、OGNL 上下文、StringBuilder(拼接 SQL) │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ MixedSqlNode.apply(context) │
│ - 遍历所有 SqlNode,调用 apply() 方法 │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 递归处理各 SqlNode │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ IfSqlNode: │ │
│ │ - 计算 OGNL 表达式(test 属性) │ │
│ │ - 条件为 true → 调用子节点的 apply() │ │
│ │ - 条件为 false → 跳过 │ │
│ │ │ │
│ │ ForEachSqlNode: │ │
│ │ - 遍历集合,为每个元素生成 SQL 片段 │ │
│ │ - 添加 open、close、separator │ │
│ │ │ │
│ │ TextSqlNode: │ │
│ │ - 处理 ${} 变量替换(OGNL 求值) │ │
│ │ - 将结果 append 到 StringBuilder │ │
│ │ │ │
│ │ StaticTextSqlNode: │ │
│ │ - 直接 append 文本到 StringBuilder │ │
│ └────────────────────────────────────────────────────────┘ │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 生成完整 SQL 字符串 │
│ String sql = context.getSql(); │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 返回 BoundSql │
│ - 包含最终 SQL + 参数映射 + 额外参数 │
└──────────────────────────────────────────────────────────────┘

SqlNode 接口与实现类

// SqlNode 接口
public interface SqlNode {
boolean apply(DynamicContext context);
}
// IfSqlNode 实现
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator; // OGNL 表达式求值器
private final String test; // test 表达式
private final SqlNode contents; // 子节点
@Override
public boolean apply(DynamicContext context) {
// 计算 OGNL 表达式
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 条件为 true,处理子节点
contents.apply(context);
return true;
}
return false; // 条件为 false,跳过
}
}
// TextSqlNode 实现(处理 ${})
public class TextSqlNode implements SqlNode {
private final String text;
@Override
public boolean apply(DynamicContext context) {
// 使用 GenericTokenParser 处理 ${} 占位符
GenericTokenParser parser = new GenericTokenParser("${", "}", content -> {
// OGNL 求值
Object value = OgnlCache.getValue(content, context.getBindings());
return value == null ? "" : value.toString(); // 字符串替换
});
context.appendSql(parser.parse(text));
return true;
}
}
// StaticTextSqlNode 实现(纯文本)
public class StaticTextSqlNode implements SqlNode {
private final String text;
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text); // 直接追加文本
return true;
}
}

DynamicSqlSource vs RawSqlSource 对比

维度DynamicSqlSourceRawSqlSource
SQL 类型含动态标签(if、foreach 等)纯静态 SQL
SQL 生成时机运行时(每次执行)启动时(只生成一次)
性能低(每次都要解析 OGNL、拼接 SQL)高(SQL 已生成,直接使用)
灵活性高(可根据参数动态生成)低(SQL 固定)
适用场景条件查询、批量操作简单查询、固定 SQL

性能数据对比

相同 SQL(简单查询),执行 10000 次:
DynamicSqlSource:
- 每次执行:OGNL 求值 + SQL 拼接 + JDBC 执行
- 总耗时:~1500ms
- 其中 OGNL + 拼接:~500ms(33%)
RawSqlSource:
- 启动时生成 SQL,执行时直接使用
- 总耗时:~1000ms
- OGNL + 拼接:0ms(启动时已完成)
性能提升:~30%(简单 SQL)

Q7:SQL 片段复用 <sql><include> 如何使用?有什么最佳实践?中频

Section titled “Q7:SQL 片段复用 <sql> 和 <include> 如何使用?有什么最佳实践?”

基本用法

<!-- 定义可复用的 SQL 片段 -->
<sql id="userColumns">
id, name, age, email, create_time, update_time
</sql>
<sql id="userTable">
FROM user
</sql>
<sql id="userWhere">
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</sql>
<!-- 使用 include 引用 -->
<select id="selectUsers" resultType="User">
SELECT
<include refid="userColumns"/>
<include refid="userTable"/>
<include refid="userWhere"/>
ORDER BY create_time DESC
</select>
<!-- 生成的 SQL -->
SELECT id, name, age, email, create_time, update_time
FROM user
WHERE name LIKE '%张%' AND status = 1
ORDER BY create_time DESC

传递变量给 SQL 片段

<!-- 定义带变量的 SQL 片段 -->
<sql id="userColumnsWithAlias">
${alias}.id, ${alias}.name, ${alias}.age, ${alias}.email
</sql>
<!-- 使用时传递变量 -->
<select id="selectUsersWithAlias" resultType="User">
SELECT
<include refid="userColumnsWithAlias">
<property name="alias" value="u"/>
</include>
FROM user u
</select>
<!-- 生成的 SQL -->
SELECT u.id, u.name, u.age, u.email FROM user u

跨 Mapper 引用

BaseMapper.xml
<mapper namespace="com.example.BaseMapper">
<sql id="commonColumns">
id, create_time, update_time, create_by, update_by
</sql>
</mapper>
<!-- UserMapper.xml -->
<mapper namespace="com.example.UserMapper">
<select id="selectUsers" resultType="User">
SELECT
<include refid="com.example.BaseMapper.commonColumns"/>,
name, age, email
FROM user
</select>
</mapper>

最佳实践

实践说明示例
通用字段抽取所有表共有的字段(create_time 等)<sql id="commonColumns">
表名/别名参数化JOIN 查询时动态指定别名${alias}.id
条件片段复用多个查询共用的 WHERE 条件<sql id="userWhere">
避免过度拆分只抽取真正复用的片段,避免碎片化单次使用的 SQL 不抽取

注意事项

  1. <sql> 片段只在当前 Mapper 内可见(除非使用全限定名跨 Mapper 引用)
  2. 变量使用 ${} 而非 #{}:因为 SQL 片段是文本替换,不是参数绑定
  3. 嵌套 <include>:可以在 <sql> 片段中再引用其他 <sql> 片段
<!-- 嵌套 include -->
<sql id="userBaseColumns">
id, name, age
</sql>
<sql id="userAllColumns">
<include refid="userBaseColumns"/>, email, create_time
</sql>
<select id="selectUsers" resultType="User">
SELECT <include refid="userAllColumns"/> FROM user
</select>
<!-- 生成的 SQL -->
SELECT id, name, age, email, create_time FROM user


<select id="queryProducts" resultType="Product">
SELECT * FROM product
<where>
<!-- 品牌筛选(多选) -->
<if test="brandIds != null and brandIds.size() > 0">
AND brand_id IN
<foreach collection="brandIds" item="brandId" open="(" separator="," close=")">
#{brandId}
</foreach>
</if>
<!-- 价格区间 -->
<if test="minPrice != null">
AND price &gt;= #{minPrice}
</if>
<if test="maxPrice != null">
AND price &lt;= #{maxPrice}
</if>
<!-- 分类(支持多级) -->
<if test="categoryPath != null and categoryPath != ''">
AND category_path LIKE CONCAT(#{categoryPath}, '%')
</if>
<!-- 关键词搜索(商品名或描述) -->
<if test="keyword != null and keyword != ''">
AND (name LIKE CONCAT('%', #{keyword}, '%')
OR description LIKE CONCAT('%', #{keyword}, '%'))
</if>
<!-- 状态 -->
<if test="status != null">
AND status = #{status}
</if>
<!-- 库存筛选 -->
<if test="inStock != null and inStock">
AND stock > 0
</if>
</where>
<!-- 排序 -->
<choose>
<when test="sortField == 'price' and sortOrder == 'asc'">
ORDER BY price ASC
</when>
<when test="sortField == 'price' and sortOrder == 'desc'">
ORDER BY price DESC
</when>
<when test="sortField == 'sales'">
ORDER BY sales DESC
</when>
<otherwise>
ORDER BY create_time DESC
</otherwise>
</choose>
<!-- 分页 -->
<if test="pageSize != null and pageSize > 0">
LIMIT #{offset}, #{pageSize}
</if>
</select>
<!-- 单条 SQL 批量更新(高性能) -->
<update id="batchUpdate">
UPDATE user SET
name = CASE id
<foreach collection="users" item="user">
WHEN #{user.id} THEN #{user.name}
</foreach>
END,
age = CASE id
<foreach collection="users" item="user">
WHEN #{user.id} THEN #{user.age}
</foreach>
END
WHERE id IN
<foreach collection="users" item="user" open="(" separator="," close=")">
#{user.id}
</foreach>
</update>
<!-- 生成的 SQL -->
UPDATE user SET
name = CASE id
WHEN 1 THEN '张三'
WHEN 2 THEN '李四'
END,
age = CASE id
WHEN 1 THEN 18
WHEN 2 THEN 20
END
WHERE id IN (1, 2)

性能对比

方案1000 条更新耗时说明
循环单条更新~8s1000 次 SQL 执行
分号分隔(allowMultiQueries)~1s1000 条 SQL,1 次网络传输
CASE WHEN(单条 SQL)~0.3s1 条 SQL,性能最优
<select id="selectOrdersByMonth" resultType="Order">
SELECT * FROM order_${yearMonth}
<where>
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>
<!-- 使用 -->
List<Order> orders = orderMapper.selectOrdersByMonth(
new HashMap<String, Object>() {{
put("yearMonth", "202401"); // 查询 order_202401 表
put("userId", 123L);
}}
);
<!-- 生成的 SQL -->
SELECT * FROM order_202401 WHERE user_id = 123

  1. if vs choose:多条件独立判断 vs 单选分支(类似 switch-case)
  2. where 标签:智能生成 WHERE 关键字,自动去掉多余的 AND/OR 前缀
  3. set 标签:UPDATE 专用,智能生成 SET 关键字,自动去掉多余的逗号
  4. trim 标签:通用方案,自定义前缀后缀处理
  5. foreach 标签:批量查询(IN)、批量插入(VALUES)、批量更新(CASE WHEN)
  6. OGNL 表达式:字符串比较用 ==,特殊字符需转义,集合判空用 size()
  7. 底层原理:SqlNode 组成的 AST 树,递归 apply() 生成 SQL
  8. 性能优化:静态 SQL 用 RawSqlSource,动态 SQL 用 DynamicSqlSource
  9. SQL 片段<sql> 定义,<include> 引用,支持跨 Mapper
  10. 性能陷阱:IN 查询参数过多、批量插入数据量过大、foreach 嵌套循环