MyBatis 动态 SQL 深度解析
面试官:MyBatis 的动态 SQL 你用过哪些标签?
你:常用的有
if、choose、where、set、foreach、trim、bind、sql、include。面试官:
where标签和直接写WHERE 1=1有什么区别?你:
where标签是智能标签,会自动处理多余的AND/OR前缀,并且只有在有子元素输出内容时才生成WHERE关键字。而WHERE 1=1是一种妥协写法,虽然性能损耗微小(数据库优化器会优化掉),但不够优雅,且无法处理所有边界情况。面试官:能说说 MyBatis 动态 SQL 的底层原理吗?它是怎么解析这些标签的?
这个追问直击底层——能说出 SqlNode 组成的 AST 树、OGNL 表达式求值、DynamicSqlSource vs RawSqlSource 的候选人,才算真正掌握动态 SQL 的精髓。
链式追问一:核心标签
Section titled “链式追问一:核心标签”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=1 | WHERE 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 标签的行为:
- 如果子元素有输出 → 生成
SET关键字 - 自动去掉结尾的逗号(
,) - 如果子元素没有输出 → 不生成
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 = 1Q4: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 | 平衡性能与内存 |
链式追问二:OGNL 表达式
Section titled “链式追问二:OGNL 表达式”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 > 18"> <!-- > 转义为 > --><if test="age < 60"> <!-- < 转义为 < -->
<!-- 或使用 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>常见陷阱与解决方案:
| 陷阱 | 错误写法 | 正确写法 | 原因 |
|---|---|---|---|
| 字符串比较用 equals | test="status.equals('ACTIVE')" | test='status == "ACTIVE"' | OGNL 会把 'ACTIVE' 解析为变量名 |
| 特殊字符未转义 | test="age > 18" | test="age > 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>链式追问三:动态 SQL 原理
Section titled “链式追问三:动态 SQL 原理”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 对比:
| 维度 | DynamicSqlSource | RawSqlSource |
|---|---|---|
| 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_timeFROM userWHERE name LIKE '%张%' AND status = 1ORDER 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 引用:
<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 不抽取 |
注意事项:
<sql>片段只在当前 Mapper 内可见(除非使用全限定名跨 Mapper 引用)- 变量使用
${}而非#{}:因为 SQL 片段是文本替换,不是参数绑定 - 嵌套
<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高频面试题总结
Section titled “高频面试题总结”实战案例:复杂动态 SQL
Section titled “实战案例:复杂动态 SQL”案例 1:多条件组合查询
Section titled “案例 1:多条件组合查询”<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 >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{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>案例 2:批量更新(CASE WHEN)
Section titled “案例 2:批量更新(CASE WHEN)”<!-- 单条 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 SETname = CASE id WHEN 1 THEN '张三' WHEN 2 THEN '李四'END,age = CASE id WHEN 1 THEN 18 WHEN 2 THEN 20ENDWHERE id IN (1, 2)性能对比:
| 方案 | 1000 条更新耗时 | 说明 |
|---|---|---|
| 循环单条更新 | ~8s | 1000 次 SQL 执行 |
| 分号分隔(allowMultiQueries) | ~1s | 1000 条 SQL,1 次网络传输 |
| CASE WHEN(单条 SQL) | ~0.3s | 1 条 SQL,性能最优 |
案例 3:动态表名(分表查询)
Section titled “案例 3:动态表名(分表查询)”<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关键知识点速记
Section titled “关键知识点速记”- if vs choose:多条件独立判断 vs 单选分支(类似 switch-case)
- where 标签:智能生成 WHERE 关键字,自动去掉多余的 AND/OR 前缀
- set 标签:UPDATE 专用,智能生成 SET 关键字,自动去掉多余的逗号
- trim 标签:通用方案,自定义前缀后缀处理
- foreach 标签:批量查询(IN)、批量插入(VALUES)、批量更新(CASE WHEN)
- OGNL 表达式:字符串比较用
==,特殊字符需转义,集合判空用size() - 底层原理:SqlNode 组成的 AST 树,递归 apply() 生成 SQL
- 性能优化:静态 SQL 用 RawSqlSource,动态 SQL 用 DynamicSqlSource
- SQL 片段:
<sql>定义,<include>引用,支持跨 Mapper - 性能陷阱:IN 查询参数过多、批量插入数据量过大、foreach 嵌套循环